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 · 196 天前 · 4838 次点击
    这是一个创建于 196 天前的主题,其中的信息可能已经有所发展或是发生改变。

    列表字典中 随机一个字典增加 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  
       196 天前
    你 print 的是当时那一刹那的值哦,你最后所谓的列表里的内容是最后 print 的哦,那也是那一刹那的值哦。
    thinkershare
        2
    thinkershare  
       196 天前
    这个根本就不是 bug ,就是可变对象的问题,你的 list 里面在最后的时候,有两个位置的元素引用了同一个对象。仅此而已。
    llsquaer
        3
    llsquaer  
    OP
       196 天前
    @darcyC 如果 print 的值是正确的,那么添加到 combinations 里的值应该也是按照 print 顺序进行添加的啊,再之后并没有做修改字典的动作了。
    ktyang
        4
    ktyang  
       196 天前   ❤️ 1
    这是来钓鱼的嘛?
    thinkershare
        5
    thinkershare  
       196 天前   ❤️ 4
    @ktyang 感觉的确有钓鱼的嫌疑。
    llsquaer
        6
    llsquaer  
    OP
       196 天前
    @thinkershare 应该是对象引用问题,但是为啥 print 的值是对的?没错乱
    thinkershare
        7
    thinkershare  
       196 天前
    @llsquaer
    因为状态随着时间的流逝被改变了(代码在线程上执行,已执行代码随时都可以改变内存中对象的状态).
    一个对象在不同时刻,完全可以显示不同状态,print 是需要将对象转换为字符串(字符串序列化).
    这个转化的时刻,会冻结那个时刻对象的字符串表示,而随着代码继续执行,这个对象被改变了。
    print 打印时候首先要获得这个对象的字符串序列化表示,然后调用系统提供的接口将字符串使用指定字体在屏幕上渲染出来,因此一切都要看某一个时刻的状态。你不能对比不同时刻对一个对象状态(除非这个对象是不可变对象).
    这个过程看似简单,实际还是涉及到很多乱七八糟的概念。
    DOLLOR
        8
    DOLLOR  
       196 天前   ❤️ 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  
       196 天前 via iPhone   ❤️ 2
    学学用调试器吧,愁
    iintothewind
        10
    iintothewind  
       195 天前
    写代码还是建议用不可变数据结构, 和无副作用的操作,
    用可变数据结构和命令式操作, 你就需要对语句块生命周期内"操作的对象"的内部状态负责,
    要不然就是自找麻烦.
    Goooooos
        11
    Goooooos  
       195 天前 via Android
    假设你列表里面只有一个元素,循环多少次更新都是同一个元素。

    另外你把 combinations 改为 set 就明白了。
    phrack
        12
    phrack  
       195 天前 via iPhone   ❤️ 1
    mutable ,immutable 的区别,很常见的 python 问题。

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

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

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

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

    https://fasionchan.com/python-source/object-model/overview
    iyaozhen
        45
    iyaozhen  
       195 天前   ❤️ 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  
       195 天前
    建议先学习 c ,搞懂 指针相关的内容吧,
    还有,别一上来就是语言的 bug 。多找找自己的原因。
    Arrowing
        47
    Arrowing  
       195 天前
    有没有可能一种可能,combinations 数组里的第二个和第四个的元素地址是一样的?
    hellomsg
        48
    hellomsg  
       195 天前
    跟 python 没关系,你换其他语言也一样。顺序执行已经很简单了,实在不行你把 for 拆开手动写成五条,再在脑子里运行一遍。
    caiqichang
        49
    caiqichang  
       195 天前
    Cu635
        51
    Cu635  
       195 天前
    @iyaozhen #45
    感觉 python 的这个特性还不如 C 语言的指针呢,好歹 c 语言指针是显式的,这种隐蔽的太坑人了。
    krixaar
        52
    krixaar  
       195 天前
    @Cu635 #51 该把 VB 那套 ByVal ByRef 学过来🤣
    honjow
        53
    honjow  
       194 天前 via iPhone
    看到标题就猜到大概是引用问题了。真就那么自信呗
    honjow
        54
    honjow  
       194 天前 via iPhone
    @Goooooos 不懂问题倒不大,好好学就行,反而是这种不符合自己预期结果就说是语言 bug 的态度才是大问题
    llsquaer
        55
    llsquaer  
    OP
       194 天前
    @DOLLOR 是当时脑子犯怵啦。 在循环里,上一次的引用对象,被下一次循环 update 更改了。。当时没转过弯,老是死磕 print 去了。

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