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
结果会是什么呢?
1
wxf666 2022-08-30 15:47:03 +08:00
不是 {1, 2, 3} 么?
|
2
MoYi123 2022-08-30 15:47:18 +08:00
老问题了. {1,2,3}啊.
|
3
Jooooooooo 2022-08-30 15:48:42 +08:00
bug 是说不符合设计预期的行为. 这显然不可能是 bug. (虽然我不懂 python
|
4
lanlanye OP 我猜这种写法大概相当于
``` default = set([1]) class Foo: def __init__(self, s=default): self.s = s ``` |
5
lusi1990 2022-08-30 15:51:04 +08:00 via Android
这样用会引发内存泄露啊
|
6
lanlanye OP @Jooooooooo 应该算符合预期吧,不过感觉有点反直觉,可能我下意识地以为这样写相当于每次创建一个新的 set
|
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 ``` |
10
lanlanye OP @est 惊了居然还有这种事情……这个设计确实挺反直觉的,而且很难注意到,刚才发现 PyCharm 对这种情况有提示,Pyright 没有
|
11
Drahcir 2022-08-30 16:08:07 +08:00
开发了这么多年 Python ,还真没在意这个问题,学习了。
|
12
zone10 2022-08-30 16:22:31 +08:00 1
算是设计失误, 就跟 Null 的设计失误一样, 反正我用过 Rust 受不了 Go 作为一门比较新的语言居然不保证空类型安全, 特别是结构体默认为 nil 太 tm 容易出错了
|
13
mxT52CRuqR6o5 2022-08-30 16:24:14 +08:00
这个我知道,参数默认值在函数声明时就初始化了,像 js 的话参数默认值是在函数执行时初始化的
|
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] ``` |
15
20015jjw 2022-08-30 16:25:13 +08:00 via iPhone
正常 lint 都会提示
老问题了 |
17
kkhaike 2022-08-30 16:31:25 +08:00
我刚学 python 也踩过这个坑,默认值要用 None
|
18
liuran 2022-08-30 16:33:24 +08:00 via Android
不进行 deep copy 算是 python 的 feature 吧,之前自己刚开始用的时候就遇到了这个坑,然后认真学习了一下 shallow copy 和 deep copy
|
19
abersheeran 2022-08-30 16:41:59 +08:00
新手常犯的错误了……
|
20
lyang 2022-08-30 16:45:52 +08:00
这个例子不知道,但是类似的碰到过,可以理解
|
21
Symo 2022-08-30 16:46:59 +08:00
应该说是一个反直觉的设计缺陷.
|
22
rev1si0n 2022-08-30 16:53:00 +08:00
不是 Bug 也不是 Feature
|
23
littlewing 2022-08-30 16:53:35 +08:00
所以说 py 垃圾语言啊
|
24
yazinnnn 2022-08-30 17:04:44 +08:00
不会 python, 试了试可以这样写
class Foo: def __init__(self, createSet = lambda : set()): self.s = createSet() 有点反直觉, 但是想了想又算合理 |
25
Leviathann 2022-08-30 17:08:35 +08:00
彻底的设计缺陷
和 js 的 var 和 this 有得一比 甚至更恶劣 |
26
wenhaoy 2022-08-30 17:09:02 +08:00
为什么操作 f1 ,会改变 f2 的值?
|
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 破案了。 |
28
ipwx 2022-08-30 17:15:16 +08:00
不是 Bug 也不是 Feature 。我觉得有过 C++ 经验的程序员更容易接受这个设定。
|
29
sillydaddy 2022-08-30 17:17:15 +08:00 1
@ipwx
为啥 C++要躺枪。这明显是一个坑,C++有这么坑吗? |
30
hhhhhh123 2022-08-30 17:18:02 +08:00
我想起来了, 这不是你们说的 设计问题, 这特么是写法问题:
如 def aaa(list =[] ) 多个使用的时候 地址同一个 所以一般参数不写空 数组 集合等。 好吧也可以说是设计问题, 但是 注意规范就行。 这特么是我老旧以前的一个面试题。。呜呜呜 |
31
ipwx 2022-08-30 17:22:11 +08:00 1
@sillydaddy ummm 本来觉得 C++ 也应该是类似 Python 的行为,没想到不是。
https://ideone.com/iulg3B 这还挺让我意外的。这么说来 C++ 的默认值是求值表达式,在调用时会展开。 |
32
ipwx 2022-08-30 17:23:25 +08:00
@sillydaddy 嘛不管怎么说我从内心最基本的原则里面其实早就接受了 Python 的这个行为。如果不是今天看到这个帖子你的回复我估计从来都不会发现这件事。
不过哪怕避开这种写法,C++ 写起来也应该很方便。。。至少我上面这个例子里面的用法肯定不是正常的 C++ 用法就对了。 |
33
SmiteChow 2022-08-30 17:23:54 +08:00
算 bug ,因为人们已经用工具来避免这种情况发生了。
|
34
ipwx 2022-08-30 17:24:13 +08:00
yysy 我觉得接受这种行为去写程序也没啥不好,写起来基本不会有啥不方便的。把对象构造从默认值上面移开其实是一种很好的习惯。
|
35
Eureka0 2022-08-30 17:26:45 +08:00
确实有点反直觉,但也不能说是 bug 了,别拿可变对象作为函数默认值就是了
|
36
h404bi 2022-08-30 17:34:50 +08:00
习惯 JavaScript 类似默参写法再去写 Python 特别容易中招。只能说此梨非彼梨。
|
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>() 不是吗(笑 |
38
ipwx 2022-08-30 17:39:31 +08:00
接上一条。总之 C++ 里面我第一反应就是写个重载,然后重载函数构造一个局部栈对象或者智能指针传给参数多的那一个,根本不会轮到 new XXX 这种默认值出场。
|
39
tairan2006 2022-08-30 17:42:08 +08:00
默认形参不能用集合,我记得刚学 Python 的时候就知道了……
|
40
HashV2 2022-08-30 17:43:27 +08:00
把默认值赋值逻辑写倒方法里 不要写在参数上
|
41
est 2022-08-30 17:48:44 +08:00
@Aloento 怎么说呢,面对技术选型,比如用 java 的倒霉的多了去了。但是人数多,你不怎么看得出来,倒霉了只能算你自己蠢。。。。。
|
43
gengchun 2022-08-30 18:23:57 +08:00
这个算是 101 的东西吧。这个是 mutable defaults 另一个是 closures late binding 。
这两个有没有放在最前面介绍应该做为判断教材是否合格的主要指标。 另外评价别人的代码,这也是很重要的参考,一旦出现,就要认真想一想了。 |
44
x7DnVcd9bA706oWp 2022-08-30 18:30:05 +08:00
|
45
DOLLOR 2022-08-30 18:30:47 +08:00 2
老实承认,就是设计失误。
其他语言的默认参数的表达式在调用的时候才执行的,但 python 是在定义的时候执行的。前者更符合多数人的直觉。 |
46
x7DnVcd9bA706oWp 2022-08-30 18:31:17 +08:00
@Aloento 菜还怪人家语言不行
|
47
Vegetable 2022-08-30 18:35:47 +08:00
@duange7X24 一般是 self.s = s or set()
|
48
lucays 2022-08-30 18:48:13 +08:00
目前应该所有 py 教学都会说避免这个,要说他是 feature 肯定不是,你根本不可能在任何场景用这个
说是 bug 嘛,这个好像也不太要修复,硬要 2 选 1 那就是 bug 还有一个就是,python 的 for 循环完了可以直接接 else 没有 if ,是 feature ,显然不是 bug ,但是我觉得,也应该删除这个 feature ,纯纯的屁用没有,如果真有人用了,还得反应一会才知道这段代码在干嘛,也很反直觉 |
49
acmore 2022-08-30 18:50:14 +08:00
是 Feature ,但它会导致一大堆 Bug
所以是个坏 Feature |
50
Aloento 2022-08-30 21:48:34 +08:00
|
51
westoy 2022-08-30 22:03:28 +08:00
python 默认的 args 在__defaults__下面, 不可能让__defaults__的结果随缘吧?
本质还是可变和不可变对象引用的问题啊,js 虽然能规避掉 (args=[])这种问题, 但是要是下意识写出 const func = (args=window.xxxx)=>{}不一样么 |
52
jatsz 2022-08-30 23:21:34 +08:00
测试的阶段算 Bug ,这种 Public API 都 release 的就算 Feature 了,只能将错就错,外加补一下文档了。
|
53
FreeEx 2022-08-31 00:49:42 +08:00 via iPhone
学习了,以前好像写过这样的代码…
|
54
dayeye2006199 2022-08-31 00:56:54 +08:00
老 feature 了
|
56
js8510 2022-08-31 07:42:10 +08:00
https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
Best practice: all mutable default to be None |
57
lanlanye OP @westoy 确实,其实发现这个现象后很快就反应过来原因了,但直觉上还是觉得参数列表里的东西应该是表达式而不是直接求好的值……
|
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() ``` |
61
janxin 2022-08-31 10:32:28 +08:00
这是 Feature
|
62
lolizeppelin 2022-08-31 13:38:06 +08:00
python 其实挺 c 的...
对象全是引用传递 |
63
DonkeyBenjamin 2022-08-31 15:07:33 +08:00
这个不是 python 新手常见问题,python💩山里面成堆出现的吗?
一般人都知道可以这么写 def foo(l=None): l = l or [] |
64
Namek77 2022-08-31 16:32:38 +08:00
我记得 Pyright 是可以检测出来的
|
65
x7DnVcd9bA706oWp 2022-08-31 17:11:32 +08:00
@Aloento 都是坑而已,何必纠结坑的哪个更坑;我现在到 go 坑了
|
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) ``` |
67
apake 2022-08-31 18:45:35 +08:00
|
68
chrawsl 2022-09-01 09:40:19 +08:00
编辑器会提示不建议这么写,这么写只会实例化一个 set 对象
|
69
llsquaer 2022-09-01 19:22:36 +08:00
又学到一个不被裁的方法了..哈哈
|
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)) ``` |
71
JasonLaw 2023-01-08 12:50:47 +08:00
|