为什么这段代码会打印一个随机选择的属性?

Why does this code print a randomly selected attribute?

今天在写一些特别糟糕的代码时,我偶然发现了这种神秘的行为。下面的 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。上面的程序并没有试图做任何有用的事情;它比原来的大大减少了。

不确定性来自 __dict__ return 中的随机性 vars(object)

打印有点可疑,因为您的 TypeUnion 没有 'x'

super_serious(TypeUnion()).x 

某些东西被 returned 的原因是因为你的 for 循环覆盖了 __getattribute__ 并因此劫持了点。添加此行将显示。

    if name == '__getattribute__':
        continue

一旦 get 被攻陷,set 也会死亡。可以这样想

setattr(t, name, lambda *x, **y: val)

在概念上与

相同
t.__dict__[name] = lambda *x, **y: val

但是 get 现在总是 return 相同的引用,无论 name 的值如何,然后被覆盖。因此,最终答案将是本次迭代中的最后一项,它是随机的,因为 for 循环经过初始 __dict__

的随机顺序

此外,请记住,如果您的目标是复制对象,那么 setattr 是错误的。调用 lambda 只会 return 原始函数,但不会调用原始函数,你需要一些类似于

的东西
setattr(t, name, lambda *x, **y: val(*x, **y)  # Which doesn't work

基本正确:

  1. 随机性来自Python 3.3 及更高版本默认随机哈希顺序(参见Why is dictionary ordering non-deterministic?)。

  2. 访问x调用已绑定到__getattribute__的lambda函数。

参见 Difference between __getattr__ vs __getattribute__ and 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 就是这样。注意在循环中,我们没有绑定/保存valcurrent值。1这意味着我们得到了last 值是 val 在函数中的值。使用原始代码,我们在创建对象 t 时完成所有这些工作,而不是稍后在 t.__getattribute__ 被调用时完成——但它仍然归结为:Of pairs in vars(object),找到最后一个满足我们条件的:这个值必须是可调用的,而这个值的类型不是它自己 type.

即使在 Python2 中,使用 class t(object) 使 t 成为新样式的 class 对象,因此此代码现在 "works" 在 [=85 中=]2 以及 Python3。当然,在 Py2k 中,字典排序不是随机的,所以每次我们得到的都是一样的东西:

$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>
$ python2 foo3.py
<slot wrapper '__init__' of 'object' objects>

对比:

$ 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。然后将 lambda 行替换为:

    ret = lambda stashed=i: stashed

又 运行 了。现在函数returns 0。这是因为我们在这里保存了icurrent值。

如果我们对示例程序做同样的事情,它将 return 第一个 val 符合条件,而不是 return 最后一个一个。

是的,torek 是正确的,因为您的代码没有绑定 val 的当前值,所以您得到分配给 val 的最后一个值。这是 "correctly" 将值与闭包绑定的版本:

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__被劫持了