转自:https://blog.tonyseek.com/post/notes-about-python-descriptor/

Python 引入的“描述符”(descriptor)语法特性真的很黄很暴力,我觉得这算是 Python 对象模型的核心成员之一。Python 语言设计的紧凑很大程度上得益于它。所以写一篇笔记文记录关于描述符我知道的一切。

低层 - 纯纯的描述符

纯纯的描述符很纯,基于类中定义的 __get__ 、 __set__ 、 __delete__ 三个特殊的方法。实现了这三个中方法的任意一个,这个类的实例就拥有了一些特殊特性。

假设现在有这么一个类 MyDescriptor,它拥有描述符的实现。把 MyDescriptor 实例化(my_descriptor),然后将实例作为另一个类 Spam 的类属性。

class MyDescriptor(object):
def __get__(self, subject_instance, subject_class):
return (self, subject_instance, subject_class) def __set__(self, subject_instance, value):
print "%r %r %r" (self, subject_instance, value) my_descriptor = MyDescriptor() class Spam(object):
my = my_descriptor spam = Spam()

吧 MyDescriptor 的实例作为 Spam 一个名为 my 的类属性之后,Spam 类中属性 my 的访问就被重载了。 Spam.my , spam.my 和 spam.my = 123 都不再直接在 spam.__dict__ 上获取和添加值,而是调用 my_descriptor.__get__(None, Spam) , my_descriptor.__get__(spam, Spam) 和 my_descriptor.__set__(spam, 123) 。如果 MyDescriptor 实现了 __delete__ ,那么 del spam.my 也会被重载为 my_descriptor.__delete__(spam) 。

所以我理解为对应“元类”(meta-class),描述符事实上是实现了“元属性”(meta-attribute)。通过这种重载方式,可以定义许多具有特殊行为的对象属性规格,而 Python 许多内置的对象模型成员也是通过这种方式实现的。

高层#0 - 从“函数”到“方法”

描述符在 Python 对象模型中的一个重要作用是实现“对象方法”(method)。我们都知道 Python 中方法有这些特性:

  • 方法本身是一个至少要有一个参数的普通函数,第 0 个参数位 self 指向实例,类似于 C++ 的 const T* this 指针。
  • 作为方法的函数在类的命名空间还是以普通函数的形式存在的,比如 Spam 类中的 egg 方法的原始形态是 Spam.__dict__['egg'] 这个函数。
  • 通过实例访问这个函数时,也就是类似 Spam().egg 的方式,得到的不再是 Spam.__dict__['egg'] 中保存的那个原始函数,而是一个包装过的 "bound method"。这个对象仍然有 __call__ 可以调用,但相比它所包装的原函数,这个对象接受调用时参数列表已经少了一个参数。这是一种类似 functools.partial(raw_function, self) 的效果,原函数已经被做了偏函数化处理,默认绑定了实例对象到第 0 个参数位(通常 self 所在的位置);
  • 奇怪的是,如果自己定义一个实现了 __call__ 的函数对象,把它放到类属性中,它并不会表现出“绑定 self”的特性。

我此前一直对此很疑惑,以为这是一个语法级别的行为。直到看了 Flask、Jinja 2 的作者 Armin Ronacher 的 一个 Presentation 我才知道,原来 Method 根本不是语法级别特性,而是通过描述符来实现的。

我此前一直忽略了的是:一个 def 定义的 Python 函数或一个 lambda 表达式,除了拥有 __call__ 实现外,还拥有 __get__ 实现。也就是说,所有 Python 函数都默认是一个描述符。这个描述符的特性在“类”这一对象上下文之外没有任何意义,但只要到了类中,就会和其他描述符所表现的一样,将 my_instance.my_method 重载为 MyClass.__dict__['my_method'].__get__(my_instance, MyClass) 。“绑定 self 参数” 这个过程正是 __get__ 的行为,导致了 spam.egg(1, 2, 3) 和 Spam.egg(spam, 1, 2, 3) 等价。

明白了这一点,我的思路就清晰多了。“方法”不就是一种特殊的类属性吗,而定义特殊类属性的行为在 Python 对象模型中的实现正是描述符。而对这点的理解也非常有用,Armin Ronacher 的演示稿中提到了可以利用这一点实现只对对象方法有效的装饰器。

高层#1 - Property 属性

不明白为啥属性这个词有 Property 和 Attribute 两种翻译,但是在 Python 中二者是有区别的。后者指的是真正的对象属性,就是保存在 obj.__dict__ 中的成员(我一般更喜欢用 vars(obj) 来取这个字典);而前者指的是类似 C# 中“方法属性”或 Java 中 Getter、Setter 方法的重载属性。

class Spam(object):

    @property
def egg(self):
return "some-value" @egg.setter
def egg(self, value):
self._egg = "value = %s" % value spam = Spam()

说起来也很清晰,就是 spam.egg 返回的是第一个 egg 方法调用的返回值, spam.egg = "abc" 调用的是对于第二个 egg 方法的 egg(self=spam, value="abc") 。此外还可以继续定义删除属性操作的重载。这个比对象方法要容易看出来得多, property 正是一个描述符。

高层#n - 不受限制的用户自定义描述符

除去语言内置的这些描述符用法,还有许多用户自定义的描述符用法。这些用法让开发者可以更加灵活地扩展对象行为,是非常有用的元编程工具。相比起元类对整个类行为的重载,描述符的重载粒度非常细,它只关注一个属性。所以使用上我们也可以比使用元类减少更多的顾忌,因为我们能非常容易看到它能量释放的边界。

用户定义描述符的例子,我觉得最经典的要属 SQLAlchemy (当然,Django Model 定义也是同理)。SQLAlchemy 的 Column 类就是描述符的实现者。定义一个模型对象的时候,以声明风格写出这个模型对应的数据表所拥有的属性:

class User(Jsonizable, LowerIdMixin, db.Model):
"""The account of the user.""" email = db.Column(db.String(64), unique=True, nullable=False)
nickname = db.Column(db.Unicode(20))

而 email 、 nickname 描述符将托管今后所有 User 的实例中,对于这两个同名属性的访问。SQLAlchemy 在映射对象关系的时候,可以用这个特性实现许多特性,比如延迟加载——直到访问user.roles 的时候才执行 SQL 语句查询 role 表并构造 Role 对象集返回。

Python 描述符(descriptor) 杂记的更多相关文章

  1. python描述符descriptor(一)

    Python 描述符是一种创建托管属性的方法.每当一个属性被查询时,一个动作就会发生.这个动作默认是get,set或者delete.不过,有时候某个应用可能会有 更多的需求,需要你设计一些更复杂的动作 ...

  2. python描述符 descriptor

    descriptor 在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor.descriptor通常用来改 ...

  3. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

     1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题 ...

  4. Python描述符 (descriptor) 详解

    1.什么是描述符? python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 __get__(), __set__(), 和__delete__().如 ...

  5. Python 描述符(Descriptor) 附实例

    在 Python 众多原生特性中,描述符可能是最少被自定义的特性之一,但它在底层实现的方法和属性却无时不刻被使用着,它优雅的实现方式体现出 Python 简洁之美. 定义 一个描述符是一个有" ...

  6. Python 描述符 (descriptor)

    1.什么是描述符? 描述符是Python新式类的关键点之一,它为对象属性提供强大的API,你可以认为描述符是表示对象属性的一个代理.当需要属性时,可根据你遇到的情况,通过描述符进行访问他(摘自Pyth ...

  7. python描述符descriptor(二)

    python内置的描述符 python有些内置的描述符对象,property.staticmethod.classmethod,python实现如下: class Property(object): ...

  8. 【python】描述符descriptor

    开始看官方文档,各种看不懂,只看到一句Properties, bound and unbound methods, static methods, and class methods are all ...

  9. 杂项之python描述符协议

    杂项之python描述符协议 本节内容 由来 描述符协议概念 类的静态方法及类方法实现原理 类作为装饰器使用 1. 由来 闲来无事去看了看django中的内置分页方法,发现里面用到了类作为装饰器来使用 ...

随机推荐

  1. java从零到变身爬虫大神(一)

    学习java3天有余,知道一些基本语法后 学习java爬虫,1天后开始出现明显效果 刚开始先从最简单的爬虫逻辑入手 爬虫最简单的解析面真的是这样 import org.jsoup.Jsoup; imp ...

  2. MySQLdb模块的安装

    在WINDOWS环境连接利用python连接MYSQL数据库的时候,需要安装python工具包MySQL-python,但是直接用pip install安装会出错,错误信息如下: _mysql.c(4 ...

  3. MySQL分库分表环境下全局ID生成方案 转

    在大型互联网应用中,随着用户数的增加,为了提高应用的性能,我们经常需要对数据库进行分库分表操作.在单表时代,我们可以完全依赖于数据库的自增ID来唯一标识一个用户或数据对象.但是当我们对数据库进行了分库 ...

  4. DebugDiag收集Dump的使用说明

    DebugDiag简介 Debug Diagnostic Tool (DebugDiag)是微软提供的工具,可以用来追踪windows平台下的程序崩溃,卡死,内存泄漏等一些疑难问题的原因,按照问题类别 ...

  5. 【性能诊断】十一、性能问题综合分析(案例2,windbg、wireshark)

    [问题描述]:       前段时间有一项目反馈,常用的审批功能有时的响应较慢,多个管理员功能不定期的出现客户端无响应的状况,并且管理员功能一旦出现卡死,也会影响到普通的业务用户致使很多用户无法操作. ...

  6. Jfinal连接自助数据库的数据源

    # 开发数据库地址 #jdbcUrl = jdbc:oracle:thin:@localhost:1521:testsa jdbcUrl = jdbc:srdbsql://127.0.0.1:1975 ...

  7. C#Winform中treeView控件使用总结

    1.如何展开结点时改变图标(注意:不是选中时) 要在目录中使用图标首先要加入一个控件ImageList(命名为imageList1),然后可以按图片的index或名称引用图片. 然后需要在TreeVi ...

  8. 删除Android自带软件方法及adb remount 失败解决方案

    删除Android自带软件方法 1.在电脑上打开cmd,然后输入命令 adb remount adb shell su 2.接着就是Linux命令行模式了,输入 cd system/app 3然后输入 ...

  9. php日期时间函数

    1,年-月-日echo date('Y-m-j');2007-02-6echo date('y-n-j');07-2-6大写Y表示年四位数字,而小写y表示年的两位数字:小写m表示月份的数字(带前导), ...

  10. .NET去掉HTML标记

    using System.Text.RegularExpressions; /// <summary> /// 去除HTML标记 /// </summary> /// < ...