详解python描述符的原理和应用

描述符在Python中有许多实际应用,它们通常被用来控制对属性的访问,添加额外的处理逻辑,或实现特定的编程模式。

一 实现原理

描述符协议

描述符是实现了特定方法(__get__、__set__、__delete__)的类,这些方法允许你控制属性的访问、设置和删除行为。Python 的属性访问是通过描述符协议来实现的。当我们定义一个新属性时,可以通过描述符来控制这个属性的访问机制。下面是每个方法及其参数的详细解释:

1. __get__(self, obj, type=None)

  • self: 描述符实例本身。这个参数是方法的标准第一个参数,代表方法的调用者。
  • obj: 访问属性的对象实例。当描述符从对象实例访问时,obj就是那个实例。如果是通过类来访问描述符,obj将会是None。
  • type: 可选参数,是访问属性的对象的类型。如果描述符是通过类来访问,type就是那个类;如果是通过实例来访问,type是实例的类。这个参数通常在需要区分是通过实例还是类来访问属性时使用。

2. __set__(self, obj, value)

  • self: 描述符实例本身。
  • obj: 将属性设置在其上的对象实例。这指的是你想要设置属性的那个对象。
  • value: 想要设置的值。这是赋给属性的新值。

在__set__方法中,你可以添加逻辑来控制如何设置属性值,比如类型检查、值验证等。

3. __delete__(self, obj)

  • self: 描述符实例本身。
  • obj: 想要删除属性的对象实例。

这个方法允许你定义当属性被删除时的行为(使用del语句)。例如,你可以在属性被删除前执行清理操作或设置一些标志。

描述符类型

  • 数据描述符:同时定义了__get__和__set__方法的描述符。
  • 非数据描述符:只定义了__get__方法的描述符。

数据描述符优先于__dict__查找 __dict__查找优先于非数据描述符

class MyClass:
    data_descriptor = DataDescriptor()
    non_data_descriptor = NonDataDescriptor()

    def __init__(self):
        self.data_descriptor = "Instance value"
        self.non_data_descriptor = "Instance value"
        self.normal_attribute = "Normal value"


instance = MyClass()
# 访问数据描述符
print(instance.data_descriptor)  # 应触发数据描述符的__get__方法
# 访问非数据描述符
print(instance.non_data_descriptor)  # 实例字典中的值优先
# 访问普通属性
print(instance.normal_attribute)  # 直接从__dict__获取
  1. 当我们访问data_descriptor时,尽管实例字典中存在同名的键,数据描述符的__get__方法仍然会被调用,因为数据描述符的优先级高于实例字典。
  2. 对于non_data_descriptor,虽然它是一个非数据描述符,但当我们尝试访问它时,将会获取实例字典中的值,因为非数据描述符的优先级低于实例字典。
  3. 对于normal_attribute,它是一个普通属性,直接从实例字典中获取其值

属性查找顺序

在每次属性查找中,描述符协议有对象的特殊方法__getattribute__调用。它是对象属性访问的入口点,当访问任何属性时都会调用它。它会按照一定的顺序查找属性:首先查找数据描述符,然后是实例属性(实例对象的__dict__里),接着是非数据描述符,最后是类属性。

property 描述符

property是一个内置的描述符类,用于创建可管理的属性。通过定义 getter、setter 和 deleter 方法,我们可以控制对属性的访问和修改。这些方法用于创建具有不同 fget, fset, fdel 函数的新属性对象。

class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        super().__init__(fget, fset, fdel, doc)
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)
   def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)
   def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)
   def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

class Person:
    def __init__(self, name):
        self.name = name
    @Property
    def name(self):
        return self._name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute")

1.其中__get__() 方法实现起来比看上去要复杂得多,归结于实例变量和类变量的不同。 如果一个描述器被当做一个类变量来访问,那么 obj 参数被设置成 None 。 这种情况下,标准做法就是简单的返回这个描述器本身即可

2.用@Property装饰一个方法时,Python创建了一个Property实例,并将这个方法作为fget参数传递给这个实例。当使用@name.setter,实际上调用了原有Property实例(此时是name属性)的setter方法。type(self)创建了一个新的Property实例,复制了fget和fdel,但使用了新的fset。这个新创建的Property实例(带有新的setter)替换了类属性中原来的Property实例。

如果你只是想简单的自定义某个类的单个属性访问的话就不用去写描述器了

描述器只能在类级别被定义,而不能为每个实例单独定义。下面的Integer描述符无法工作

class Point:
    def __init__(self, x):
        self.x = Integer('x') 
        self.x = x

扩展定义父类的property

class Person:
    def __init__(self, name):
        self.name = name
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute")

如下继承自Person并扩展了 name 属性的功能:

class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name
    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)#请求从SubPerson的父类中查找方法或属性
    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)

如果只想扩展property的某一个方法可以像下面这样写:

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name

这么写 property之前已经定义过的方法会被复制过来,而getter函数被替换

二 实际应用

类中的staticmethod 和 classmethod

  • staticmethod 返回函数的静态方法版本,它不接收额外的首个参数(如实例或类)。
class MyStaticMethod(staticmethod):
    def __init__(self, func):
        super().__init__(func)
    def __get__(self, obj, objtype=None):
        return self.func
  • classmethod 将方法转换为类方法,第一个参数始终是类本身,而不是实例。
class MyClassMethod(classmethod):
    def __init__(self, func):
        super().__init__(func)
    def __get__(self, obj, klass=None):#obj和klass指的是对象和类
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.func(klass, *args)
        return newfunc

通过这些描述符,Python 提供了灵活的方式来控制属性的行为,使得我们可以根据需要定制属性的读取、设置和删除操作。

类中的方法

当一个方法被定义在类中时,它自动成为那个类的非数据描述符。这是因为方法是通过__get__方法来绑定到类或实例的。当你从类或类的实例访问方法时,__get__方法会被调用,它返回的是一个绑定方法(如果是从实例访问)或者一个函数(如果是从类访问)。

这种机制允许方法在被调用时知道它们是从类的哪个实例调用的,这也是为什么方法的第一个参数通常是self,它代表实例本身。非数据描述符的这种行为允许方法具有与它们所属的实例相关的上下文,这是面向对象编程的一个关键特性。

当你在Python中定义一个普通方法时,Python自动为你创建了这样一个描述符对象,所以你不需要手动实现它,这是语言内置的特性。

在如下示例中,MethodDescriptor类实现了__get__方法。当尝试访问my_method时,__get__方法会被调用。如果__get__是从实例调用的(即obj不是None),它返回一个绑定到特定对象的方法。如果是从类调用的(即obj是None),它只返回函数本身。

class MethodDescriptor:
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.func
        return self.func.__get__(obj, objtype)

class MyClass:
    @MethodDescriptor
    def my_method(self, x):
        return x * 2

属性验证

描述符可以用来确保给属性赋值时符合特定条件。例如,你可以创建一个描述符来确保某个属性的值总是大于零。

class PositiveNumber:
    def __init__(self, name):
        self.name = name
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("值必须大于0")
        instance.__dict__[self.name] = value

class MyClass:
    number = PositiveNumber('number')
    def __init__(self, number):
        self.number = number

缓存

描述符可以用来实现缓存机制。当计算或检索属性值特别昂贵时,可以使用描述符缓存这些值,避免重复计算。

class CachedAttribute:
    def __init__(self, method):
        self.method = method
        self.cache_name = '_' + method.__name__
    def __get__(self, instance, owner):
        if self.cache_name not in instance.__dict__:
            instance.__dict__[self.cache_name] = self.method(instance)
        return instance.__dict__[self.cache_name]

class MyClass:
    @CachedAttribute
    def expensive_computation(self):
        # 假设这是一个计算成本很高的操作
        return sum(i * i for i in range(10000))      

当一个描述器被放入一个类的定义时, 每次访问属性时它的 __get__() 、__set__() 和 __delete__() 方法就会被触发。 不过,如果一个描述器仅仅只定义了一个 __get__() 方法的话,它比通常的具有更弱的绑定。 特别地,只有当被访问属性不在实例底层的字典中时 __get__() 方法才会被触发。lazyproperty 类利用这一点,使用 __get__() 方法在实例中存储计算出来的值, 这个实例使用相同的名字作为它的property。 这样一来,结果值被存储在实例字典中并且以后就不需要再去计算这个property了。

class lazyproperty:
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)#结果值被存储在实例字典
            return value      

class Circle:
    def __init__(self, radius):
        self.radius = radius
    @lazyproperty
    def area(self):
        print('Computing area')
        return math.pi * self.radius ** 2

属性访问日志记录

class LoggingDescriptor:
    def __init__(self, name):
        self.name = name
    def __get__(self, instance, owner):
        value = instance.__dict__.get(self.name, None)
        print(f"访问属性 {self.name},值为 {value}")
        return value
    def __set__(self, instance, value):
        print(f"设置属性 {self.name} 为 {value}")
        instance.__dict__[self.name] = value

class MyClass:
    attribute = LoggingDescriptor('attribute')

只读属性

通过仅实现__get__方法的描述符,可以创建只读属性。尝试修改这些属性将会引发错误。

class ReadOnly:
    def __init__(self, value):
        self.value = value
    def __get__(self, instance, owner):
        return self.value

class MyClass:
    readonly_attribute = ReadOnly('我是只读的')

三 存在意义

Python中的描述符提供了一种强大的方式来重定义属性的访问、修改和删除行为。这种机制允许开发者在底层捕捉到这些操作,并嵌入额外的逻辑,比如类型检查、属性保护、延迟计算等。

他编程语言提供了可以实现类似功能的机制:

  1. Java:Java中没有直接与Python描述符相对应的特性,但可以通过getter和setter方法实现属性访问控制。此外,Java的注解和反射机制也可以用来在一定程度上模拟描述符的某些行为。
  2. C#:C#中的属性(Properties)与Python的描述符有类似的功能。在C#中,你可以定义属性的get和set方法,来控制属性的访问和赋值行为,这与Python的描述符有相似之处。
  3. JavaScript:JavaScript提供了getter和setter,可以在对象的属性被访问或修改时执行特定的函数,这与Python描述符的某些功能相似。
  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值