发现一个 Python bug,最初以为是引用问题,后来逐步 print 看还真是 bug - V2EX
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 5515 次点击
    这是一个创建于 585 天前的主题,其中的信息可能已经有所发展或是发生改变。

    列表字典中 随机一个字典增加 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 打印时候首先要获得这个对象的字符串序列化表示,然后调用系统提供的接口将字符串使用指定字体在屏幕上渲染出来,因此一切都要看某一个时刻的状态。你不能对比不同时刻对一个对象状态(除非这个对象是不可变对象).
    这个过程看似简单,实际还是涉及到很多乱七八糟的概念。
    AV1
        8
    AV1  
       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 呀
    mogita
        30
    mogita  
       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 拆开手动写成五条,再在脑子里运行一遍。
    dandeli0n
        49
    dandeli0n  
       2024-05-10 15:38:59 +08:00
    dandeli0n
        50
    dandeli0n  
       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     Solana     4430 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 10:04 PVG 18:04 LAX 02:04 JFK 05:04
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86