提高AttributeError具有@properties的意外副作用

时间:2018-10-10 02:35:40

标签: python

给出了人为的Python类:

class Item(object):
    def __getattr__(self, name):
        print("In __getattr__ looking for '{}'".format(name))
        if name.startswith("exists"):
            return "Here's a dummy string"
        message = "'{}' object has no attribute '{}'".format(type(self).__name__, name)
        raise AttributeError(message)

    def thing_a(self):
        return self.thing_b()

    def thing_b(self):
        return self.nonexistent_thing()

调用Item().thing_a()会产生预期的输出:

In __getattr__ looking for 'nonexistent_thing'
Traceback (most recent call last):
  File "debug.py", line 13, in <module>
    Item().thing_a()
  File "debug.py", line 8, in thing_a
    return self.thing_b()
  File "debug.py", line 11, in thing_b
    return self.nonexistent_thing()
  File "debug.py", line 5, in __getattr__
    raise AttributeError(message)
AttributeError: 'Item' object has no attribute 'nonexistent_thing'

但是,我确实希望thing_athing_bnonexistent_thing为属性。因此,如果我将代码更改为:

class Item(object):
    def __getattr__(self, name):
        print("In __getattr__ looking for '{}'".format(name))
        if name.startswith("exists"):
            return "Here's a dummy string"
        message = "'{}' object has no attribute '{}'".format(type(self).__name__, name)
        raise AttributeError(message)

    @property
    def thing_a(self):
        return self.thing_b

    @property
    def thing_b(self):
        return self.nonexistent_thing

并致电Item().thing_a,我得到了意外的结果:

In __getattr__ looking for 'nonexistent_thing'
In __getattr__ looking for 'thing_b'
In __getattr__ looking for 'thing_a'
Traceback (most recent call last):
  File "debug.py", line 15, in <module>
    Item().thing_a
  File "debug.py", line 5, in __getattr__
    raise AttributeError(message)
AttributeError: 'Item' object has no attribute 'thing_a'

嗯...... print()的输出似乎与执行顺序相反,并且AttributeError的消息是给thing_a的,确实存在。

在尝试找出答案的过程中,我发现将AttributeError更改为普通的Exception可以使一切正常:

In __getattr__ looking for 'nonexistent_thing'
Traceback (most recent call last):
  File "debug.py", line 15, in <module>
    Item().thing_a
  File "debug.py", line 9, in thing_a
    return self.thing_b
  File "debug.py", line 13, in thing_b
    return self.nonexistent_thing
  File "debug.py", line 5, in __getattr__
    raise Exception(message)
Exception: 'Item' object has no attribute 'nonexistent_thing'

因此,看来AttributeError本身在@property上的表现不佳,可能是因为它被property对象捕获了。但是AttributeError似乎是__getattr__筹集的最合适的例外,并且不带子集筹集Exception是不理想的。

有人能阐明这个问题,并且知道让@property__getattr__AttributeError和谐相处的方法吗?

我已经在Python 2.7和3.6中进行了尝试。

编辑:为__getattr__添加了(人为设计的)逻辑以响应name"exists"开头的情况。我确实希望它在nonexistent_thing时失败,但我希望它在正确的时间以正确的AttributeError升高name

1 个答案:

答案 0 :(得分:1)

这个难题的缺失之处在于@propertydescriptor(一个实现__get__ / __set__ / __del__来参与属性查找的对象)。

描述符的实现是通过__getattribute__魔术方法完成的。

the docs(重点是我的)中实现的重要部分:

  

无条件调用以实现类实例的属性访问。如果该类还定义了__getattr__(),则除非__getattribute__()显式调用或引发AttributeError ,否则不会调用后者。

为了进一步说明您的示例,

class Item(object):
    def __getattribute__(self, name):
        print(f'__getattribute__ {name}')
        try:
            return super().__getattribute__(name)
        except Exception as e:
            print(f'{e} during __getattribute__ {name}')
            raise

    def __getattr__(self, name):
        print("In __getattr__ looking for '{}'".format(name))
        # useful logic here which might return a value...
        message = "'{}' object has no attribute '{}'".format(type(self).__name__, name)
        raise AttributeError(message)

    @property
    def thing_a(self):
        return self.thing_b

    @property
    def thing_b(self):
        return self.nonexistent_thing


Item().thing_a

呼叫顺序如下:

- __getattribute__(thing_a)
  - property.__get__(...)
    - thing_a
      - __getattriute__(thing_b)
        - property.__get__(...)
          - thing_B
            - __getattribute__(nonexistent_thing)
            - (raised AE) -> __getattr__(nonexistent_thing)
      - (raised AE) -> __getattr_(thing_b)
- (raised AE) -> getattr(thing_a)

__getattr__中的描述符解析期间,对AttributeError的调用是对__getattribute__的响应。

这是上面的python代码段的输出:

__getattribute__ thing_a
__getattribute__ thing_b
__getattribute__ nonexistent_thing
'Item' object has no attribute 'nonexistent_thing' during __getattribute__ nonexistent_thing
In __getattr__ looking for 'nonexistent_thing'
'Item' object has no attribute 'nonexistent_thing' during __getattribute__ thing_b
In __getattr__ looking for 'thing_b'
'Item' object has no attribute 'thing_b' during __getattribute__ thing_a
In __getattr__ looking for 'thing_a'
Traceback (most recent call last):
  File "t.py", line 25, in <module>
    Item().thing_a
  File "t.py", line 14, in __getattr__
    raise AttributeError(message)
AttributeError: 'Item' object has no attribute 'thing_a'