引入形容器

以stackoverflow上对于形容器(descriptor )的疑难开篇。

class Celsius:    def __get__(self, instance, owner):        return 5 * (instance.fahrenheit - 32) / 9    def __set__(self, instance, value):        instance.fahrenheit = 32 + 9 * value / 5class Temperature:    celsius = Celsius()    def __init__(self, initial_f):        self.fahrenheit = initial_ft = Temperature(212)print(t.celsius)  # 输入100.0t.celsius = 0print(t.fahrenheit)  # 输入32.0

以上代码实现了温度的摄氏温度和华氏温度之间的主动转换。其中Temperature类含有实例变量fahrenheit和类变量celsius,celsius由形容器Celsius进行代理。由这段代码引出的三点疑难:

  1. 疑难一:什么是形容器?
  2. 疑难二:__get__,__set__,__delete__三种办法的参数
  3. 疑难三:形容器有哪些利用场景
  4. 疑难四:property和形容器的区别是什么?

<!--more-->

疑难一:什么是形容器?

形容器是一个 实现了 __get____set____delete__中1个或多个办法的类对象。当一个类变量指向这样的一个装璜器的时候, 拜访这个类变量会调用__get__ 办法, 对这个类变量赋值会调用__set__ 办法,这品种变量就叫做形容器。

形容器 事实上是一种代理机制:当一个类变量被定义为形容器,对这个类变量的操作,将由此形容器来代理。

疑难二:形容器三种办法的参数

class descriptor:    def __get__(self, instance, owner):        print(instance)        print(owner)        return 'desc'    def __set__(self, instance, value):        print(instance)        print(value)    def __delete__(self, instance):        print(instance)class A:    a = descriptor()del A().a  # 输入<__main__.A object at 0x7f3fc867cbe0>A().a  # 返回desc,输入<__main__.A object at 0x7f3fc86741d0>,<class '__main__.A'>A.a  # 返回desc,输入None,<class '__main__.A'>A().a = 5  # 输入<__main__.A object at 0x7f3fc86744a8>,5A.a = 5  # 间接批改类A的类变量,也就是a不再由descriptor形容器进行代理。

由以上输入后果能够得出结论:

参数解释

  • __get__(self, instance, owner) instance 示意以后实例 owner 示意类自身, 应用类拜访的时候, instance为None
  • __set__(self, instance, value) instance 示意以后实例, value 右值, 只有实例才会调用 __set__
  • __delete__(self, instance) instance 示意以后实例

三种办法的实质

  • 拜访:instance.descriptor理论是调用了descriptor.__get__(self, instance, owner)办法,并且须要返回一个value
  • 赋值:instance.descriptor = value理论是调用了descriptor.__set__(self, instance, value)办法,返回值为None。
  • 删除:del instance.descriptor理论是调用了descriptor.__delete__(self, obj_instance)办法,返回值为None

疑难三:形容器有哪些利用场景

咱们想创立一种新模式的实例属性,除了批改、拜访之外还有一些额定的性能,例如 类型查看、数值校验等,就须要用到形容器 《Python Cookbook》

即形容器次要用来接管对实例变量的操作。

实现classmethod装璜器

from functools import partialfrom functools import wrapsclass Classmethod():    def __init__(self, fn):        self.fn = fn    def __get__(self, instance, owner):        return wraps(self.fn)(partial(self.fn, owner))

将办法fn的第一个参数固定成实例的类。可参考python官网文档的另一种写法:descriptor

class ClassMethod(object):    def __init__(self, fn):        self.fn = fn    def __get__(self, instance, owner=None):        if owner is None:            owner = type(obj)        def newfunc(*args):            return self.f(owner, *args)        return newfunc

实现staticmethod装璜器

class Staticmethod:    def __init__(self, fn):        self.fn = fn    def __get__(self, instance, cls):        return self.fn

实现property装璜器

class Property:    def __init__(self, fget, fset=None, fdel=None, doc=''):        self.fget = fget        self.fset = fset        self.fdel = fdel        self.doc = doc    def __get__(self, instance ,owner):        if instance is not None:            return self.fget(instance)        return self    def __set__(self, instance, value):        if not callable(self.fset):            raise AttibuteError('cannot set')        self.fset(instance, value)    def __delete__(self, instance):        if not callable(self.fdel):            raise AttributeError('cannot delete')        self.fdel(instance)    def setter(self, fset):        self.fset = fset        return self    def deleter(self, fdel):        self.fdel = fdel        return self

应用自定义的Property来形容farenheit和celsius类变量:

class Temperature:    def __init__(self, cTemp):        self.cTemp = cTemp  # 有一个实例变量cTemp:celsius temperature    def fget(self):        return self.celsius * 9 /5 +32    def fset(self, value):        self.celsius = (float(value) -32) * 5 /9    def fdel(self):        print('Farenhei cannot delete')    farenheit = Property(fget, fset, fdel, doc='Farenheit temperature')    def cget(self):        return self.cTemp    def cset(self, value):        self.cTemp = float(value)    def cdel(self):        print('Celsius cannot delete')    celsius = Property(cget, cset, cdel, doc='Celsius temperature')

应用后果:

t = Temperature(0)t.celsius  # 返回0.0del t.celsius  # 输入Celsius cannot deletet.celsius = 5t.farenheit  # 返回41.0t.farenheit = 212t.celsius  # 返回100.0del t.farenheit  # 输入Farenhei cannot delete

应用装璜器的形式来装璜Temperature的两个属性farenheit和celsius:

class Temperature:    def __init__(self, cTemp):        self.cTemp = cTemp    @Property  # celsius = Property(celsius)    def celsius(self):        return self.cTemp    @celsius.setter    def celsius(self, value):        self.cTemp = value    @celsius.deleter    def celsius(self):        print('Celsius cannot delete')    @Property  # farenheit = Property(farenheit)    def farenheit(self):        return self.celsius * 9 /5 +32    @farenheit.setter    def farenheit(self, value):        self.celsius = (float(value) -32) * 5 /9    @farenheit.deleter    def farenheit(self):        print('Farenheit cannot delete')

应用后果同间接用形容器形容类变量

实现属性的类型查看

首先实现一个类型查看的形容器Typed

class Typed:    def __init__(self, name, expected_type):        # 每个属性都有一个名称和对应的类型        self.name = name        self.expected_type = expected_type    def __get__(self, instance, cls):        if instance is None:            return self        return instance.__dict__[self.name]    def __set__(self, instance ,value):        if not isinstance(value, self.expected_type):            raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))        instance.__dict__[self.name] = value    def __delete__(self, instance):        del instance.__dict__[self.name]

而后实现一个Person类,Person类的属性name和age都由Typed来形容

class Person:    name = Typed('name', str)    age = Typed('age', int)    def __init__(self, name: str, age: int):        self.name = name        self.age = age

类型查看过程:

>>> Person.__dict__mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,              '__doc__': None,              '__init__': <function __main__.Person.__init__>,              '__module__': '__main__',              '__weakref__': <attribute '__weakref__' of 'Person' objects>,              'age': <__main__.Typed at 0x7fe2f440bd68>,              'name': <__main__.Typed at 0x7fe2f440bc88>})>>> p = Person('suncle', 18)>>> p.__dict__{'age': 18, 'name': 'suncle'}>>> p = Person(18, 'suncle')---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-88-ca4808b23f89> in <module>()----> 1 p = Person(18, 'suncle')<ipython-input-84-f876ec954895> in __init__(self, name, age)      4       5     def __init__(self, name: str, age: int):----> 6         self.name = name      7         self.age = age<ipython-input-83-ac59ba73c709> in __set__(self, instance, value)     11     def __set__(self, instance ,value):     12         if not isinstance(value, self.expected_type):---> 13             raise TypeError('Attribute {} expected {}'.format(self.name, self.expected_type))     14         instance.__dict__[self.name] = value     15 TypeError: Attribute name expected <class 'str'>

然而上述类型查看的办法存在一些问题,Person类可能有很多属性,那么每一个属性都须要应用Typed形容器形容一次。咱们能够写一个带参数的类装璜器来解决这个问题:

def typeassert(**kwargs):    def wrap(cls):        for name, expected_type in kwargs.items():            setattr(cls, name, Typed(name, expected_type))  # 经典写法        return cls    return wrap

而后应用typeassert类装璜器从新定义Person类:

@typeassert(name=str, age=int)class Person:    def __init__(self, name, age):        self.name = name        self.age = age

能够看到typeassert类装璜器的参数是传入的属性名称和类型的键值对。

如果咱们想让typeassert类装璜器主动的辨认类的初始化参数类型,并且减少相应的类变量的时候,咱们就能够借助inspect库和python的类型注解实现了:

import inspectdef typeassert(cls):    params = inspect.signature(cls).parameters    for name, param in params.items():        if param.annotation != inspect._empty:            setattr(cls, name, Typed(name, param.annotation))    return cls@typeassertclass Person:    def __init__(self, name: str, age: int):  # 没有类型注解的参数不会被托管        self.name = name        self.age = age

疑难四:property和形容器的区别

咱们能够利用Python的外部机制获取和设置属性值。总共有三种办法:

  1. Getters和Setter。咱们能够应用办法来封装每个实例变量,获取和设置该实例变量的值。为了确保实例变量不被内部拜访,能够把这些实例变量定义为公有的。所以,拜访对象的属性须要通过显式函数:anObject.setPrice(someValue); anObject.getValue()。
  2. property。咱们能够应用内置的property函数将getter,setter(和deleter)函数与属性名绑定。因而,对属性的援用看起来就像间接拜访那么简略,然而实质上是调用对象的相应函数。例如,anObject.price = someValue; anObject.value。
  3. 形容器。咱们能够将getter,setter(和deleter)函数绑定到一个独自的类中。而后,咱们将该类的对象调配给属性名称。这时候对每个属性的援用也像间接拜访一样,然而实质上是调用这个形容器对象相应的办法,例如,anObject.price = someValue; anObject.value。

Getter和Setter这种设计模式不够Pythonic,尽管在C++和JAVA中很常见,然而Python谋求的是简介,谋求的是可能间接拜访。


附1、data-descriptor and no-data descriptor

翻译为中文其实就是材料形容器和非材料形容器

  • data-descriptor:同时实现了__get____set__办法的形容器
  • no-data descriptor:只实现了__get__办法的形容器

两者的区别在于:

  • no-data descriptor的优先级低于instance.__dict__
class Int:    def __get__(self, instance, cls):        return 3class A:    val = Int()    def __init__(self):        self.__dict__['val'] = 5A().val  # 返回5
  • data descriptor的优先级高于instance.__dict__
class Int:    def __get__(self, instance, cls):        return 3    def __set__(self, instance, value):        passclass A:    val = Int()    def __init__(self):        self.__dict__['val'] = 5A().val  # 返回3

附2、形容器机制剖析材料:

  1. 官网文档-descriptor
  2. understanding-get-and-set-and-python-descriptors
  3. anyisalin - Python - 形容器
  4. Python形容器疏导(翻译)
  5. Properties and Descriptors

记得帮我点赞哦!

精心整顿了计算机各个方向的从入门、进阶、实战的视频课程和电子书,依照目录正当分类,总能找到你须要的学习材料,还在等什么?快去关注下载吧!!!

朝思暮想,必有回响,小伙伴们帮我点个赞吧,非常感谢。

我是职场亮哥,YY高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。

听我说,提高多,程序人生一把梭

如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。

职场亮哥文章列表:更多文章

自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!