【问题标题】:Why does this code print a randomly selected attribute?为什么此代码打印随机选择的属性?
【发布时间】:2017-05-21 01:27:16
【问题描述】:

今天在编写一些特别糟糕的代码时,我偶然发现了这种神秘的行为。下面的 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)

注意上面的程序并没有尝试做任何有用的事情;它比原来大大减少了。

【问题讨论】:

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


    【解决方案1】:

    Andrei Cioara's answer 基本正确:

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

    2. 访问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 所发生的事情。请注意,在循环中,我们不会绑定/保存 valcurrent 值。1 这意味着我们得到了 last val 在函数中的值。使用原始代码,我们在创建对象t 时完成所有这些工作,而不是稍后在调用t.__getattribute__ 时——但它仍然归结为: Of 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>
    

    对比:

    $ 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。这是因为我们在此处保存了 icurrent 值。

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

    【讨论】:

      【解决方案2】:

      非确定性来自vars(object)返回的__dict__中的随机性

      打印有点可疑,因为您的 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
      

      【讨论】:

      • 根据获取属性如何设置属性?我试过这段代码:class c: def __getattr__(self, a): print('getattr') def __getattribute__(self, a): print('getattribute') c().x = 9 并没有调用 getter。
      • 我可能错了,只是想给出一些提示。
      • 不是set,而是get被稍后调用,使用val中最后剩下的值。
      【解决方案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)
      

      这会一直输出&lt;slot wrapper '__getattribute__' of 'object' objects&gt;,证明问题是__getattribute__被劫持了。

      【讨论】:

      • 注意,顺便说一句,lambda _, *x, **y 有点傻:这只是一个函数,它有一个必需的参数和任意数量的可选附加位置和关键字参数。对于一个真正的通用函数,我们需要一个带有 或多个位置参数的函数,我们可以写成lambda *args, **kwargs: ...。当然,整件事很愚蠢,意味着混淆,所以我想_, *x, **y 只是让它变得更是如此。 :-)
      猜你喜欢
      • 1970-01-01
      • 2021-12-15
      • 2014-11-23
      • 2014-05-06
      • 2017-02-08
      • 2017-03-02
      • 1970-01-01
      • 1970-01-01
      • 2020-01-16
      相关资源
      最近更新 更多