Python学习:描述符

时间:2022-03-30 14:53:26

一、描述符是什么

  描述符:是一个类,只要内部定义了方法__get__, __set__, __delete__中的一个或者多个。描述符,属性,方法绑定等内部机制都是描述符在起作用。描述符以单个属性出现,并针对该属性的不同访问行为做出响应。最重要的是,描述符能“感知”通过什么引用该属性,从而和目标建立绑定关联。

二、描述符的实现

  

class Descriptor:
    """
    描述符
    """

    def __set_name__(self, owner, name):
        """
        描述符属性必须定义为类型成员,所以其自身不适合存储实例相关的状态,在创建属性时,__set_name__方法被调用,并可以通过参数获知目标类型(owner),以及属性名称
        :param owner:
        :param name:
        :return:
        """
        print(self, owner, name)
        self.name = f"__{name}__"

    def __get__(self, instance, owner):
        """
        以类型或实例访问描述符属性时,__get__被自动调用,且会接收到类型和实例引用
        :param instance:
        :param owner:
        :return:
        """
        print(self,instance,owner)
        return getattr(instance,self.name, None)

    def __set__(self, instance, value):
        """
        仅在实例引用时被调用。以类型引用进行赋值,会导致描述符属性被替换
        :param instance:
        :param value:
        :return:
        """
        print(self, instance, value)
        setattr(instance, self.name, value)

    def __delete__(self, instance):
        """
        仅在实例被引用时调用。以类型引用进行删除操作,会导致描述符属性被删除
        :param instance:
        :return:
        """
        print(self, instance)
        raise AttributeError("delete is disabled")


class X:
    data = Descriptor()


x = X()    # 执行__set_name__  <__main__.Descriptor object at 0x0000026DEB4E8438> <class '__main__.X'> data
x.data = 100    # 执行__set__  <__main__.Descriptor object at 0x0000018A54408470> <__main__.X object at 0x0000018A54408908> 100
print(x.data)    # 执行__get__ <__main__.Descriptor object at 0x0000020685688438> <__main__.X object at 0x00000206856888D0> <class '__main__.X'>
print(x.__dict__)    # {'__data__': 100}
print(X.__dict__)    #  {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x000001E841598438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}
X.data = 2    # 以类型引用进行赋值,会导致描述符属性被替换
print(x.data)    # 2
print(X.data)    # 2
print(x.__dict__)    # {'__data__': 100}
print(X.__dict__)    # {'__module__': '__main__', 'data': 2, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

三、数据描述符

  如果定义了__set__或__delete__方法,那么我们便称其为数据描述符(data descriptor),而仅有__get__的则是非数据描述符(non-data descriptor)。这两者的区别在于,数据描述符属性的优先级高于实例名字空间中的同名成员。

class Descriptors:
    """
    数据描述符
    """
    def __set_name__(self, owner, name):
        self.name = name     # 获取Descriptors 实例对象名字

    def __get__(self, instance, owner):
        print("执行Descriptors的get")
        return self.name

    def __set__(self, instance, value):

        self.name = value
        print("执行Descriptors的set")

    def __delete__(self, instance):
        print("执行Descriptors的delete")


class Light:
    # 使用描述符
    name = Descriptors()

    def __init__(self, name, price):
        self.name = name
        self.price = price


# 使用类的实例对象来测试
light = Light("电灯泡", 60)  # 执行描述符的set内置属性
light.name  # 执行描述符的get内置属性
print(light.__dict__)  # 查看实例的字典,不存在name  {'price': 60}
print(Light.__dict__)  # 查看类的字典,存在name(为描述符的对象)
# {'__module__': '__main__', 'name': <__main__.Descriptors object at 0x000002261E7D8438>,
# '__init__': <function Light.__init__ at 0x00000226257FED90>, '__dict__': <attribute '__dict__' of 'Light' objects>,
# '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None}
del light.name  # 执行描述符的delete内置属性
del Light.name    # 以类型引用进行删除操作,会导致描述符属性被删除
print(Light.__dict__)    # {'__module__': '__main__', '__init__': <function Light.__init__ at 0x000001CBC197EEA0>, '__dict__': <attribute '__dict__' of 'Light' objects>, '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None}
print(light.name)    # 报错,描述符属性被删除

  如果注释掉__set__,就成为了非数据描述符。

  描述符的优先级问题:类属性>数据描述符>实例属性>非数据描述符>找不到属性触发__getattr__()

    说明问题一:类属性>数据描述符

class  Descriptor:

    def __get__(self, instance, owner):
        print("__get__")
        return self.name

    def __set__(self, instance, value):
        print("开始赋值:", value)
        self.name = value
        print("__set__")


class X:
    data = Descriptor()


x = X()
x.data = 100     # 调用__set__赋值
print(x.__dict__)    # {}
x.data = 3
print(x.data)    # 3
print(x.__dict__)    # {}
print(X.data)    # 调用__get__
print(X.__dict__)    #  {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x0000015EE4A38438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

X.data = 44444    # 语句没有触发set的执行,说明类属性的优先级大于数据描述符的优先,此时相当于类属性覆盖了数据描述符,从而说明对类属性的一切操作都与描述符无关
print(x.data)    # 4
print(x.__dict__)    # {}
print(X.__dict__)    # {'__module__': '__main__', 'data': 44444, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}

  说明问题二:数据描述符>实例属性    参考“描述符代码” ,数据描述符的优先级大于实例属性的优先级,此时实例属性name被数据描述符所覆盖,而price没有描述符代理,所以它任然是实例属性。

  说明问题三:实例属性>非数据描述符

class Descriptors:
    """
    非数据描述符
    """

    def __get__(self, instance, owner):
        print("执行Descriptors的get")

    def __delete__(self, instance):
        print("执行Descriptors的delete")


class X:
    data = Descriptors()


x = X()
x.data = 3    # 报错,AttributeError: __set__

四、方法绑定

  因为函数默认实现了描述符协议,所以当以实例或类型访问方法时,__get__首先被调用。类型和实例作为参数被传入__get__,从而截获绑定目标(self),如此就将函数包装称绑定方法对象返回。实际被执行的,就是这个会隐式传入第一个参数的包装品。

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_info(self):
        print("my name is %s ,my age is %s" % (self.name, self.age))

p = Person("ways", 13)
print(p.print_info)    # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>>
print(p.print_info.__get__(p,Person))    # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>>
m = p.print_info.__get__(p,Person)
Person.print_info(m.__self__,)    # my name is ways ,my age is 13
print(m.__self__, m.__func__)    # <__main__.Person object at 0x000002092DBE8438> <function Person.print_info at 0x0000020934BEED08>
"""
方法执行分成了两个步骤:
    p.print_info():
        #1. m = p.print_info.__get__(p,Person)      将函数包装成绑定方法
        #2. m()等价Person.print_info(m.__self__,)   执行时,隐式将self/cls参数传递给目标函数
"""

五、描述符的使用例子

  1、模拟property

class My_Property:
    """
    使用描述符模拟property
    """

    def __init__(self, func):
        self.func = func
        print(self.func)    # <function Test.my_area at 0x000001671607ED90>

    def __get__(self, instance, owner):
        res = self.func(instance)    # 回调传入的函数,将运行结果保存在res中
        setattr(instance,self.func.__name__,res)    # 为函数名func.__name__ 设置值为res,存入对象的字典
        return res


class Test:
    def __init__(self,weight,height):
        self.weight = weight
        self.height = height
    @My_Property
    def my_area(self):
        return self.weight*self.height


test = Test(3, 4)
print(test.my_area)    # 12
print(test.__dict__)    # {'weight': 3, 'height': 4, 'my_area': 12}

六、描述符的使用总结

  1、描述符是可以实现大部分python类特性中的底层魔法,包括@classmethod,@staticmethd,@property甚至是__slots__属性

  2、描述父是很多高级库和框架的重要工具之一,描述符通常是使用到装饰器或者元类的大型框架中的一个组件.

 

 

参考:https://www.cnblogs.com/Lynnblog/p/9033455.html 和《python3学习笔记上》