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
lanlanye
V2EX  ›  Python

你们说 Python 这个是 Bug 还是 Feature?

  •  1
     
  •   lanlanye ·
    laipz8200 · 2022-08-30 15:44:32 +08:00 · 7127 次点击
    这是一个创建于 849 天前的主题,其中的信息可能已经有所发展或是发生改变。
    class Foo:
        def __init__(self, s: set = set()):
            self.s = s
    
    
    if __name__ == "__main__":
        f1 = Foo()
        f2 = Foo()
        f1.s.add(1)
        f1.s.add(2)
        f1.s.add(3)
        print(f2.s)
    
    

    版本 3.10.4

    结果会是什么呢?

    71 条回复    2023-01-08 12:50:47 +08:00
    wxf666
        1
    wxf666  
       2022-08-30 15:47:03 +08:00
    不是 {1, 2, 3} 么?
    MoYi123
        2
    MoYi123  
       2022-08-30 15:47:18 +08:00
    老问题了. {1,2,3}啊.
    Jooooooooo
        3
    Jooooooooo  
       2022-08-30 15:48:42 +08:00
    bug 是说不符合设计预期的行为. 这显然不可能是 bug. (虽然我不懂 python
    lanlanye
        4
    lanlanye  
    OP
       2022-08-30 15:50:26 +08:00   ❤️ 1
    我猜这种写法大概相当于

    ```
    default = set([1])


    class Foo:
    def __init__(self, s=default):
    self.s = s
    ```
    lusi1990
        5
    lusi1990  
       2022-08-30 15:51:04 +08:00 via Android
    这样用会引发内存泄露啊
    lanlanye
        6
    lanlanye  
    OP
       2022-08-30 15:51:58 +08:00
    @Jooooooooo 应该算符合预期吧,不过感觉有点反直觉,可能我下意识地以为这样写相当于每次创建一个新的 set
    lanlanye
        7
    lanlanye  
    OP
       2022-08-30 15:53:56 +08:00
    @lusi1990 会吗?不是每次实例化时 copy 一个指向 set 的引用吗?
    wxf666
        8
    wxf666  
       2022-08-30 15:54:29 +08:00   ❤️ 2
    @lanlanye 人家[官方文档]( https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function)写得很清楚了:

    > **默认形参值会在执行函数定义时按从左至右的顺序被求值。** 这意味着当函数被定义时将对表达式求值一次,相同的“预计算”值将在每次调用时被使用。 这一点在默认形参为可变对象,例如列表或字典的时候尤其需要重点理解:如果函数修改了该对象(例如向列表添加了一项),则实际上默认值也会被修改。 这通常不是人们所想要的。 绕过此问题的一个方法是使用 None 作为默认值,并在函数体中显式地对其进测试,例如:

    ```python
    def whats_on_the_telly(penguin=None):
    __if penguin is None:
    ____penguin = []
    __penguin.append("property of the zoo")
    __return penguin
    ```
    est
        9
    est  
       2022-08-30 15:57:07 +08:00   ❤️ 4
    当年 1.6 亿美金估值的公司—— Digg 是如何被一句 Python 函数可变默参 毁掉的

    /t/467817
    lanlanye
        10
    lanlanye  
    OP
       2022-08-30 16:04:23 +08:00
    @est 惊了居然还有这种事情……这个设计确实挺反直觉的,而且很难注意到,刚才发现 PyCharm 对这种情况有提示,Pyright 没有
    Drahcir
        11
    Drahcir  
       2022-08-30 16:08:07 +08:00
    开发了这么多年 Python ,还真没在意这个问题,学习了。
    zone10
        12
    zone10  
       2022-08-30 16:22:31 +08:00   ❤️ 1
    算是设计失误, 就跟 Null 的设计失误一样, 反正我用过 Rust 受不了 Go 作为一门比较新的语言居然不保证空类型安全, 特别是结构体默认为 nil 太 tm 容易出错了
    mxT52CRuqR6o5
        13
    mxT52CRuqR6o5  
       2022-08-30 16:24:14 +08:00
    这个我知道,参数默认值在函数声明时就初始化了,像 js 的话参数默认值是在函数执行时初始化的
    fds
        14
    fds  
       2022-08-30 16:25:10 +08:00   ❤️ 1
    现在流行用 https://docs.python.org/3/library/dataclasses.html#dataclasses.field
    ```py
    @dataclass
    class C:
    mylist: list[int] = field(default_factory=list)

    c = C()
    c.mylist += [1, 2, 3]
    ```
    20015jjw
        15
    20015jjw  
       2022-08-30 16:25:13 +08:00 via iPhone
    正常 lint 都会提示
    老问题了
    Aloento
        16
    Aloento  
       2022-08-30 16:27:53 +08:00 via iPhone
    @est 所以我说谁用 python 谁倒霉
    kkhaike
        17
    kkhaike  
       2022-08-30 16:31:25 +08:00
    我刚学 python 也踩过这个坑,默认值要用 None
    liuran
        18
    liuran  
       2022-08-30 16:33:24 +08:00 via Android
    不进行 deep copy 算是 python 的 feature 吧,之前自己刚开始用的时候就遇到了这个坑,然后认真学习了一下 shallow copy 和 deep copy
    abersheeran
        19
    abersheeran  
       2022-08-30 16:41:59 +08:00
    新手常犯的错误了……
    lyang
        20
    lyang  
       2022-08-30 16:45:52 +08:00
    这个例子不知道,但是类似的碰到过,可以理解
    Symo
        21
    Symo  
       2022-08-30 16:46:59 +08:00
    应该说是一个反直觉的设计缺陷.
    rev1si0n
        22
    rev1si0n  
       2022-08-30 16:53:00 +08:00
    不是 Bug 也不是 Feature
    littlewing
        23
    littlewing  
       2022-08-30 16:53:35 +08:00
    所以说 py 垃圾语言啊
    yazinnnn
        24
    yazinnnn  
       2022-08-30 17:04:44 +08:00
    不会 python, 试了试可以这样写

    class Foo:
    def __init__(self, createSet = lambda : set()):
    self.s = createSet()

    有点反直觉, 但是想了想又算合理
    Leviathann
        25
    Leviathann  
       2022-08-30 17:08:35 +08:00
    彻底的设计缺陷
    和 js 的 var 和 this 有得一比 甚至更恶劣
    wenhaoy
        26
    wenhaoy  
       2022-08-30 17:09:02 +08:00
    为什么操作 f1 ,会改变 f2 的值?
    hhhhhh123
        27
    hhhhhh123  
       2022-08-30 17:14:24 +08:00
    print(id(f1), id(f2))
    print(id(f1.s), id(f2.s))

    140294972551120 140294972551024
    140295241401280 140295241401280

    破案了。
    ipwx
        28
    ipwx  
       2022-08-30 17:15:16 +08:00
    不是 Bug 也不是 Feature 。我觉得有过 C++ 经验的程序员更容易接受这个设定。
    sillydaddy
        29
    sillydaddy  
       2022-08-30 17:17:15 +08:00   ❤️ 1
    @ipwx
    为啥 C++要躺枪。这明显是一个坑,C++有这么坑吗?
    hhhhhh123
        30
    hhhhhh123  
       2022-08-30 17:18:02 +08:00
    我想起来了, 这不是你们说的 设计问题, 这特么是写法问题:
    如 def aaa(list =[] ) 多个使用的时候 地址同一个 所以一般参数不写空 数组 集合等。

    好吧也可以说是设计问题, 但是 注意规范就行。 这特么是我老旧以前的一个面试题。。呜呜呜
    ipwx
        31
    ipwx  
       2022-08-30 17:22:11 +08:00   ❤️ 1
    @sillydaddy ummm 本来觉得 C++ 也应该是类似 Python 的行为,没想到不是。

    https://ideone.com/iulg3B

    这还挺让我意外的。这么说来 C++ 的默认值是求值表达式,在调用时会展开。
    ipwx
        32
    ipwx  
       2022-08-30 17:23:25 +08:00
    @sillydaddy 嘛不管怎么说我从内心最基本的原则里面其实早就接受了 Python 的这个行为。如果不是今天看到这个帖子你的回复我估计从来都不会发现这件事。

    不过哪怕避开这种写法,C++ 写起来也应该很方便。。。至少我上面这个例子里面的用法肯定不是正常的 C++ 用法就对了。
    SmiteChow
        33
    SmiteChow  
       2022-08-30 17:23:54 +08:00
    算 bug ,因为人们已经用工具来避免这种情况发生了。
    ipwx
        34
    ipwx  
       2022-08-30 17:24:13 +08:00
    yysy 我觉得接受这种行为去写程序也没啥不好,写起来基本不会有啥不方便的。把对象构造从默认值上面移开其实是一种很好的习惯。
    Eureka0
        35
    Eureka0  
       2022-08-30 17:26:45 +08:00
    确实有点反直觉,但也不能说是 bug 了,别拿可变对象作为函数默认值就是了
    h404bi
        36
    h404bi  
       2022-08-30 17:34:50 +08:00
    习惯 JavaScript 类似默参写法再去写 Python 特别容易中招。只能说此梨非彼梨。
    ipwx
        37
    ipwx  
       2022-08-30 17:38:33 +08:00
    @h404bi 怎么说呢,其实我内心觉得任何语言在默认值里面构造堆对象都不是好的行为。

    @sillydaddy 我第一反应 C++ 程序员应该不会意外这种设计,主要是 C++ 有栈对象。如果是栈对象作为默认值,那么我理所当然地认为这里栈对象本身是默认值的一个拷贝。反而如果默认值是 new XXX(),我理所当然地觉得,那这种东西作为默认值,本来就不行,涉嫌内存泄露。

    简而言之就是我潜意识觉得符合 RAII 原则的程序就不会在默认值 new XXX()。毕竟连 std::shared_ptr<XXX>(new XXX) 都是错误用法,应该使用 std::make_shared<XXX>() 不是吗(笑
    ipwx
        38
    ipwx  
       2022-08-30 17:39:31 +08:00
    接上一条。总之 C++ 里面我第一反应就是写个重载,然后重载函数构造一个局部栈对象或者智能指针传给参数多的那一个,根本不会轮到 new XXX 这种默认值出场。
    tairan2006
        39
    tairan2006  
       2022-08-30 17:42:08 +08:00
    默认形参不能用集合,我记得刚学 Python 的时候就知道了……
    HashV2
        40
    HashV2  
       2022-08-30 17:43:27 +08:00
    把默认值赋值逻辑写倒方法里 不要写在参数上
    est
        41
    est  
       2022-08-30 17:48:44 +08:00
    @Aloento 怎么说呢,面对技术选型,比如用 java 的倒霉的多了去了。但是人数多,你不怎么看得出来,倒霉了只能算你自己蠢。。。。。
    gengchun
        43
    gengchun  
       2022-08-30 18:23:57 +08:00
    这个算是 101 的东西吧。这个是 mutable defaults 另一个是 closures late binding 。

    这两个有没有放在最前面介绍应该做为判断教材是否合格的主要指标。

    另外评价别人的代码,这也是很重要的参考,一旦出现,就要认真想一想了。
    x7DnVcd9bA706oWp
        44
    x7DnVcd9bA706oWp  
       2022-08-30 18:30:05 +08:00
    @kkhaike 是的,一般这么写
    class Foo:
    def __init__(self, s=None):
    if s is None:
    s = set()
    self.s = s
    DOLLOR
        45
    DOLLOR  
       2022-08-30 18:30:47 +08:00   ❤️ 2
    老实承认,就是设计失误。

    其他语言的默认参数的表达式在调用的时候才执行的,但 python 是在定义的时候执行的。前者更符合多数人的直觉。
    x7DnVcd9bA706oWp
        46
    x7DnVcd9bA706oWp  
       2022-08-30 18:31:17 +08:00
    @Aloento 菜还怪人家语言不行
    Vegetable
        47
    Vegetable  
       2022-08-30 18:35:47 +08:00
    @duange7X24 一般是 self.s = s or set()
    lucays
        48
    lucays  
       2022-08-30 18:48:13 +08:00
    目前应该所有 py 教学都会说避免这个,要说他是 feature 肯定不是,你根本不可能在任何场景用这个
    说是 bug 嘛,这个好像也不太要修复,硬要 2 选 1 那就是 bug

    还有一个就是,python 的 for 循环完了可以直接接 else 没有 if ,是 feature ,显然不是 bug ,但是我觉得,也应该删除这个 feature ,纯纯的屁用没有,如果真有人用了,还得反应一会才知道这段代码在干嘛,也很反直觉
    acmore
        49
    acmore  
       2022-08-30 18:50:14 +08:00
    是 Feature ,但它会导致一大堆 Bug
    所以是个坏 Feature
    Aloento
        50
    Aloento  
       2022-08-30 21:48:34 +08:00
    @est 中肯的,客观的


    @duange7X24 确实,我推荐您使用 Jvav
    westoy
        51
    westoy  
       2022-08-30 22:03:28 +08:00
    python 默认的 args 在__defaults__下面, 不可能让__defaults__的结果随缘吧?

    本质还是可变和不可变对象引用的问题啊,js 虽然能规避掉 (args=[])这种问题, 但是要是下意识写出 const func = (args=window.xxxx)=>{}不一样么
    jatsz
        52
    jatsz  
       2022-08-30 23:21:34 +08:00
    测试的阶段算 Bug ,这种 Public API 都 release 的就算 Feature 了,只能将错就错,外加补一下文档了。
    FreeEx
        53
    FreeEx  
       2022-08-31 00:49:42 +08:00 via iPhone
    学习了,以前好像写过这样的代码…
    dayeye2006199
        54
    dayeye2006199  
       2022-08-31 00:56:54 +08:00
    老 feature 了
    mikewang
        55
    mikewang  
       2022-08-31 02:25:29 +08:00 via iPhone   ❤️ 1
    @Vegetable Python 之父中国分父是吧
    js8510
        56
    js8510  
       2022-08-31 07:42:10 +08:00
    lanlanye
        57
    lanlanye  
    OP
       2022-08-31 09:24:04 +08:00 via iPhone
    @westoy 确实,其实发现这个现象后很快就反应过来原因了,但直觉上还是觉得参数列表里的东西应该是表达式而不是直接求好的值……
    lanlanye
        58
    lanlanye  
    OP
       2022-08-31 09:26:16 +08:00 via iPhone
    @Vegetable 老实说我 16 年入门时看过,当时好像还没有这段……不过我也记不太清了
    fbichijing
        59
    fbichijing  
       2022-08-31 10:26:07 +08:00
    很多 Python 书函数部分都会提到,**参数尽量不要用可变对象**。函数参数部分的可变对象在内存中使用了相同的地址,导致实例化后的操作会产生意料之外的行为。

    我觉得说是 bug 有点过,至少是可以理解的行为。只是在一开始不知道的时候容易被坑。

    ```python
    class Foo:
    def __init__(self, s=None):
    self.s = s if s else set()
    ```
    wtfedc
        60
    wtfedc  
       2022-08-31 10:28:06 +08:00
    @zone10 结构体默认可不是 nil
    janxin
        61
    janxin  
       2022-08-31 10:32:28 +08:00
    这是 Feature
    lolizeppelin
        62
    lolizeppelin  
       2022-08-31 13:38:06 +08:00
    python 其实挺 c 的...
    对象全是引用传递
    DonkeyBenjamin
        63
    DonkeyBenjamin  
       2022-08-31 15:07:33 +08:00
    这个不是 python 新手常见问题,python💩山里面成堆出现的吗?
    一般人都知道可以这么写

    def foo(l=None):
    l = l or []
    Namek77
        64
    Namek77  
       2022-08-31 16:32:38 +08:00
    我记得 Pyright 是可以检测出来的
    x7DnVcd9bA706oWp
        65
    x7DnVcd9bA706oWp  
       2022-08-31 17:11:32 +08:00
    @Aloento 都是坑而已,何必纠结坑的哪个更坑;我现在到 go 坑了
    rebeccaMyKid
        66
    rebeccaMyKid  
       2022-08-31 18:02:59 +08:00
    正确的:
    ```
    class Foo:
    def __init__(self, s: set[int] | None = None):
    self.s = s or set()


    if __name__ == "__main__":
    f1 = Foo(set())
    f2 = Foo()
    f1.s.add(1)
    f1.s.add(2)
    f1.s.add(3)
    print(f2.s)
    print(f1.s)

    ```
    apake
        67
    apake  
       2022-08-31 18:45:35 +08:00
    并不是 bug, 也不是 feature. 是一个编程要注意的事项. 默认参数不要使用可变类型.

    @lolizeppelin 并不是引用传递, 而是按值传递引用
    chrawsl
        68
    chrawsl  
       2022-09-01 09:40:19 +08:00
    编辑器会提示不建议这么写,这么写只会实例化一个 set 对象
    llsquaer
        69
    llsquaer  
       2022-09-01 19:22:36 +08:00
    又学到一个不被裁的方法了..哈哈
    Alias4ck
        70
    Alias4ck  
       2022-09-02 16:11:27 +08:00
    It's a bug,but also is a feature. It depends on how to use it.
    缓存数据的时候可以用这个特性 参考 https://foofish.net/python-tricks.html
    ```python

    def factorial(num, cache={}):
    if num == 0:
    return 1
    if num not in cache:
    print('xxx')
    cache[num] = factorial(num - 1) * num
    return cache[num]


    print(factorial(4))
    print("-------")
    print(factorial(4))
    ```
    JasonLaw
        71
    JasonLaw  
       2023-01-08 12:50:47 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4042 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 56ms · UTC 05:25 · PVG 13:25 · LAX 21:25 · JFK 00:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.