描述器(descriptor)

环境 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也跟着变化。

Descriptor Protocol

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()。

属性 Properties

调用 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.

函数与方法 Functions and Methods

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(34)
# 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

你可能感兴趣的:(python,python,descriptor,函数,methods)