`Python`的`描述符`:`__get__`、`__set__`和`__delete__`的魔术方法与`属性`的`实现`。

好的,让我们深入探讨Python描述符及其与属性实现的关系。

Python描述符:控制属性访问的利器

描述符是Python中一种强大的协议,它允许你自定义对象属性的访问方式。它通过实现__get____set____delete__这三个特殊方法(也称为魔术方法)来实现。当一个类的属性被设置为描述符时,对该属性的访问、赋值和删除操作会被重定向到描述符对象的方法。

描述符协议

描述符协议定义了如何通过描述符来管理属性。一个实现了__get____set____delete__中任何一个方法的类,就可以被称为描述符。

  • __get__(self, instance, owner): 这个方法在访问属性时被调用。

    • self: 描述符实例本身。
    • instance: 拥有该属性的实例。如果通过类访问属性,则为None
    • owner: 拥有该属性的类。
    • 该方法应该返回属性的值。
  • __set__(self, instance, value): 这个方法在给属性赋值时被调用。

    • self: 描述符实例本身。
    • instance: 拥有该属性的实例。
    • value: 要赋给属性的值。
    • 该方法应该修改实例的属性。
  • __delete__(self, instance): 这个方法在删除属性时被调用。

    • self: 描述符实例本身。
    • instance: 拥有该属性的实例。
    • 该方法应该删除实例的属性。

描述符的类型

根据实现了哪些方法,描述符可以分为两类:

  • 数据描述符 (Data Descriptor): 实现了__get____set__ 方法的描述符。数据描述符优先于实例字典中的同名属性。

  • 非数据描述符 (Non-Data Descriptor): 只实现了 __get__ 方法的描述符。如果实例字典中存在同名属性,则实例字典中的属性优先。方法(methods)通常是非数据描述符。

描述符的应用场景

描述符在以下场景中非常有用:

  • 属性验证: 在设置属性值之前进行验证,确保值的有效性。
  • 类型转换: 在访问或设置属性时进行类型转换。
  • 延迟计算: 只在需要时才计算属性的值,并将结果缓存起来。
  • 只读属性: 禁止修改属性的值。
  • 属性代理: 将属性的访问委托给另一个对象。

属性的实现

Python的property()函数是一种内置的非数据描述符,它提供了一种简洁的方式来创建和管理属性。property() 函数接受四个可选参数:

  • fget: 获取属性值的函数。
  • fset: 设置属性值的函数。
  • fdel: 删除属性的函数。
  • doc: 属性的文档字符串。

property() 函数返回一个属性对象,该对象是一个非数据描述符。当访问、赋值或删除属性时,会调用相应的函数。

描述符与属性的比较

特性 描述符 property()
实现方式 通过定义包含 __get____set____delete__ 方法的类。 通过 property() 函数创建。
灵活性 更灵活,可以实现更复杂的逻辑。 相对简单,适用于常见的属性管理场景。
代码量 通常需要更多的代码。 代码量较少,更简洁。
数据/非数据描述符 可以是数据描述符或非数据描述符。 创建的是非数据描述符。

代码示例

1. 使用描述符进行属性验证

class ValidatedString:
    def __init__(self, minlen=0, maxlen=None):
        self.minlen = minlen
        self.maxlen = maxlen
        self._value = None
        self._name = None  # 用于存储属性名

    def __set_name__(self, owner, name):
        #在Python 3.6之后,如果描述符类定义了__set_name__方法,
        #那么在描述符被定义到某个类的类属性中时,__set_name__方法会被自动调用。
        #owner 是拥有这个描述符的类,name 是描述符所赋值的属性名。
        self._name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError(f"'{self._name}' must be a string")
        if len(value) < self.minlen:
            raise ValueError(f"'{self._name}' must be at least {self.minlen} characters long")
        if self.maxlen is not None and len(value) > self.maxlen:
            raise ValueError(f"'{self._name}' must be at most {self.maxlen} characters long")
        self._value = value

    def __delete__(self, instance):
        del self._value

class Person:
    name = ValidatedString(minlen=3, maxlen=20)
    age = ValidatedString()

    def __init__(self, name, age):
        self.name = name  # 调用 ValidatedString 的 __set__ 方法
        self.age = str(age) # 调用 ValidatedString 的 __set__ 方法

# 示例用法
person = Person("Alice", 30)
print(person.name)

try:
    person.name = "Bo"  # 长度不够,引发 ValueError
except ValueError as e:
    print(e)

try:
    person.age = 123  # 类型错误,引发 TypeError
except TypeError as e:
    print(e)

del person.name # 删除属性

在这个例子中,ValidatedString 是一个数据描述符,它验证 name 属性的值是否为字符串,并且长度是否在指定的范围内。Person 类使用 ValidatedString 描述符来定义 name 属性。当设置 person.name 的值时,会调用 ValidatedString__set__ 方法,进行验证。

2. 使用 property() 函数创建属性

class Circle:
    def __init__(self, radius):
        self._radius = radius

    def get_radius(self):
        return self._radius

    def set_radius(self, radius):
        if radius <= 0:
            raise ValueError("Radius must be positive")
        self._radius = radius

    def get_area(self):
        return 3.14159 * self._radius * self._radius

    radius = property(get_radius, set_radius)
    area = property(get_area)  # 只读属性

# 示例用法
circle = Circle(5)
print(circle.radius)  # 调用 get_radius
circle.radius = 10   # 调用 set_radius
print(circle.area)    # 调用 get_area

在这个例子中,property() 函数用于创建 radiusarea 属性。get_radiusset_radius 函数分别用于获取和设置 _radius 属性的值。get_area 函数用于计算圆的面积,area 属性是只读的,因为它没有定义 fset 函数。

3. 使用描述符实现延迟计算

import time

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self._value = None

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self._value is None:
            print("Calculating value...")
            self._value = self.func(instance)
            print("Value calculated.")
        return self._value

class MyClass:
    def __init__(self, data):
        self.data = data

    @LazyProperty
    def result(self):
        # 模拟耗时计算
        time.sleep(2)
        return self.data * 2

# 示例用法
obj = MyClass(10)
print("First access:")
print(obj.result)
print("Second access:")
print(obj.result)

在这个例子中,LazyProperty 是一个描述符,它实现了延迟计算。result 属性的值只在第一次访问时才计算,并将结果缓存起来。后续的访问直接返回缓存的值,避免重复计算。

4. 实现只读属性

class ReadOnlyProperty:
    def __init__(self, value):
        self._value = value

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        raise AttributeError("Cannot set read-only attribute")

class Configuration:
    version = ReadOnlyProperty("1.0")

# 示例用法
config = Configuration()
print(config.version)

try:
    config.version = "2.0"
except AttributeError as e:
    print(e)

在这个例子中,ReadOnlyProperty 是一个描述符,它只定义了 __get__ 方法,没有定义 __set__ 方法。因此,version 属性是只读的,不能被修改。尝试修改 version 属性会引发 AttributeError 异常。

5. 属性代理

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        print("Engine started with {} horsepower".format(self.horsepower))

class Car:
    def __init__(self, horsepower):
        self.engine = Engine(horsepower)

    def start(self):
        self.engine.start()

class EngineProxy:
    def __get__(self, instance, owner):
        return instance.engine

class CarWithProxy:
    engine = EngineProxy()

    def __init__(self, horsepower):
        self.engine = Engine(horsepower) # 实际赋值到实例的 __dict__ 中

car = CarWithProxy(200)
car.engine.start()
print(car.engine.horsepower)

在这个示例中, EngineProxy充当了Engine的一个代理。在CarWithProxy中访问engine属性,实际上是通过EngineProxy来获取CarWithProxy实例中的engine属性。但是请注意,因为EngineProxy只实现了__get__方法,所以它是一个非数据描述符。这意味着,如果你直接在CarWithProxy实例上设置engine属性,例如car.engine = new_engine,你将在实例的__dict__中创建一个新的engine属性,它会覆盖掉类定义的描述符。 如果想要阻止这种行为,EngineProxy需要变成数据描述符,需要实现__set__方法,并在__set__方法中决定如何处理赋值操作,例如抛出异常或将值赋给内部的Engine实例。

6. 使用__set_name__ (Python 3.6+)

这个方法在描述符被绑定到类时自动调用,允许描述符知道它所绑定的属性名称。

class MyDescriptor:
    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        setattr(instance, self.private_name, value)

class MyClass:
    my_attribute = MyDescriptor()

    def __init__(self, value):
        self.my_attribute = value

# 示例用法
obj = MyClass(10)
print(obj.my_attribute)  # 输出 10
obj.my_attribute = 20
print(obj.my_attribute)  # 输出 20

在这个例子中,__set_name__ 方法被用来创建一个私有属性名,以避免与实例字典中的其他属性冲突。

理解方法绑定

当一个方法被作为类的属性访问时,它会被转换为一个非数据描述符。当通过实例访问这个方法时,描述符协议会创建一个绑定方法对象,并将实例作为第一个参数传递给方法。这就是为什么在定义实例方法时,需要使用 self 参数。

总结

描述符是一种强大的工具,可以让你自定义对象属性的访问方式,实现属性验证、类型转换、延迟计算等功能。property() 函数是一种内置的非数据描述符,提供了一种简洁的方式来创建和管理属性。理解描述符协议和 property() 函数的工作原理,可以帮助你编写更灵活、更健壮的 Python 代码。

选择合适的实现方式

使用描述符还是 property() 函数,取决于你的具体需求。如果需要实现复杂的属性管理逻辑,或者需要控制属性的访问、赋值和删除操作,那么描述符是更好的选择。如果只需要简单的属性管理,那么 property() 函数可能更简洁。

深入理解属性访问机制

理解Python的属性访问机制对于编写高质量的Python代码至关重要。描述符是这个机制的核心组成部分,掌握描述符的使用可以让你更好地控制对象的行为。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注