深入浅出 Python Descriptors / Properties

来源:互联网 发布:如何制作淘宝宝贝长图 编辑:程序博客网 时间:2024/06/03 17:02

原文

    • 补充一下基础知识dira 和 a__dict__
    • Descriptor Protocol
      • Descriptors as attributes 这种情况常称为 Property
      • Property
      • Property 举例
      • Descriptors as Functions and Methods

补充一下基础知识:dir(a)a.__dict__

任何 object 都可以使用 dir() 获取其 attributes 信息;
而并不是所有的 objects 都有 __dict__ 属性,例如 int 有 int.__dict__,而 int object 2 却没有,一般来说,class,function(包括class method) 都有 __dict__ 属性。

In Python, functions created using def and lambda have a __dict__ attribute so you can dynamically add attributes to them.[Stack Overflow]

在python中,如果一个 object 具有 __dict__,那么你可以动态的为该 object 添加属性,例如 a.xxx = 3 会自动为 a 添加一个新属性xxx;注意如果 type(a) 是类 C,那么上面的操作并不会为 C 添加新属性。但你可以用 C.xxx = 9 来为类 C 添加新属性。

注意
__dict__ 属性是read-only 的,尝试写会 raise AttributeError;
__ 开头和结尾并不 imply 该属性是read-only 的。
Class functions that begins with double underscore (__) are called special functions as they have special meaning. [2]
a.__dict__ 返回一个 dict 对象

如果对象 a 包含属性 attr,可以用 a.__getattribute__('attr') 来获取名字为 attr 属性的值。

如果想检查 a 是否包含属性 attr,可以用 built-in 函数 hasattr(a, 'attr') 来判断

Descriptor Protocol

在 python 中,只要定义了 __get__(), __set__(), __delete__() 这三个方法其中至少一个的 object,就称为一个 descriptor

在 python 中,descriptor 是很重要的基础,主要是为类服务的。

注意
一个 class/instance 默认并不是 descriptor;
如果一个 object 是 descriptor,它必定包含 __get__, __set__, __delete__ 中至少一个 attribute。
一个object 可以含有成员变量 x,则该 x 是一个 attribute,但并不一定是一个 descriptor。要想让它成为一个 descriptor(或称为 property),可以把 x 的类型定义为一个类,该类必须至少实现一个 descriptor method。而使用 @property 正是完成这个工作。见后面。

如果一个 descriptor 定义了 __get__()__set__(),则称为 data descriptor;如果只定义了 __get__(),则称为 non-data descriptor

定义 descriptor 的语法为:

descr.__get__(self, obj, type=None) --> valuedescr.__set__(self, obj, value) --> Nonedescr.__delete__(self, obj) --> None--> 表示 return

Descriptor Protocol 是 properties,bound/unbound methods, instance/class/static methods 的基础。注意他们都是类的成员。

1. Descriptors as attributes, 这种情况常称为 Property

对于一个 object 的 attributes,其默认的行为就是字面意义的 get/取用, set/设置, delete/删掉

而如果 该attribute所属的类 定义了 __get__(), __set__(), __delete__(),则在对之进行 get,set,delete 操作时,会覆盖其默认的行为。

下面是一个例子:

class RevealAccess(object):    def __init__(self, initval=None):        self.val = initval    def __get__(self, obj, objtype):        print 'Get method invokded'        return self.val    def __set__(self, obj, val):        print 'Set method invoked'        self.val = valclass MyClass(object):    x = RevealAccess(10)In [104]: m = MyClass()In [105]: m.xGet method invokdedOut[105]: 10In [106]: m.x = 20Set method invokedIn [107]: m.xGet method invokdedOut[107]: 20

Property

用上面的方法定义一个 descriptor 是不是太麻烦了?是的,python 提供了定义一个 descriptor 的简便方法:property

property 的语法是:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

那么上面的例子就可以写成:

class MyClass(object):    def getx(self): return self._x    def setx(self, value): self._x = value    def delx(self): del self._x    x = property(getx, setx, delx, "I'm the 'x' property.")

注意,上面的 MyClass 并没有给构造函数 __inti__(),因此在引用 m.x 之前,必须先用 m.x = 2 赋值一下才可以正常显示,否则提示 m 不含有属性 _x

来看一下 property 的实现原理(对于 CPython,实际上是用 C 代码写的,这里给出了 python 实现):

class Property(object):    "Emulate PyProperty_Type() in Objects/descrobject.c"    def __init__(self, fget=None, fset=None, fdel=None, doc=None):        self.fget = fget        self.fset = fset        self.fdel = fdel        if doc is None and fget is not None:            doc = fget.__doc__        self.__doc__ = doc    def __get__(self, obj, objtype=None):        if obj is None:            return self        if self.fget is None:            raise AttributeError("unreadable attribute")        return self.fget(obj)    def __set__(self, obj, value):        if self.fset is None:            raise AttributeError("can't set attribute")        self.fset(obj, value)    def __delete__(self, obj):        if self.fdel is None:            raise AttributeError("can't delete attribute")        self.fdel(obj)    def getter(self, fget):        return type(self)(fget, self.fset, self.fdel, self.__doc__)    def setter(self, fset):        return type(self)(self.fget, fset, self.fdel, self.__doc__)    def deleter(self, fdel):        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Property 举例

假设我们有一个记录温度的类 Celsius,定义如下:

class Celsius:    def __init__(self, temperature = 0):        self.temperature = temperature

用户在使用时会直接使用 c.temperature = 10 的形式来修改。

有一天我们发现这个温度范围有一个合理的区间,即必须大于 -275 摄氏度。

虽然说我们可以通过增加 Celsius.get_temp()Celsius.set_temp() 方法来修改,但也意味着用户所有的代码都需要改,这显然是不合理的。

这时候,@property 就帮了大忙。只需要添加如下的代码即可:

class Celsius:    def __init__(self, temperature = 0):        self.temperature = temperature    def get_temperature(self):        print("Getting value")        return self._temperature    def set_temperature(self, value):        if value < -273:            raise ValueError("Temperature below -273 is not possible")        print("Setting value")        self._temperature = value    temperature = property(get_temperature,set_temperature)

这样,任何获得 temperature 值的地方都会调用 get_temperature(),任何给 tempearture 赋值的地方都会调用 set_temperature()。是不是很 Cool?

注意
此时在 object 内部存在 temperature_temperature,前面的是一个 descriptor,是用户使用的接口,另一个是真正的存放数据的地方。
在第一次执行 c = Celsius() 时,setter 也会被调用一次,因为注意在 __init__() 中使用的是 temperature 而不是 _temperature

更进一步,property() 是一个 Python built-in function,其形式为:

property(fget=None, fset=None, fdel=None, doc=None)

你可以认为它作为一个类最后创建了一个 property object。

property object 都包含有三个方法:getter(), setter(), 和 delete(),用来分别指定其 fget, fset, fdel 对应的函数。

上面的例子中,有一行:

temperature = property(get_temperature,set_temperature)

可以分为三行写:

# make empty propertytemperature = property()# assign fgettemperature = temperature.getter(get_temperature)# assign fsettemperature = temperature.setter(set_temperature)

或者简化一点,分为两行写:

# make empty property & assign fgettemperature = property(get_temperature)# assign fsettemperature = temperature.setter(set_temperature)

因此,利用@符号可以把代码优化成:

class Celsius:    def __init__(self, temperature = 0):        self._temperature = temperature    @property    def temperature(self):        print("Getting value")        return self._temperature    @temperature.setter    def temperature(self, value):        if value < -273:            raise ValueError("Temperature below -273 is not possible")        print("Setting value")        self._temperature = value

这样就不需要引入临时的 get_temperature 等 names。

注意:第一个是@property,第二个是@temperature.setter,但两个 def 后面接的都是 temperature,注意参数不同,因为一个是默认的 fget,另外一个是 fset

2. Descriptors as Functions and Methods

python 的 class 的 __dict__ 中,将 methods 保存为 functions。并且一参必须是 object instance。一参作为 instance reference,习惯上我们称之为 self,但你也可以用 this 等其他名字,但效果都是一样的。

Class dictionaries store methods as functions. In a class definition, methods are written using def and lambda, the usual tools for creating functions. The only difference from regular functions is that the first argument is reserved for the object instance. By Python convention, the instance reference is called self but may be called this or any other variable name. —— [0]

为了支持 method 调用,functions 实现了 __get__() 方法。

functions have a __get__() method so that they can be converted to a method when accessed as attributes. —— [0]

这意味着所有的 functions 都是 non-data descriptors。当 functions 作为 methods 时,会根据调用者的不同(object/class)返回不同的方法(bound/unbound)。如下:

class Function(object):    . . .    def __get__(self, obj, objtype=None):        "Simulate func_descr_get() in Objects/funcobject.c"        return types.MethodType(self, obj, objtype)

来看看 function descriptor 的实际效果:

>>> class D(object):...     def f(self, x):...         return x...>>> d = D()>>> D.__dict__['f']  # Stored internally as a function<function f at 0x00C45070>>>> D.f              # Get from a class becomes an unbound method<unbound method D.f>>>> d.f              # Get from an instance becomes a bound method<bound method D.f of <__main__.D object at 0x00B18C90>>

输出结果显示,D.fa.f 是不同的。
对于 bound method,a.f 在被调用时,一参被设置为 instance 本身后传递给原函数 f;对于 unbound method,D.f 在被调用时,所有参数都被原封不动的传递给原函数 f
所以你可以这样写:

>>> D.f(D(), 2)2

虽然 D.f 不是 classmethod 或者 staticmethod,这种形式仍然是合法的。

This is because, whenever an object calls its method, the object itself is passed as the first argument. So, ob.func() translates into MyClass.func(ob). [2]

在下面的例子中,__init__() 方法也利用了这一特性:

class Triangle(Polygon):    def __init__(self):        Polygon.__init__(self,3)

而对于 classmethod,由于@classmethod 实际上是修改了 function 的 __get__ 函数,因此只能用 D.cf(2)的形式;对于@staticmethod,道理亦然。

来看一下 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

可以看到,@staticmethod 返回了一个 function,其 __get__ 覆盖了 function 默认的服务于类的 __get__,从而实现了 static method 的概念。

再看一下 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

可以看到,@classmethod 将传入的构造参数 f 存了起来,在被 get/取用 时返回了一个 newf,这个newf 是 f 的升级版,它把传入给它的参数 *args 最终转化成了 klass, *args 的形式,并传递给了原 f进行处理。

再次强调,@decorator 的形式只是一个 syntax sugar,它的本质仍然是一个返回函数的函数,例如

>>> class E(object):...     def f(klass, x):...          return klass.__name__, x...     f = classmethod(f)...>>> print E.f(3)('E', 3)>>> print E().f(3)('E', 3)

多说一个知识点:把 类的 method f 通过 __get__ 转化因为原函数 f 的这个过程,也叫做 binding

原创粉丝点击