zoukankan      html  css  js  c++  java
  • 描述器

    定义:

    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) # 返回类实例

     
  • 相关阅读:
    you-get 下载网络上的富媒体信息
    响应式布局
    网页头部的声明应该是用 lang="";
    htm、html、shtml网页区别
    请求头出现Provisional headers are shown
    配置nginx直接使用webpack生成的gz压缩文件,而不用nginx自己压缩
    babel-plugin-equire
    VUE中$refs的基本用法
    element-ui--按需引入
    vue ts ,vue使用typescript,三种组件传值方式
  • 原文地址:https://www.cnblogs.com/yaoqingzhuan/p/11878906.html
Copyright © 2011-2022 走看看