描述器

定义:

Python中如果一个类实现了 __get__、__set__、__delete__ 三个方法中的任意一个,那么这个类就是一个描述器。

如果仅实现了 __get__就是非数据描述器non data descriptor;

同时实现了 __get__、__set__ 就是数据描述器 data descriptor;

如果一个类的类属性设置为描述器,那么它被称为owner属主。method也是类的属性

 

__get__方法的触发时机:

如果一个类的类属性是一个实例,并且这个实例是一个描述器,当访问这个类属性时,就会触发描述器中的__get__方法。ps:通过实例属性访问,不会触发__get__方法

 

# 定义一个非数据描述器

class A:

    def __init__(self):

        print('A init')

    def __get__(self, instance, owner):

        print('__get__', self, instance, owner)

        return self

 

class B:

    # 类加载时就已经创建,

    x = A()  # 类属性是一个描述器

    def __init__(self):

        print('B init')

        self.x = A()

 

print(B.x)  # 访问类属性,触发__get__方法

print(B().x) # 访问实例属性,不会触发__get__方法,即使实例属性名和类属性相同,非数据描述器不会触发

 

输出:

A init

__get__ <__main__.A object at 0x0000018F48169C50> None <class '__main__.B'>

<__main__.A object at 0x0000018F48169C50>

 

B init

A init

<__main__.A object at 0x0000018F48379C50>

__get__方法参数说明:

         instance:属主类B的实例

         owner:属主类B

返回值:类属性x的值

 

__set__方法的触发时机:

         如何一个类的类属性是一个数据描述器(同时有__get__ 和 __set__ 方法),且这个类的实例属性名称和这个类属性具有相同的名字,当访问这个类和类属性同名的类实例时,实际访问的是这个类的类属性,会同时触发__get__ 和 __set__ 方法

# 定义一个数据描述器

class A:

    def __init__(self):

        print('A init')

    def __get__(self, instance, owner):

        print('__get__', self, instance, owner)

        return self

    def __set__(self, instance, value):

        print('__set__', self, instance, value)

 

 

class B:

    # 类加载时就已经创建,

    x = A()  # 类属性是一个数据描述器

 

    def __init__(self):

        print('B init')

        self.x = 100 # 实例名字和类属性名字相同

 

 

# 访问实例属性,实际访问的类属性,会先触发__set__方法在触发__get__方法

print(B().x)

输出:

A init

B init

__set__ <__main__.A object at 0x000001A66DD19C50> <__main__.B object at 0x000001A66DF29DD8> 100

__get__ <__main__.A object at 0x000001A66DD19C50> <__main__.B object at 0x000001A66DF29DD8> <class '__main__.B'>

<__main__.A object at 0x000001A66DD19C50>

 

B().x = 1 在外部操作时,同样会触发__set__方法

 

官方解释:

         当类属性是一个数据描述器,且实例属性和类属性名称相同,访问类实例属性时,属性查找顺序,类字典的优先级高于实例字典优先级,所以因先在类字典进行搜索

 

本质:

         通过打印 B.__dict__ 和 B().__dict__ 发现,B().__dict__中没有定义实例属性,所以在访问时发现实例字典中没有,会在类中进行搜索,最终访问的是类属性

print(B().__dict__)

print(B.__dict__)

{}

{'__weakref__': <attribute '__weakref__' of 'B' objects>, 'x': <__main__.A object at 0x000001D23EA29C88>, '__dict__': <attribute '__dict__' of 'B' objects>, '__module__': '__main__', '__doc__': None, '__init__': <function B.__init__ at 0x000001D23EA69E18>}

 

描述器在Python中应用非常广泛

Python中的方法(包括staticmethod 和 classmethod )都实现了非数据描述器,因此,实例可以重新定义和覆盖方法,这允许单个实例获取与同一个的其他实例不同的行为。

Property类实现了一个数据描述器,因此,实例不能覆盖属性的行为。

 

应用实例:

①   自定义类中的静态方法staticmethod

         分析过程:

1、  自定义一个类 StaticMethod;

2、  用于装饰另外一个A类中的foo方法,相当于foo = StaticMethod(foo);

3、  所以在StaticMethod的初始化方法中需要接受foo函数,__init__(self, fn);

4、  foo方法本身为A类中类属性,值为StaticMethod类的实例,如果StaticMethod为非数据描述时,访问foo时,会触发StaticMethod类中的__get__方法;

5、  __get__返回值为foo方法本身,这样在执行foo()时,才能指向foo本身

代码如下:

class StaticMthod:

    def __init__(self, fn):

        self._fn = fn

    def __get__(self, instance, owner):

        print('__get__: ', self, instance, owner)

        return self._fn # 返回fn本身

 

class A:

    @StaticMthod # 相当于 foo = StaticMthod(foo)

    def foo():

        print('static')

# foo是A的类属性,值为StaticMthod实例,且StaticMthod是一个描述器

# 访问A.foo时,会触发__get__方法,返回值为A.foo的值

a = A.foo # 此时就会触发__get__方法

a() # 执行

输出:

__get__:  <__main__.StaticMthod object at 0x000001FE6A189CC0> None <class '__main__.A'>

static

②   自定义类中的类方法classmethod

         分析过程和静态方法类似,classmethod执行是多一个cls参数,如果__get__ 返回值还是self._fn的话,就会报错,缺少一个参数,所以引入偏函数将cls进行固化,返回一个新的可调用对象

示例代码:

from functools import partial

class ClassMethod:

    def __init__(self, fn):

        self._fn = fn

 

    def __get__(self, instance, owner):

        print("__get__ : ", self, instance, owner)

        return partial(self._fn, owner)

 

 

class A:

    @StaticMthod # 相当于 foo = StaticMthod(foo)

    def foo():

        print('static')

 

    @ClassMethod # 相当于 bar = StaticMthod(bar)

    def bar(cls):

        print(cls.__name__)

 

# foo是A的类属性,ClassMethod,ClassMethod是一个描述器

# 访问A.bar时,会触发__get__方法,返回值为A.bar的值

a = A.bar # 此时就会触发__get__方法

a() # 执行

输出:

__get__ :  <__main__.ClassMethod object at 0x000001C7DE6C9DD8> None <class '__main__.A'>

A

 

③  对实例的数据进行检查

class Person:

    def __init__(self, name: str, age: int):

        self.name = name

        self.age = age

 

思路:

    写函数,在__init__中先检查,如果不合格,直接跑异常

    装饰器,使用inpect模块完成

    描述器

使用描述器实现:

    分析:实例数据进行检查,即需要触发__set__ 方法,所以将类实例属性的名称和类属性的名称定义为相同。

第一版代码如下:

class Typed:

    def __init__(self, name, _type):

        self.name = name

        self.type = _type

 

    def __get__(self, instance, owner):

        print('__get__:', self, instance, owner)

        if instance is not None:

            return instance.__dict__[self.name]

        return self

 

    def __set__(self, instance, value):

        print('__set__:', self, instance, value)

        if not isinstance(value, (self.type,)):

            raise ValueError(value)

        instance.__dict__[self.name] = value

 

 

class Person:

    # 类属性名称和类实例属性定义为一样

    name = Typed('name', str)

    age = Typed('age', int)

 

    def __init__(self, name: str, age: int):

        # 初始化实例属性时会触发__set__方法

        self.name = name

        self.age = age

 

p = Person('lisi',18)

# 访问实例属性时会触发__get__方法

print(p.name)

print(p.age)

 

上面代码在类中需要定义和实例属性相同的名称,显然不够灵活

改进如下:

    使用inspect模块动态获取初始化方法签名

    使用装饰器的方式动态给类添加类属性

 

# 定义描述器

class Typed:

    def __init__(self, name, _type):

        self.name = name

        self.type = _type

 

    def __get__(self, instance, owner):

        # print('__get__:', self, instance, owner)

        if instance is not None:

            # 访问属性时触发__get__,访问__set__访问的值

            return instance.__dict__[self.name]

        return self

 

    def __set__(self, instance, value):

        # print('__set__:', self, instance, value)

        if not isinstance(value, (self.type,)):

            raise ValueError(value)

        # 将value保存在__dict__中

        instance.__dict__[self.name] = value

 

# 装饰器类的装饰器

def typeassert(cls):

    import inspect

    params = inspect.signature(cls).parameters

    for name, param in params.items():

        # print(name, param.annotation)

        if param.annotation != param.empty(): # 只检查注解不为空的参数

            # 动态方式给类添加类属性,类属性值为描述器实例

            # 将装饰器和描述器联系起来

            setattr(cls, name, Typed(name, param.annotation))

    return cls

 

 

@typeassert  # Person = TypeAssert(Pseron), Person()

class Person:

    def __init__(self, name: str, age: int):

        self.name = name

        self.age = age

 

上面的装饰器还可以定义为类装饰器

class TypeAssert:

    def __init__(self, cls):

        self.cls = cls

 

    def __call__(self, *args, **kwargs):

        import inspect

        params = inspect.signature(self.cls).parameters

        for name, param in params.items():

            # print(name, param.annotation)

            if param.annotation != param.empty():

                setattr(self.cls, name, Typed(name, param.annotation))

        return self.cls(*args, **kwargs) # 返回类实例

 
posted @ 2019-11-17 22:39  ilovetesting  阅读(213)  评论(0编辑  收藏  举报