环境 mac osx, python3.5
定义描述器(descriptor),总结协议,展示描述器的调用,研究一个自定义的描述器,以及内置的python描述器,包括:函数,属性(properties),静态方法(static methods),以及类方法。通过给出纯python等效的以及一个样本application.
学习有关描述器不仅仅提供通往一个更大的工具箱,它也创造了一个关于python怎么的工作的更深的理解,以及赞赏它的优雅设计。
一个descriptor是一个简单的方法管理访问属性。方式有三种:set, get, delete。
一般,一个descriptor是一个“绑定行为”的对象的属性,该属性可以被在描述器协议(descriptor protocol)中的方法重写。这些方法: __get__(), __set__(), __delete__(), 如果任何这些方法在一个对象中被定义,就说他是个描述器。
默认的访问这些属性的方法是get,set,或者delete这些属性从一个对象的字典中。例如:a.x有一个查找链,以a.__dict__[“x”]开始,然后 type(a).__dict__[“x”],然后继续以类似的通过type(a)基类的方式,除了元类(metaclasses),如果一个查找值是一个定义了一个描述器方法的对象,python将会重写默认的行为,并且调用描述器方法。这种情况的发生的优先链取决于哪一个描述器方法被定义了。
描述器是一个强大的,通用功能的协议。他们是属性(properties),方法,静态方法,类方法,以及super()等背后的工作机理。他们被用于整个python中,来完成新实的类。
假设通过一个python写的管理系统来运营一家书店,系统中包含一个类Book,采集作者,标题,书的价格。
class Book(object):
def __init__(self, author, title, price):
self.author = author
self.title = title
self.price = price
def __str__(self):
return "{0} - {1}".format(self.author, self.title)
从上面的定义来看,这个设计是没什么问题,但是可以发现,书的价格可以是任意值,包括负值,这与实际情况不合.作如下修改:
from weakref import WeakKeyDictionary
class Price(object):
def __init__(self):
self.default = 0
self.values = WeakKeyDictionary()
def __get__(self, instance):
return self.values.get(instance, self.default)
def __set__(self, instance, value):
if value < 0 or value > 100:
raise ValueError("Price must be between 0 and 100.")
self.values[instance] = value
def __delete__(self, instance):
del self.values[instance]
注:使用弱引用weakref,使得不在使用的对象能被垃圾回收。
修改Book 类:
class Book(object):
price = Price()
def __init__(self, author, title, price):
self.author = author
self.title = title
self.price = price
def __str__(self):
return "{0} - {1}".format(self.author, self.title)
b = Book("William Faulkner", "The Sound and the Fury", 12)
b.price
#12
b.price = -12
#Traceback (most recent call last):
# File "", line 1, in
# b.price = -12
# File "", line 9, in __set__
# raise ValueError("Price must be between 0 and 100.")
#ValueError: Price must be between 0 and 100.
b.price = 101
#Traceback (most recent call last):
# File "", line 1, in
# b.price = 101
# File "", line 9, in __set__
# raise ValueError("Price must be between 0 and 100.")
#ValueError: Price must be between 0 and 100.
当获取b.prce的值时,python识别出 price 是个描述器,并且调用Book.price.__get__。
当更改price的值时,如b.price=60,python再次识别price是个描述器,使用Book.price.__set__ 替代赋值。
当删除Book实例的price属性时,python自动调用Book.price.__delete__
以上的Price定义中使用了弱引用,如果不使用:
class Price(object):
def __init__(self):
self.__price = 0
def __get__(self, instance):
return self.__price
def __set__(self, instance, value):
if value < 0 or value > 100:
raise ValueError("Price must be between 0 and 100.")
self.__price = value
def __delete__(self, instance):
del self.__price
结果:
b1 = Book("William Faulkner", "The Sound and the Fury", 12)
b1.price
#12
b2 = Book("John Dos Passos", "Manhattan Transfer", 13)
b1.price
#13
按照定义中的第二部分说的。查找属性的顺序:
b.__dict__
#{'title': 'the sound and the fury', 'author': 'william'}
没有 price这一属性。
type(b).__dict__
#mappingproxy({'__init__': <function Book.__init__ at 0x106830840>, '__doc__': None, '__dict__': '__dict__' of 'Book' objects>, '__str__': <function Book.__str__ at 0x1068306a8>, '__module__': '__main__', '__weakref__': '__weakref__' of 'Book' objects>, 'price': <__main__.Price object at 0x1066fe048>})
#or
type(b).__dict__["price"]
#<__main__.Price object at 0x1066fe048>
可以知道,在给 b2 赋值price时,实际上修改的是 price 这个对象的值,同时,依据元类的原理(metaclass),Price 也是类,所以 b1 的属性price也跟着变化。
descr.__get__(self, obj, type=None) –> value
descr.__set__(self, obj, value) –> None
descr.__delete__(self, obj) –> None
如果一个对象定义类 __get__() 以及 __set__(),那么它被视为一个资料描述器(data descriprot)。仅仅定义__get__()被称为非资料描述器(non-data descriprot).
资料描述器和非资料描述器的区别在于:相对于实例的字典的优先级。如果实例字典中有与描述器同名的属性,如果描述器是资料描述器,优先使用资料描述器,如果是非资料描述器,优先使用字典中的属性。(实例 a 的方法和属性重名时,比如都叫 foo Python会在访问 a.foo 的时候优先访问实例字典中的属性,因为实例函数的实现是个非资料描述器)
class test:
def __init__(self):
self.method = 99
def method(self):
print("output...")
t = test()
t.method
#99
t.method()
#Traceback (most recent call last):
# File "", line 1, in
#TypeError: 'int' object is not callable
t.__dict__
#{'method': 99}
查字典的方式使人想到元类中使用type进行的动态定义类的最后一个参数的定义。
如果要定义一个只读的资料描述器,定义 __get__() 以及 __set__()并设置__set__()引起AttributeError的调用。
一个描述器可以直接使用方法的名字直接调用。d.get(obj)。
或者,通过访问属性的方式是更常见的自动调用描述器的方式.例如 obj.d 在 obj 的字典中查找 d。 如果定义了 __get__() ,然后 d.__get__(obj)会根据优先规则列表被调用。
对于对象:
运行机制在 object.getattribute() 里面,它将 b.x 转换成 type(b).__dict__[“x”].__get__(b, type(b)).工作的完成是通过优先级链,给予data descriptor 高于 实例变量的优先级,或者,实例变量高于 non-data descriptor 的优先级,并分配最低的优先级给 __getattr__()(如果存在)。整个过程的是现在Objects/object.c中的 PyObject_GenericGetAttr()。
对于类:
运行的机制在 type.getattribute(), 它将 B.x 转换成 B.__dict__[“x”].__get__(None, B), 在纯python中,看起来类似:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
重要的点:
1 descriptor被 __getattribute__() 调用。
2 重写 __getattribute__() 可以防止 自动 descriptor 调用。
3 object.__getattribute__() and type.__getattribute__() 使用不同于 __get__()的调用.
4 data descriptor 经常重写实例字典。
5 non-data descriptor 可能被实例字典重写。
被 super()返回的对象拥有自定义的 __getattribute__() 方法来调用descriptor。
调用super(B, obj).m()将会搜索 obj.__class__.__mro__ ,因为基类 A 立即跟随 B, 然后返回A.__dict__[“m”].__get__(obj, B)。 如果不是一个 descriptor, m 返回不变。 如果不在字典中, m 通过 object.__getattribute__() 继续搜索。
实现详情在Objects/typeobject.c 中的super_getattro()。
调用 property() 是 一种简洁的方式来构建能触发函数访问属性的 data descriptor。语法形势:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
fget, fset, fdel 对应get, set,delete 方法,doc 是一个文档字符串docstring
上面的关于书的类可以写为:
class Book(object):
def __init__(self, author, title, price):
self.author = author
self.title = title
self.price = price
self.default = 0
self.values = self.price
def __str__(self):
return "{0} - {1}".format(self.author, self.title)
def get_price(self):
return self.values
def set_price(self, value):
if value < 0 or value > 100:
raise ValueError("Price must be between 0 and 100.")
self.values= value
def delete_price(self, instance):
del self.values
price = property(get_price, set_price, delete_price, "price name")
b = Book("one", "book name", 24)
print(b.price)
b.price=50
print(b.price)
b.price=-50
#Traceback (most recent call last):
# File "descriptor1.py", line 31, in
# b.price=-50
# File "descriptor1.py", line 18, in set_price
# raise ValueError("Price must be between 0 and 100.")
#ValueError: Price must be between 0 and 100.
python的对象面向的特征是构建在基于环境函数之上。使用 non-data descriptors,这两者可以无缝合并。
类的字典存储方法为函数。在类的定义中, 方法是通过 def 或 lambda 实现,这也是常见的创建函数的工具。与常规的函数的唯一的差别是:第一个参数保留给对象实例。在python的传统中,实例的引用是通过self,或者this, 或者是其他的变量名。
为支持方法调用,在访问属性的过程中,函数包括 __get__() 来绑定方法。也就意味着,所有的函数 non-data descriptors, non-data descriptors返回绑定与非绑定方法取决于它们是被对象还是类调用。 在纯python中,工作原理类似:
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
class D(object):
def f(self, x);
return x
d = D()
D.__dict__["f"]
#D.f at 0x106830e18>
D.f
#D.f at 0x106830e18>
d.f
D.f of <__main__.D object at 0x1066f99e8>>
可以看出类对方法的调用都是未绑定的函数, 对象对方法的调用都是绑定的方法。
如果是未绑定非绑定方法的参数跟原始的函数是一样的,如果绑定了,第一个参数将代表类。
D.f(3)
#raceback (most recent call last):
# File "" , line 1, in
#TypeError: f() missing 1 required positional argument: 'x'
D.f(3,4)
# 4
d.f(3,4)
# Traceback (most recent call last):
# File "", line 1, in
# TypeError: f() takes 2 positional arguments but 3 were given
d.f(4)
# 4
non-data descriptors 提供了给变量在绑定函数成方法一般模式的简单的机制。
从前面可知,函数拥有 __get__(),因此它们会在被作为属性访问时被转化成方法。non-data descriptor 将 obj.f(*arg) 调用成 f(obj, *arg),调用 klass.f(*args)变成f(*args)
一下图表总结了绑以及最有用的两个变量。
Transformation | Called from an Object | Called from a Class |
---|---|---|
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
静态方法返回基本的没变化的函数。调用 c.f 或者 C.f 分别等价于直接查找 object.__getattribute__(c, “f”) 或者 object.__getattribute__(C, “f”)。结果, 函数变成等同访问,不论是来自对象还是类。
在静态方法中,好的候选方案是不引用 self 变量。
例如,一个统计包包含一个实验数据的容器类。这个类提供了一般的方法进行计算均值,中位值,以及其他基于数据的描述性统计。然而,可能存在一些有用的函数,它们是概念上相关,但不取决于数据。例如 erf(x)只是在统计工作中提出的方便的常规的方式,但不直接依赖于特定的数据集。它能被对象或者类调用。:s.erf(1.5)–>.9332, 或 Sample.erf(1.5) –>.9332。
由于静态方法返回基本的无变化的函数,样例调用是没什么变化的。
class E(object):
def f(x):
print(x)
f = staticmethod(f)
E.f(3) # E 是对象, 其类为 type, type(E), <class 'type'>
#3
E().f(3) #E() 为类,, <class '__main__.E'>
#3
使用non-data descriptor 协议,纯python的staticmethod()类似:
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
与静态方法不同,类方法在调用函数前提前设置类引用参数列表。这种格式对于对象或者类的调用是一样的。
class E(object):
def f(klass, x):
return klass.__name__, x
f = classmethod(f)
E.f(3)
#对象调用
#('E', 3)
#klass 指代对象本身
E().f(3)
#类调用
#('E', 3)
#klass 指代类本身
这种行为是很有用的,只要函数紧紧需要类引用,不关心任何基本的数据。classmethod的一个用处是创建可替换的类构造器。在python2.3中,classmethod dict.fromkeys()从一个包含键值keys列表list创建了一个新的字典.纯python等同于:
class Dict(object):
. . .
def fromkeys(klass, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c"
d = klass()
for key in iterable:
d[key] = value
return d
fromkeys = classmethod(fromkeys)
一个新的字典可以被创建为:
Dict.fromkeys('abracadabra')
#{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
使用non-data descriptor 协议,纯 python的classmethod()类似于:
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
注:现在定义 staticmethod 与 classmethod 是使用装饰器(decorator)的方法。
参考文章
Descriptor HowTo Guide
https://docs.python.org/3.5/howto/descriptor.html
Python Descriptors Made Simple
https://www.smallsurething.com/python-descriptors-made-simple
Python描述器引导(翻译)
http://pyzh.readthedocs.io/en/latest/Descriptor-HOW-TO-Guide.html