为什么此代码会打印随机选择的属性?

时间:2017-05-21 01:27:16

标签: python python-3.x name-lookup

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

3 个答案:

答案 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基本上是正确的:

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

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

  3. 请参阅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。然后用:

    替换lambda行
        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__被劫持。