考虑以下代码:
class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar)
今天,我们将分析问题的答案:“编写foo.bar时会发生什么?”
您可能已经知道,大多数对象都有一个内部__dict__字典,其中包含它们的所有属性。 而且特别令人高兴的是,在Python中研究这些底层细节有多么容易:
>>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'}
让我们首先尝试阐述这个(不完整的)假设:
foo.bar等效于foo .__ dict __ ['bar'] 。
虽然听起来像是事实:
>>> foo = Foo() >>> foo.__dict__['bar'] 'hello!'
现在假设您已经知道可以在类中声明动态属性 :
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'}
嗯...好吧。 可以看出__getattr__可以模拟对“ fake”属性的访问,但是如果已经有一个声明的变量(例如foo.bar返回'hello!'而不是'goodbye!' ),则将无法正常工作。 一切似乎都比开始时要复杂一些。
确实如此:每当我们尝试获取属性时,都会调用一个魔术方法,但是,如上面的示例所示,这不是__getattr__ 。 调用的方法称为__getattribute__ ,我们将通过观察各种情况来尝试了解其确切的工作方式。
到目前为止,我们对假设进行了如下修改:
foo.bar等效于foo .__ getattribute __('bar') ,其大致工作原理如下:
def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item)
我们将通过实现此方法(使用其他名称)并直接调用它来对其进行测试:
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... def my_getattribute(self, item): ... if item in self.__dict__: ... return self.__dict__[item] ... return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!'
看起来不错吧?
好吧,剩下的就是验证是否支持变量分配,然后您可以回家...-
>>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call
地狱
my_getattribute返回一个对象。 如果它是可变的,我们可以对其进行更改,但是不能使用赋值运算符将其替换为另一个。 怎么办 毕竟,如果foo.baz等价于调用函数,那么原则上我们如何为属性分配新值?
当我们查看类似foo.bar = 1的表达式时,它不仅仅具有调用函数来获取foo.bar的值的功能 。 为属性分配值似乎与获取属性值根本不同。 事实是:我们可以实现__setattr__来看到以下内容:
>>> class Foo: ... def __init__(self): ... self.__dict__['my_dunder_dict'] = {} ... self.bar = 'hello!' ... ... def __setattr__(self, item, value): ... self.my_dunder_dict[item] = value ... ... def __getattr__(self, item): ... return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last): File "<pyshell#75>", line 1, in <module> foo.baz File "<pyshell#70>", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}}
关于此代码的几点注意事项:
- __setattr__与__getattribute__没有对应关系(即,魔术__setattribute__方法不存在)。
- 在__init__内部调用__setattr__ ,这就是为什么我们被迫做self .__ dict __ ['my_dunder_dict'] = {}而不是self.my_dunder_dict = {} 。 否则,我们将遇到无限递归。
但是我们也有财产 (和他的朋友们)。 装饰器,允许方法充当属性。
让我们尝试了解这是如何发生的。
>>> class Foo(object): ... def __getattribute__(self, item): ... print('__getattribute__ was called') ... return super().__getattribute__(item) ... ... def __getattr__(self, item): ... print('__getattr__ was called') ... return super().__getattr__(item) ... ... @property ... def bar(self): ... print('bar property was called') ... return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called
只是为了好玩, f .__ dict__有什么?
>>> f.__dict__ __getattribute__ was called {}
__dict__中没有条形码键,但是由于某种原因未调用__getattr__ 。 ?
bar是一种也将self作为参数的方法,只有此方法在类中,而不在类实例中。 这很容易看到:
>>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__doc__': None, '__getattr__': <function Foo.__getattr__ at 0x038308A0>, '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, 'bar': <property object at 0x0381EC30>})
条形码键确实位于类属性字典中。 要了解__getattribute__的工作原理 ,我们需要回答以下问题: 谁先调用了__getattribute__-类或实例?
>>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100
可以看出,验证的第一件事是__dict__类,即 它具有比实例更高的优先级。
等一下,我们什么时候调用bar方法? 我的意思是,我们用于__getattribute__的伪代码从不调用对象。 这是怎么回事?
符合描述符协议 :
descr .__得到__(自己,obj,类型= None)->值
descr .__ set __(self,obj,value)->无
descr .__删除__(self,obj)->无
整点都在这里。 实现这三种方法中的任何一种,以使对象成为描述符,并在将其视为属性时可以更改默认行为。
如果对象同时声明了__get __()和__set __() ,则它称为数据描述符。 仅实现__get __()的描述符称为非数据描述符。
两种类型的描述符在覆盖对象属性字典的元素方面有所不同。 如果字典包含与数据描述符同名的键,则数据描述符优先(即, 调用__set __() )。 如果字典包含与没有数据的描述符具有相同名称的键,则字典具有优先级(即字典元素被覆盖)。
要创建只读数据描述符,请声明__get __()和__set __() ,其中__set __()在调用时引发AttributeError 。 实现此__set __()足以创建数据描述符。
简而言之,如果您声明了__get__ , __set__或__delete__这些方法中的任何一个,则可以实现对描述符协议的支持。 这正是属性装饰器的作用:它声明了一个只读描述符,该描述符将在__getattribute__中调用。
我们实施中的最后更改:
foo.bar等效于foo .__ getattribute __('bar') ,其大致工作原理如下:
def __getattribute__(self, item): if item in self.__class__.__dict__: v = self.__class__.__dict__[item] elif item in self.__dict__: v = self.__dict__[item] else: v = self.__getattr__(item) if hasattr(v, '__get__'): v = v.__get__(self, type(self)) return v
让我们尝试在实践中进行演示:
class Foo: class_attr = "I'm a class attribute!" def __init__(self): self.dict_attr = "I'm in a dict!" @property def property_attr(self): return "I'm a read-only property!" def __getattr__(self, item): return "I'm dynamically returned!" def my_getattribute(self, item): if item in self.__class__.__dict__: print('Retrieving from self.__class__.__dict__') v = self.__class__.__dict__[item] elif item in self.__dict__: print('Retrieving from self.__dict__') v = self.__dict__[item] else: print('Retrieving from self.__getattr__') v = self.__getattr__(item) if hasattr(v, '__get__'): print("Invoking descriptor's __get__") v = v.__get__(self, type(self)) return v
>>> foo = Foo() ... ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ... ... print() ... ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned! Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned!
我们只是略微介绍了Python中属性实现的表面。 尽管我们最后一次模拟foo.bar的尝试通常是正确的,但请记住,始终会有一些小细节以不同的方式实现。
我希望除了了解属性如何工作外,我还设法传达了鼓励您尝试的语言之美。 今天还清一些知识债务 。