V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
llsquaer
V2EX  ›  Python

发现一个 Python bug,最初以为是引用问题,后来逐步 print 看还真是 bug

  •  1
     
  •   llsquaer · 2024-05-10 00:10:51 +08:00 · 5140 次点击
    这是一个创建于 379 天前的主题,其中的信息可能已经有所发展或是发生改变。

    列表字典中 随机一个字典增加 key ,再放入新列表中。出现预期不符。 直接上代码

    有 bug 的情况

    aaa = [
        {'id': 35,'src':'xxx'},
        {'id': 36,'src':'xxx'},
        {'id': 37,'src':'xxx'},
        {'id': 38,'src':'xxx'},
    ]
    
    combinations = []
    
    for i in range(5):
        cname = f'张三-{i}'
        ccc = random.choice(aaa)
        ccc.update({'cname': cname})
        print(ccc)                  # 这里的结果符合预期
    
        combinations.append(ccc)
    
    print(combinations)             # 但是这里就错了
    
    

    返回结果

    {'id': 37, 'src': 'xxx', 'cname': '张三-0'}
    {'id': 38, 'src': 'xxx', 'cname': '张三-1'}
    {'id': 35, 'src': 'xxx', 'cname': '张三-2'}
    {'id': 38, 'src': 'xxx', 'cname': '张三-3'}
    {'id': 36, 'src': 'xxx', 'cname': '张三-4'}
    # 以上 print 结果是对的
    
    [{'id': 37, 'src': 'xxx', 'cname': '张三-0'}, {'id': 38, 'src': 'xxx', 'cname': '张三-3'}, {'id': 35, 'src': 'xxx', 'cname': '张三-2'}, {'id': 38, 'src': 'xxx', 'cname': '张三-3'}, {'id': 36, 'src': 'xxx', 'cname': '张三-4'}]
    # 但是这里打印新生成的 combinations 列表就出现两个 `张三-3` 
    

    改代码

    后来想起来是引用对象问题,需要浅复制下.即只需要将 ccc = random.choice(aaa)改为ccc = random.choice(aaa).copy() 就符合预期了.

    bug 的疑问

    bug 问题在于示例里,单个 print 结果和添加到列表里的结果不一致.

    python 版本 3.10.8

    55 条回复
    darcyC
        1
    darcyC  
       2024-05-10 00:15:09 +08:00
    你 print 的是当时那一刹那的值哦,你最后所谓的列表里的内容是最后 print 的哦,那也是那一刹那的值哦。
    thinkershare
        2
    thinkershare  
       2024-05-10 00:16:37 +08:00
    这个根本就不是 bug ,就是可变对象的问题,你的 list 里面在最后的时候,有两个位置的元素引用了同一个对象。仅此而已。
    llsquaer
        3
    llsquaer  
    OP
       2024-05-10 00:19:28 +08:00
    @darcyC 如果 print 的值是正确的,那么添加到 combinations 里的值应该也是按照 print 顺序进行添加的啊,再之后并没有做修改字典的动作了。
    ktyang
        4
    ktyang  
       2024-05-10 00:20:12 +08:00   ❤️ 1
    这是来钓鱼的嘛?
    thinkershare
        5
    thinkershare  
       2024-05-10 00:21:30 +08:00   ❤️ 4
    @ktyang 感觉的确有钓鱼的嫌疑。
    llsquaer
        6
    llsquaer  
    OP
       2024-05-10 00:27:02 +08:00
    @thinkershare 应该是对象引用问题,但是为啥 print 的值是对的?没错乱
    thinkershare
        7
    thinkershare  
       2024-05-10 00:39:14 +08:00
    @llsquaer
    因为状态随着时间的流逝被改变了(代码在线程上执行,已执行代码随时都可以改变内存中对象的状态).
    一个对象在不同时刻,完全可以显示不同状态,print 是需要将对象转换为字符串(字符串序列化).
    这个转化的时刻,会冻结那个时刻对象的字符串表示,而随着代码继续执行,这个对象被改变了。
    print 打印时候首先要获得这个对象的字符串序列化表示,然后调用系统提供的接口将字符串使用指定字体在屏幕上渲染出来,因此一切都要看某一个时刻的状态。你不能对比不同时刻对一个对象状态(除非这个对象是不可变对象).
    这个过程看似简单,实际还是涉及到很多乱七八糟的概念。
    DOLLOR
        8
    DOLLOR  
       2024-05-10 00:51:38 +08:00   ❤️ 2
    @llsquaer
    “再之后并没有做修改字典的动作了”——这话错了。
    后面循环的时候,random.choice 仍会抽取到之前同一个 ccc ,然后 update 掉了。

    你要明白一点,把 aaa 里的元素直接 append 到 combinations 里,combinations 的元素跟 aaa 的元素都是相同的引用。
    任何对 aaa 元素的修改,都会影响到 combinations 里的元素。

    类似的例子
    list1 = [{'name': '张三'}]
    list2 = []

    # 抽取 list1 的元素,加入 list2
    item = list1[0]
    list2.append(item)

    print(list1, list2) # 都是 [{'name': '张三'}]
    item['name'] = '李四' # 修改了 list1 里的 item ,但 list2 里的也跟着变了
    print(list1, list2) # 都是 [{'name': '李四'}]
    fatigue
        9
    fatigue  
       2024-05-10 00:56:42 +08:00 via iPhone   ❤️ 2
    学学用调试器吧,愁
    iintothewind
        10
    iintothewind  
       2024-05-10 06:59:02 +08:00
    写代码还是建议用不可变数据结构, 和无副作用的操作,
    用可变数据结构和命令式操作, 你就需要对语句块生命周期内"操作的对象"的内部状态负责,
    要不然就是自找麻烦.
    Goooooos
        11
    Goooooos  
       2024-05-10 07:17:45 +08:00 via Android
    假设你列表里面只有一个元素,循环多少次更新都是同一个元素。

    另外你把 combinations 改为 set 就明白了。
    phrack
        12
    phrack  
       2024-05-10 07:47:32 +08:00 via iPhone   ❤️ 1
    mutable ,immutable 的区别,很常见的 python 问题。

    我也怀疑楼主钓鱼。
    Marlon
        13
    Marlon  
       2024-05-10 07:56:57 +08:00
    新手可能会遇到这个问题,理解可变对象和不可变对象就好了,类似于对象的引用。
    Muniesa
        14
    Muniesa  
       2024-05-10 08:00:47 +08:00 via Android
    不是,你没发现 id38 被选了两次吗?第二次修改的时候会覆盖上一次的修改啊,你在循环里打印下 combinations 就知道咋回事了吧
    shinession
        15
    shinession  
       2024-05-10 08:04:06 +08:00
    还好 OP 上代码了, 不然还真以为是啥 bug
    lakitus
        16
    lakitus  
       2024-05-10 08:37:00 +08:00
    test
    lakitus
        17
    lakitus  
       2024-05-10 08:39:31 +08:00   ❤️ 1
    这应该算是 python 中可变对象的原处修改这一块的知识,op 有时间可以把 python 里面的共享引用、驻留、对象拷贝机制(浅复制、深复制) 这一块的知识过一遍
    zhtyytg
        18
    zhtyytg  
       2024-05-10 08:44:14 +08:00
    钓鱼?
    lsk569937453
        19
    lsk569937453  
       2024-05-10 08:45:52 +08:00   ❤️ 3
    现在的人都这么自信了吗?代码不符合自己预期,一眼就是编程语言 bug.......
    编程语言有 bug 吗?有。但不是一些新手能发现的。如果你发现程序不符合你的预期,首先应该是反思程序是不是有问题,或者拿给 chatGpt 解读一下也好,上来就是"发现一个编程语言 bug"。承包了我今天的笑料。
    customsshen
        20
    customsshen  
       2024-05-10 08:48:52 +08:00
    最后 print(aaa),看看结果就应该理解了
    cyrivlclth
        21
    cyrivlclth  
       2024-05-10 08:49:38 +08:00   ❤️ 1
    钓鱼司马
    anzu
        22
    anzu  
       2024-05-10 08:56:26 +08:00 via iPhone
    既然你觉得最后打印 combinations 的结果是错的,那么就应该也在 for 循环中打印 combinations 的值,观察其是怎么变化的。
    FYFX
        23
    FYFX  
       2024-05-10 09:07:03 +08:00
    你打印 ccc 的时候获得是当前 ccc.__repr__()的值,让后放到 combinations 里的 ccc 只是引用,后面修改了这个 ccc 之后再 print 的结果就是不一样啊
    theprimone
        24
    theprimone  
       2024-05-10 09:13:25 +08:00
    @phrack 大多数语言都有这个问题吧,有语言层面默认 immutable 的吗?
    accelerator1
        25
    accelerator1  
       2024-05-10 09:14:06 +08:00
    进来之前就能猜到 LZ 要被群嘲了
    superrichman
        26
    superrichman  
       2024-05-10 09:15:52 +08:00
    把 id 打出来,你会发现其实有多个 id 一样的元素,他们指向同一个对象

    print([id(x) for x in combinations])

    学一下 c 的指针就能理解了
    Kinnice
        27
    Kinnice  
       2024-05-10 09:34:13 +08:00 via Android   ❤️ 2
    如果你不是某个语言的 Master ,那你遇到的不符合你的理解的现象,基本都是你的理解不到位.
    InkStone
        28
    InkStone  
       2024-05-10 09:37:37 +08:00
    Python 的作用域规则跟 C 不一样,cname 在出了 for 循环之后还是一个有效的对象,在下一次 for 循环中做的事情不是重新绑定了这个对象,而是修改了这个对象的值。
    InkStone
        29
    InkStone  
       2024-05-10 09:38:42 +08:00
    @theprimone Rust 呀
    ck65
        30
    ck65  
       2024-05-10 09:47:45 +08:00 via iPhone
    一个观察不一定不准,发现了各个语言 bug 的新手,多半是来到了作用域的门前。
    theprimone
        31
    theprimone  
       2024-05-10 10:17:21 +08:00
    @InkStone #29 这样啊,写过 Rust 的 Hello World ,还不知道这么硬核呢
    djangovcps
        32
    djangovcps  
       2024-05-10 10:20:38 +08:00
    能怀疑语言的内置容器有 bug ,我是没想到的
    HashV2
        33
    HashV2  
       2024-05-10 10:32:46 +08:00
    没有问题 循环内的打印对象在后续的循环过程中被修改了
    mylifcc
        34
    mylifcc  
       2024-05-10 10:38:23 +08:00
    我是鱼
    visper
        35
    visper  
       2024-05-10 10:46:26 +08:00
    鱼,好大的鱼,虎纹鲨鱼
    hooych
        36
    hooych  
       2024-05-10 10:47:28 +08:00
    代码执行的顺序并非是严格遵守代码逻辑的顺序,在不发生相关冲突的情况下,会发生顺序调整以优化性能。
    1018ji
        37
    1018ji  
       2024-05-10 10:55:20 +08:00
    好大的 bug
    agegcn
        38
    agegcn  
       2024-05-10 11:12:03 +08:00
    不符合预期就是 python 的 bug 。太自信了
    CloveAndCurrant
        39
    CloveAndCurrant  
       2024-05-10 11:12:39 +08:00
    {'id': 38, 'src': 'xxx', 'cname': '张三-1'}、{'id': 38, 'src': 'xxx', 'cname': '张三-3'}这两个其实指向的是同一个字典,你更改一个,相当于都改了,字典的.copy()方法是浅拷贝,浅拷贝后就是指向不同的字典了。
    Goooooos
        40
    Goooooos  
       2024-05-10 11:18:00 +08:00   ❤️ 1
    OP 可能真不适合编程。状态值都不懂。
    Masterlxj
        41
    Masterlxj  
       2024-05-10 11:22:52 +08:00
    因为 python 中列表和字典均为可变对象,列表内元素可变对象是引用,你 ID38 的对象存进去 2 次,两个元素指向的是同一个地址。for 循环中打印的是瞬时值,打印 combinations 是最终值。
    jstony
        42
    jstony  
       2024-05-10 11:28:11 +08:00
    op 但凡把调试器打开看一眼都不会这么自信
    tomczhen
        43
    tomczhen  
       2024-05-10 12:55:27 +08:00
    看到标题逐步 print 就能猜到内容了。
    hxysnail
        44
    hxysnail  
       2024-05-10 13:19:26 +08:00
    这个行为很正常啊,指针或引用类型的数据都是这样的

    有空可以了解一下语言的内部机制,你就会本能地避开某些机制性的坑,比如 Python 对象模型可以参考这个:

    https://fasionchan.com/python-source/object-model/overview
    iyaozhen
        45
    iyaozhen  
       2024-05-10 13:43:31 +08:00   ❤️ 1
    这个对于编程新手来说绝对是个门槛

    首先有个概念 dict/map 、list 是可变的,不管这个 map 放那里,你操作的都是它的指针(或者说是一个包含其内存地址的数据结构),简单理解类似 Windows 的快捷方式。不管你把这个 map 赋值给多少新的变量,都是复制了多个快捷方式

    你肯定也知道 id:38 被随机选出来了两次,第二次 cname='张三-3',相当于修改了快捷方式对应的 map ,但往 combinations 里面放的都是其快捷方式。最后 print(combinations),就是拿着一个个快捷方式,去找对应的 map ,那当然 id:38 的 cname 都是'张三-3'了。.copy()嘛,则是复制原文件,而不是快捷方式了。

    至于为什么要这样,就是为了节省内存。

    更深入的话还可以看下 Copy On Write 机制
    deplives
        46
    deplives  
       2024-05-10 13:50:13 +08:00
    建议先学习 c ,搞懂 指针相关的内容吧,
    还有,别一上来就是语言的 bug 。多找找自己的原因。
    Arrowing
        47
    Arrowing  
       2024-05-10 14:26:19 +08:00
    有没有可能一种可能,combinations 数组里的第二个和第四个的元素地址是一样的?
    hellomsg
        48
    hellomsg  
       2024-05-10 15:00:41 +08:00
    跟 python 没关系,你换其他语言也一样。顺序执行已经很简单了,实在不行你把 for 拆开手动写成五条,再在脑子里运行一遍。
    caiqichang
        49
    caiqichang  
       2024-05-10 15:38:59 +08:00
    caiqichang
        50
    caiqichang  
       2024-05-10 15:43:40 +08:00
    Cu635
        51
    Cu635  
       2024-05-10 15:49:46 +08:00
    @iyaozhen #45
    感觉 python 的这个特性还不如 C 语言的指针呢,好歹 c 语言指针是显式的,这种隐蔽的太坑人了。
    krixaar
        52
    krixaar  
       2024-05-10 16:26:10 +08:00
    @Cu635 #51 该把 VB 那套 ByVal ByRef 学过来🤣
    honjow
        53
    honjow  
       2024-05-11 03:31:04 +08:00 via iPhone
    看到标题就猜到大概是引用问题了。真就那么自信呗
    honjow
        54
    honjow  
       2024-05-11 03:36:45 +08:00 via iPhone
    @Goooooos 不懂问题倒不大,好好学就行,反而是这种不符合自己预期结果就说是语言 bug 的态度才是大问题
    llsquaer
        55
    llsquaer  
    OP
       2024-05-11 09:28:58 +08:00
    @DOLLOR 是当时脑子犯怵啦。 在循环里,上一次的引用对象,被下一次循环 update 更改了。。当时没转过弯,老是死磕 print 去了。

    还有一个是当时标题写的夸张点了,今天回来还在想为啥会取一个这个标题。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1170 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 48ms · UTC 23:47 · PVG 07:47 · LAX 16:47 · JFK 19:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.