28. 描述符

一、什么是描述符

  如果一个类中有如下 3 个方法中的任意一个,那么这个类创建的对象,可以称为 描述符对象

object.__get__(self, instance, owner=None)
object.__set__(self, instance, value)
object.__delete__(self, instance)

  如果有另外一个类,这个类中有一个 类属性,这个类属性对应的是上面类创建的实例对象,我们称此时的这个类属性叫做 描述符。通常,描述符具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。

# 定义一个类,让其创建的对象是描述符对象
class Name:
    def __init__(self):
        self__name = None

    def __get__(self, instance, owner):
        print("__get__()方法被调用了!")
        print(f"self: {self}")                  # self是当前描述符对象
        print(f"instance: {instance}")          # instance获取属性值的那个实例对象
        print(f"owner: {owner}", end="\n\n")    # owner是instance的类
        return self.__name

    def __set__(self, instance, value):
        print("__set__()方法被调用了!")
        print(f"self: {self}")                  # self是当前描述符对象
        print(f"insance: {instance}")           # instance是给属性赋值的时候的那个实例对象
        print(f"value: {value}", end="\n\n")    # value是要设置的属性值
        if isinstance(value, str):
            self.__name = value
        else:
            raise TypeError("必须是字符串")

    def __delete__(self, instance):
        print("__delete__()方法被调用了!")
        print(f"self: {self}")
        print(f"instance: {instance}", end="\n\n")
        del self.__name
class Person:
    # 定义一个类属性,它指向的是一个实例对象
    # 且这个对象中有__get__()/__set__()/__delete()
    # 因此,我们就称类属性name就是描述符
    name = Name()
# 使用Person创建一个对象
p = Person()
p.name = "Sakura"
print(p.name, end="\n\n")
del p.name

  当我们自定义一个类属性,且类属性是一个具有 __get__()/__set__()/__delete__() 3 个方法中任意实现一个的类创建的实例对象,那么在获取这个属性的时候,会自动调用 __get__() 方法,在设置类属性的时候,会自动变成调用 __set__() 方法,在删除这个类属性的时候,会自动变成调用 __delete__() 方法;看上去很像 @property 装饰器,实际上 @property 装饰器就是如下所述的方式实现的。

  当访问一个属性时,我们可以不直接给一个值,而是接一个描述器(描述符对象),让访问和修改设置时自动调用 __get__() 方法和 __set__() 方法。再在 __get__() 方法和 __set__() 方法中进行某种处理,就可以实现更改操作属性行为的目的,这就是描述器做的事情。简单的来说,通过描述符能够实现调用属性时,执行特定的方法。

如果将描述符对象赋值给实例属性,就相当于这个属性指向了一个普通的对象,只不过这个对象中 __get__()__set__()__delete__() 方法罢了,不会自动调用 __get__() 方法,而如果类属性是一个描述符,则会自动调用其 __get__() 方法。

二、描述符的调用机制

  在 Python 3 中,所有的类都继承 object 类。当我们调用一个属性时,会自动调用 object 类中的 __getattribute__() 方法,__getattribute__() 方法会起到拦截属性的作用,它会自动判断调用对象时普通数据还是描述符对象。如果 普通的属性,那么获取之后直接返回。如果是描述符,那么就调用其 __get__() 方法,这个方法的返回值当作这个属性的值。如果在调用 __getattribute__() 方法的时候,没有找到这个属性,那么它会调用 __getattr__() 方法来处理没有这个属性的情况,一般会产生一个异常。

一般来说,可以不重写 __getattribute__() 方法,调用默认继承类中中的即可,如果要是重写了,那么可能会导致当前类中的描述符失效。

三、数据描述符与非数据描述符

  同时定义 __get__()__set__() 方法的描述符称为 数据描述符(资料描述符),只定义 __get__() 方法的描述符称为 非数据描述符(非资料描述符);

  当属性名和描述符相同时,在访问这个同名属性时,如果是数据描述符就会先访问描述符,如果是非数据描述符就会先访问属性。

class M:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
        print("get m here")
        return self.x
  
    def __set__(self, instance, value):
        print("set m here")
        self.x = value + 1
class N:
    def __init__(self):
        self.x = 1

    def __get__(self, instance, owner):
        print("get n here")
        return self.x
class AA:
    m = M()             # 数据描述符
    n = N()             # 非数据描述符

    def __init__(self, m, n):
        self.m = m      # 属性m和数据描述符m名字相同,先访问数据描述符
        self.n = n      # 属性n和非数据描述符n名字相同,先访问属性
aa = AA(2, 5)
# 只有n没有m,因为数据描述符同名时,不会访问到属性,会直接访问描述符,所以属性里就查不到m这个属性
print(f"aa.__dict__: {aa.__dict__}")
# m和n都有   
print(f"AA.__dict__: {AA.__dict__}")
# 非数据描述符同名时调用的是属性,为传入的5   
print(f"aa.n: {aa.n}")
# 如果类访问,就调用的是描述符,返回self.x的值        
print(f"AA.n: {AA.n}")
# 其实在aa=AA(2,5)创建实例时,进行了属性赋值,其中相当于进行了aa.m=2
# 但是aa调用m时却不是常规地调用属性m,而是数据描述符m         
print(f"aa.m: {aa.m}")         

四、描述符的注意点

【1】、是否可以用实例属性定义描述符

  如果将描述符对象赋值给实例属性,就相当于这个属性指向了一个普通的对象,只不过这个对象中 __get__()__set__()__delete__() 方法罢了,不会自动调用 __get__() 方法,而如果类属性是一个描述符,则会自动调用其 __get__() 方法。

class Name:
    def __init__(self, name):
        self.__name = name
  
    def __get__(self, instance, owner):
        print("__get__()方法被调用了")
        return self.__name
  
    def __set__(self, instance, value):
        print("__set__()方法被调用了")
        if isinstance(value, str):
            self.__name = value
        else:
            raise TypeError("必须是字符串")
class Person:
    def __init__(self):
        self.name = Name
p = Person()
print(p.name)
p.name = 27185
print(p.name)

【2】、描述符的实例一定是类的属性

class MaxValue:
    def __init__(self, init_value, max_value):
        self.value = init_value
        self.max_value = max_value

    def __get__(self, instance, owner):
        return self.value
  
    def __set__(self, instance, value):
        self.value = min(self.max_value, value)
class Widget:
    volume = MaxValue(0, 10)
a = Widget()
print("a的默认volume值: ", a.volume)
a.volume = 12
print("a设置后的volume值:", a.volume)

b = Widget()
print("b默认的volume值:", b.volume)

描述符的实例一定是类的属性

  当我们通过 a 对象设置 volume 属性时,由于 volume 属性是一个数据描述符,它会调用 MaxValue 类的 __set__() 方法,此时会将 MaxValue 类的 self.value 属性设置为 10,其中 self 对象是 MaxValue 的实例对象。

  然后,我们通过 b 对象访问 volume 属性时,由于 volume 属性是一个数据描述符,它会调用 MaxValue 类的 __get__() 方法,返回属性 self.value 的值,其中 self 对象还是之前的 MaxValue 的实例对象。由于之前通过 a 对象将 self.value 的值设置为 10,此时,我们通过 b 对象访问 volume 属性时,返回 10。

  此时,我们可以通过字典来解决这个问题。

使用字典

class MaxValue:
    def __init__(self, init_value, max_value):
        self.value = init_value
        self.max_value = max_value
        self.data = dict()

    def __get__(self, instance, owner):
        if not instance:
            return self
        return self.data.get(instance, self.value)
  
    def __set__(self, instance, value):
        self.data[instance] = min(self.max_value, value)

五、描述符的应用

5.1、实现@classmethod装饰器

class my_classmethod:
    def __init__(self, func):
        # func属性指向show_info原来指向的函数
        self.func = func

    # my_classmethod实现了__get__()方法
    # 因此my_classmethod是一个描述符
    # __get__()方法是一个闭包
    def __get__(self, instance, owner):
        print(f"self: {self}")
        print(f"instance: {instance}")
        print(f"owner: {owner}")

        def call(*args, **kwargs):
            # 实现原函数的功能
            # owner是Person类
            return self.func(owner, *args, **kwargs)

        return call

class Person:
    name = "unknown"
  
    # 等价于show_info = my_classmethod(show_info)
    # 执行完之后,description指向my_classmethod实例出的对象
    @my_classmethod
    def description(cls):
        print(f"{cls.name}是一个人")

# 使用Person创建一个对象
p = Person()
# p.description是一个描述符
# 它会自动调用my_classmethod类中的__get__()方法
p.description()

5.2、惰性计算

class LazyProperty:
    """
    实现惰性计算(访问时才计算,并将值缓存)
    利用了object.__dict__优先于非数据描述符的特性
    第一次调用__get__()以同名属性存于实例字典中,之后就不再调用__get__()
    """
    # func指向原来的area()方法
    def __init__(self, func):
        self.func = func
  
    # self指向LazyProperty的实例对象,当前对象
    # instance指向Cricle的实例对象,a
    # owner指向Cricle类对象
    def __get__(self, instance, owner):
        print("lazyproperty.__get__()")
        if instance is None:
            return self
        # self.func指向原来的area()方法
        # instance指向Cricle的实例对象,a
        value = self.func(instance)
        # setattr()方法给instance(a)指向的实例对象添加一个属性
        # 属性的名是self.func.__name__(area),值是value(pi*radius**2)
        setattr(instance, self.func.__name__, value)
        return value
class ReadOnlyNumber:
    """
    实现只读属性(实例属性初始化后无法被释放)
    利用了数据描述符优先级高于object.__dict__的特性
    当试图对属性赋值时,总会先调用__set__()方法从而抛出异常
    """
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value
  
    def __set__(self, instance, value):
        raise AttributeError(
            "'%s' is not modifiable" % self.value
        )
class Cricle:
    # ReadOnlyNumber实现了__get__()和__set__()方法
    # 因此,pi是一个描述符
    pi = ReadOnlyNumber(3.14)

    def __init__(self, radius):
        self.radisu = radius
  

    #@LazyProperty是一个装饰器,相当于area=LazyProperty(area)
    @LazyProperty
    def area(self):
        print("computing area")
        return self.pi * self.radisu ** 2
a = Cricle(4)
# area指向LazyProperty的实例对象
# LazyProperty实现了__get__()和__set__()方法
# 因此area是一个描述符,当我们访问a.area会调用LazyProperty的__get__()方法
print(a.area)
# 第二次调用时a的对象已经添加了area属性
# 当属性名和描述符相同时,在访问这个同名属性时,如果是非数据描述符就会先访问属性。
print(a.area)

惰性计算

当属性名和描述符相同时,在访问这个同名属性时,如果是数据描述符就会先访问描述符,如果是非数据描述符就会先访问属性。

posted @ 2024-11-05 21:06  星光映梦  阅读(11)  评论(0编辑  收藏  举报