今天在编写一些特别糟糕的代码时,我偶然发现了这种神秘的行为。下面的Python 3程序打印随机选择的属性object
。这是怎么发生的?
对于非确定性的明显怀疑是vars(object)
字典的随机排序,但我看不出它是如何导致观察到的行为的。我得到的一个假设是,它是由被覆盖的__setattr__
的排序引起的,但是lambda总是只被调用一次(通过打印调试检查)这一事实证明了这一点。
class TypeUnion:
pass
class t:
pass
def super_serious(obj):
proxy = t()
for name, val in vars(object).items():
if not callable(val) or type(val) is type:
continue
try:
setattr(t, name, lambda _, *x, **y: val)
except AttributeError:
pass
return proxy
print(super_serious(TypeUnion()).x)
N.B。上述程序并没有尝试做任何有用的事情;它比原来大大减少了。
答案 0 :(得分:3)
非确定性来自__dict__
vars(object)
中的随机性
由于您的TypeUnion没有'x'
,因此打印有点可疑super_serious(TypeUnion()).x
返回某些内容的原因是因为你的for循环覆盖了__getattribute__
并因此劫持了这个点。添加此行将显示。
if name == '__getattribute__':
continue
get
遭到入侵后,set
也已失效。可以这样想想
setattr(t, name, lambda *x, **y: val)
在概念上与
相同t.__dict__[name] = lambda *x, **y: val
但是get
现在总是返回相同的引用,而不管name
的值是什么,然后被覆盖。因此,最终答案将是此迭代中的最后一项,这是随机的,因为for循环经历了初始__dict__
的随机顺序
另外,请记住,如果您的目标是制作对象的副本,那么setattr
是错误的。调用lambda将只返回原始函数,但是不会调用原始函数,你需要的东西是
setattr(t, name, lambda *x, **y: val(*x, **y) # Which doesn't work
答案 1 :(得分:3)
Andrei Cioara's answer基本上是正确的:
随机性来自Python 3.3及更高版本默认随机化哈希顺序(参见Why is dictionary ordering non-deterministic?)。
访问x
会调用已绑定到__getattribute__
的lambda函数。
请参阅Difference between __getattr__ vs __getattribute__和the Python3 datamodel reference notes for object.__getattribute__
。
我们可以通过以下方式使整个事情变得更加模糊:
class t(object):
def __getattribute__(self, name):
use = None
for val in vars(object).values():
if callable(val) and type(val) is not type:
use = val
return use
def super_serious(obj):
proxy = t()
return proxy
这是lambda发生的事情。请注意,在循环中,我们不绑定/保存val
的当前值。 1 这意味着我们得到 last val
在函数中的值。使用原始代码,我们在创建对象t
时执行所有这些工作,而不是在t.__getattribute__
被调用时执行 - 但它仍然归结为: Of< name,value&gt ; vars(object)中的对,找到符合我们条件的最后一个:值必须是可调用的,而值的类型本身不是type
。
使用class t(object)
使t
成为一种新式的类对象,即使在Python2中也是如此,因此这段代码现在可以在Python2和Python3中“起作用”。当然,在Py2k中,字典排序不是随机的,所以我们每次都会得到同样的东西:
$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>
$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>
VS
$ python3 foo3.py
<slot wrapper '__eq__' of 'object' objects>
$ python3 foo3.py
<slot wrapper '__lt__' of 'object' objects>
将环境变量PYTHONHASHSEED
设置为0
也使Python3中的顺序具有确定性:
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
$ PYTHONHASHSEED=0 python3 foo3.py
<method '__subclasshook__' of 'object' objects>
1 要了解这是什么,请尝试以下方法:
def f():
i = 0
ret = lambda: i
for i in range(3):
pass
return ret
func = f()
print('func() returns', func())
请注意,它显示的是func() returns 2
,而不是func() return 0
。然后用:
ret = lambda stashed=i: stashed
再次运行它。现在函数返回0.这是因为我们在这里保存了{em>当前的i
值。
如果我们对示例程序执行了同样的操作,它将返回符合条件的 first val
,而不是 last
答案 2 :(得分:3)
是的,torek是正确的,因为您的代码未绑定val
的当前值,因此您获得分配给val
的最后一个值。这是一个“正确”绑定值的版本:
class TypeUnion:
pass
class t:
pass
def super_serious(obj):
proxy = t()
for name, val in vars(object).items():
if not callable(val) or type(val) is type:
continue
try:
setattr(t, name, (lambda v: lambda _, *x, **y: v)(val))
except AttributeError:
pass
return proxy
print(super_serious(TypeUnion()).x)
这将始终输出<slot wrapper '__getattribute__' of 'object' objects>
,证明问题是__getattribute__
被劫持。