Python 描述符

学会描述符之后,不仅有更多的工具集可用,还会对 Python 的运作
方式有更深入的理解,并由衷赞叹 Python 设计的优雅。——Raymond Hettinger
Python 核心开发者和专家

什么是描述符

描述符 (Descriptor) 是 Python 中一个非常重要的特性,在实际应用中我们经常使用到它,但是也最容易被忽略,property、classmethod、staticmethod。那么究竟什么叫描述符呢?看一下官方的定义:

In general, a descriptor is an attribute value that has one of the methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an attribute, it is said to be a descriptor.

一般而言,描述器是一个包含了描述器协议中的方法的属性值。 这些方法有 __get__(), __set__() __delete__()。 如果为某个属性定义了这些方法中的任意一个,它就可以被称为 descriptor

如何使用描述符

除了上面提到的三个内置属性,其实在不少 Python 库中都有关于描述符的应用,比如各种 ORM。

ORM 示例

这里以官方的 ORM 示例做个简单的演示 – 通过描述符来实现简单的 object relational mapping 框架。
其核心思路是将数据存储在外部数据库中,Python 实例仅持有数据库表中对应的的键。描述器负责对值进行查找或更新:

1
2
3
4
5
6
7
8
9
10
11
12
class Field:

def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]

def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()

可以用 Field 类来定义描述了数据库中每张表的模式的 models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()

def __init__(self, key):
self.key = key

class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()

def __init__(self, key):
self.key = key

然后连接数据库验证一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'

>>> Song('Country Roads').artist
'John Denver'

>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams

自定义验证器

官方还提供了一个验证器的例子,同样值得一看。验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from abc import ABC, abstractmethod

class Validator(ABC):

def __set_name__(self, owner, name):
self.private_name = '_' + name

def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)

def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)

@abstractmethod
def validate(self, value):
pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

1
2
3
4
5
6
7
8
class OneOf(Validator):

def __init__(self, *options):
self.options = set(options)

def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

验证结果

1
2
3
4
5
6
class Component:
kind = OneOf('wood', 'metal', 'plastic')
#...
def __init__(self, name, kind, quantity):
self.kind = kind
#...

描述器会阻止无效实例的创建

1
2
3
4
>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

参考

描述器使用指南