Python中的属性和处理协议

考虑以下代码:


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}} 

关于此代码的几点注意事项:


  1. __setattr____getattribute__没有对应关系(即,魔术__setattribute__方法存在)。
  2. __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的尝试通常是正确的,但请记住,始终会有一些小细节以不同的方式实现。


我希望除了了解属性如何工作外,我还设法传达了鼓励您尝试的语言之美。 今天还清一些知识债务

Source: https://habr.com/ru/post/zh-CN479824/


All Articles