分享

Python属性访问背后的魔法

 River_LaLaLa 2016-08-21

20160609  作者:SRJOGLEKAR246

 提到属性访问,大多数人仅仅知道一件事,即点运算符’.’(就像x.some_attribute)。简单来说,属性访问是从一个你已有的对象中获得另一个对象的方法。对于那些使用Python而不愿深入探索的人来说,上一句话可能非常简单。然而,这个看起来非常容易的东西其实深藏着许多秘密。

 让我们一个个地来看每个部分吧。

__dict__属性

 Python中的每个对象都有一个__dict__属性。这个字典,或者说类字典的(我将在后面解释)对象包含了这个对象本身的所有属性。它存储着这些属性的名称与值的键值对。

 这里是一个例子:


注意到’x’并不在c.__dict__中。原因很简单。y是定义为对象c的属性,而x则是类C的属性。因此,它实际上将会出现在C__dict__中。事实上,C__dict__也包括许多键(包括’__dict__’):


我们将在后面看到dictproxy的含义。

 对象的__dict__属性非常容易理解。它以Python字典的方式工作,事实上,它就是一个字典。


然而,类的__dict__属性就不是那么容易理解了。它实际上是类dictproxy的一个对象。dictproxy是一个特殊类,它的对象就像普通的字典,但是它们某些键却不太一样。


注意到你不可以直接在dictproxy中设置一个键(C.__dict__[‘x’] = 4不起作用)。然而你却可以使用C.x = 6完成同样的功能,因为这两者内部实现是不同的。同样需要注意的是,你也不可以为__dict__属性直接设置值(C.__dict__ = {}不起作用)。

有一个原因可以解释这一奇怪的实现。如果你不想深入细节,仅仅需要知道这样做是为了保证Python解释器能够正常工作,并且能够提升某些优化性能。如果想知道更详细的解释,请到StackOverflow中看一看 Scott H的回答。

描述符

描述符是一个在其属性中至少包含下列魔法方法其中之一的对象,即__get__, __set__或者__delete__(记住,Python中方法本质上也是对象)。注意,我们要讨论的是对象,而类可能实现了它们,也可能没有实现。

描述符可以帮助你在Python中定义一个对象的属性的行为。通过上面所提到的魔法方法,你可以分别实现获取、设置及删除对象中的属性(由描述符所描述的)。Python中存在两种描述符——数据描述符非数据描述符

非数据描述符仅仅定义了__get__属性。所有其他类型的都属于数据描述符。你自然会想到,为什么这两类描述符会这样命令。答案很直观。通常,一个对象的数据相关的属性我们才会趋向于设置或者删除它。其他属性,例如方法本身,我们不会这样做。所以这些描述符被称作非数据描述符。

如同Python中其他许多东西一样,这不是一个必须遵守的规则,而是一个惯例。你也可以用数据描述符来描述一个方法。但是这样的话,你的__get__应当返回一个函数。 

这里有一个例子。两个类分别定义了数据描述符对象和非数据描述符对象:


注意到__get__函数接收一个对象obj和它的类objclass作为参数。类似的,设置值需要obj对象和一些候选的值。删除仅仅需要对象obj。(通过初始化函数__init__)获得这些参数能够帮助你区分同一个描述符类的不同对象。注意,是对象才可能成为描述符。(另外,如果你不为一个描述符定义__get__方法,描述符对象本身将被返回)。

让我们在一些代码中使用这些类:


就是这样!你可以像平常在Python中一样接入这些描述的对象。


描述符在Python中用于许多属性和方法相关的功能中,包括静态方法,类方法和属性函数。使用描述符,你可以获得更好的对如何访问一个类或者它的对象的属性和方法的控制——包括定义一些幕后的功能,例如日志。

现在让我们看一下Python中管理属性访问的高层的规则

规则 

这里一字不差地引用了Shalabh Chatuvedi的书中的内容,流程如下:

1.    如果attrname是一个objectname的特殊(例如,Python提供的)属性,返回它;

2.    检查objectname.__class__.__dict__中的attrname。如果它存在并且是一个数据描述符,返回这个描述符本身。搜索所有objectname.__class__的基类并做相同的操作;

3.    检查objectname.__dict__中的attrname,如果找到即返回。如果objectname是一个类,同时搜索它的基类。如果它是类并且一个描述符存在其中或是基类中,返回这个描述符的结果;

4.    检查objectname.__class__.__dict__中的attrname。如果存在并且是一个非数据描述符,返回这个描述符的结果。如果存在并且不是一个描述符,直接返回。如果是存在并且是一个数据描述符,我们不应当来这里因为我们应当已经在第二步时返回了。搜索所有的objectname.__class__的基类并做同样的操作。

5.    抛出AttributeError异常。

为了更好的说明,这里有一些使用我们在描述符一节所写的代码(请再看一遍以便有个更清醒的认识)所做的测试:

data_attr_childsome_object类中的一个描述符。所以你可以重写它。同样,ChildClass(‘desc3’)中的version已经使用了,不是ParentClass中的那一个。


事实上,你在some_object的字典中插入一个合适的条目,它也不会起作用(参见规则1)。


另一方面,非数据描述符属性可以轻松地被重写。


然而,如果你到some_object的类中,在dictproxy里修改data_attr_child本身,你可以改变它的行为。


注意到当你使用一些非数据描述符(或者在本例中类似字符串的一些对象)替代类中的数据描述符时,我们最初在some_object__dict__中添加的条目开始起作用了。因此,some_object.data_attr_child返回’xyz’,而不是’abc’

data_attr_parent的属性行为和data_attr_child的类似。


注意到你不能在ChildClass中重写data_attr_parent。一旦你这么做了,我们将穿过规则1-2-3并在规则4停下,最终得到结果’xyz’

设置属性的规则

这里要比获得属性的规则简单许多。在此引用Shalabh的书:

  1.       检查objectname.__class__.__dict__中的attrname属性。如果存在并且是一个数据描述符,使用描述符来设置值。搜索所有objectname.__class__的基类并做相同的操作。

  2.       objectname.__dict__attrname键插入一些内容。

 就是这些!

 __slots__

简单得说,__slots__Python中阻止对象拥有它自己的__dict__属性的方式。这意味着,如果你在类里定义了__slots__,你就不可以给它的对象设置任意属性(除了在’slots’中定义的)。

这里有一个这样的类的例子:


现在看看它是如何起作用的:


你当然可以这样做:


但是这时,记住你是在SomeClass__dict__中定义的z,而不是在obj中。

正如Guido van Rossum他自己在他的博客中所提到的,Python__slots__实现是为了带来效率的提升,而不是严格限制属性设置。直观地说:假设你有一个类,你想要构造大规模的它的对象。你不希望对象本身拥有灵活的动态的属性,而是想要更高的效率。由于slots本质上去掉了每个对象的__dict__属性,这样你会节省大量的内存。

有趣的是,Pythonslots是由描述符实现的。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多