Python的描述符:理解`__get__`、`__set__`和`__delete__`在属性访问中的作用。

Python 描述符:深入理解属性访问的魔法

大家好,今天我们来深入探讨 Python 中一个强大而有时令人困惑的特性:描述符。 描述符是 Python 实现属性访问和管理的一种方式,它允许我们自定义属性的读取、设置和删除行为。 掌握描述符对于编写更灵活、更可控、更符合面向对象原则的代码至关重要。

什么是描述符?

简单来说,描述符是一个实现了描述符协议的 Python 对象。 描述符协议定义了三个特殊方法:__get____set____delete__。 当一个类属性是一个描述符对象时,对该属性的访问(读取、设置、删除)会被描述符对象的方法所拦截。

更具体地说,如果一个类(我们称之为 包含类)的属性是一个实现了 __get____set____delete__ 中至少一个方法的类的实例(这个实例就是 描述符对象),那么这个属性就成为了一个描述符。

描述符协议方法

方法 触发时机 参数 返回值
__get__ 当描述符属性被访问时触发。 例如:obj.xClass.x,其中 x 是一个描述符。 self: 描述符实例本身. instance: 拥有该描述符属性的对象实例。 如果通过类访问,则为 None. owner: 拥有该描述符属性的类。 通常是描述符属性的值. 如果 instanceNone,通常返回描述符对象本身 (例如,用于文档或进一步配置).
__set__ 当描述符属性被赋值时触发。 例如:obj.x = value,其中 x 是一个描述符。 self: 描述符实例本身. instance: 拥有该描述符属性的对象实例。 value: 要赋给描述符属性的值. 通常是 None. __set__ 方法负责执行赋值操作,不需要显式返回任何值.
__delete__ 当描述符属性被删除时触发。 例如:del obj.x,其中 x 是一个描述符。 self: 描述符实例本身. instance: 拥有该描述符属性的对象实例。 通常是 None. __delete__ 方法负责执行删除操作,不需要显式返回任何值.

描述符的两种类型:数据描述符和非数据描述符

根据描述符实现的方法,可以将描述符分为两种类型:

  • 数据描述符 (Data Descriptor): 实现了 __get____set__ 方法的描述符。数据描述符会覆盖实例字典中的属性查找。
  • 非数据描述符 (Non-data Descriptor): 只实现了 __get__ 方法的描述符。 非数据描述符不会覆盖实例字典中的属性查找。 如果实例字典中存在同名属性,则优先使用实例字典中的属性。

这两种描述符在属性查找的优先级上有所不同,这是理解描述符行为的关键。

描述符的应用场景

描述符在 Python 中有很多应用场景,例如:

  • 属性验证: 可以在 __set__ 方法中验证属性的值,确保其符合特定的规则。
  • 属性计算: 可以在 __get__ 方法中动态计算属性的值,而不是将其存储在实例中。
  • 只读属性: 只实现 __get__ 方法,而不实现 __set__ 方法,可以创建只读属性。
  • 属性代理: 可以将属性的访问委托给另一个对象。
  • 类型转换: 可以在 __set__ 方法中将属性的值转换为特定的类型。

描述符的工作原理:属性查找的优先级

理解描述符的工作原理,需要了解 Python 属性查找的优先级。 当我们访问一个对象的属性时,Python 会按照以下顺序查找:

  1. 数据描述符: 如果类中存在数据描述符,则优先调用描述符的 __get____set__ 方法。
  2. 实例字典: 如果实例字典中存在同名属性,则返回实例字典中的值。
  3. 非数据描述符: 如果类中存在非数据描述符,则调用描述符的 __get__ 方法。
  4. 类字典: 如果类字典中存在同名属性,则返回类字典中的值。
  5. 父类: 如果在类和父类中都没有找到属性,则会沿着继承链向上查找。
  6. __getattr__: 如果对象定义了 __getattr__ 方法,则调用该方法。
  7. AttributeError: 如果以上步骤都无法找到属性,则抛出 AttributeError 异常。

描述符的实现示例

接下来,我们通过一些具体的示例来演示描述符的实现和使用。

示例 1:温度转换描述符

class Celsius:
    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

class Temperature:
    def __get__(self, instance, owner):
        return instance._temperature

    def __set__(self, instance, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below -273.15")
        instance._temperature = value

class Celsius:
    temperature = Temperature()  # 描述符实例

    def __init__(self, temperature=0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

# 使用示例
c = Celsius(25)
print(c.temperature)  # 输出: 25
c.temperature = 30
print(c.temperature)  # 输出: 30

try:
    c.temperature = -300
except ValueError as e:
    print(e)  # 输出: Temperature cannot be below -273.15

在这个例子中,Temperature 类是一个数据描述符,它实现了 __get____set__ 方法。 Celsius 类使用 Temperature 描述符来管理 temperature 属性。 __set__ 方法用于验证温度值,确保其不低于绝对零度。 __get__ 方法用于返回温度值。

示例 2:只读属性描述符

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

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

class MyClass:
    read_only_attr = ReadOnly(10)

# 使用示例
obj = MyClass()
print(obj.read_only_attr)  # 输出: 10

try:
    obj.read_only_attr = 20
except AttributeError as e:
    print(e)  # 输出: can't set attribute

在这个例子中,ReadOnly 类是一个非数据描述符,它只实现了 __get__ 方法。 MyClass 类使用 ReadOnly 描述符来创建一个只读属性 read_only_attr。 由于没有实现 __set__ 方法,尝试修改该属性会引发 AttributeError 异常。注意:如果直接在实例字典中设置obj.__dict__['read_only_attr'] = 20, 那么这个值是可以被设置的,因为绕过了描述符的__set__方法。

示例 3:属性代理描述符

class Name:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

class Person:
    def __init__(self, first_name, last_name):
        self.name = Name(first_name, last_name) #组合

    @property
    def first_name(self):
        return self.name.first_name

    @first_name.setter
    def first_name(self, value):
        self.name.first_name = value

    @property
    def last_name(self):
        return self.name.last_name

    @last_name.setter
    def last_name(self, value):
        self.name.last_name = value

    @property
    def full_name(self):
        return self.name.full_name

# 使用示例
person = Person("John", "Doe")
print(person.full_name)
person.first_name = "Jane"
print(person.full_name)

这个例子展示了如何使用属性代理。 Person 类将 first_namelast_name 属性的访问委托给 Name 对象。 通过使用 @property 装饰器,我们可以将属性的访问和设置操作委托给 Name 对象的相应属性。

示例 4:利用描述符进行类型检查

class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"Expected {self.expected_type}, got {type(value)}")
        instance.__dict__[self.name] = value

class Integer(Typed):
    def __init__(self, name):
        super().__init__(name, int)

class Float(Typed):
    def __init__(self, name):
        super().__init__(name, float)

class String(Typed):
    def __init__(self, name):
        super().__init__(name, str)

class MyClass:
    age = Integer('age')
    salary = Float('salary')
    name = String('name')

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

# 使用示例
my_object = MyClass(age=30, salary=50000.0, name="Alice")
print(my_object.age, my_object.salary, my_object.name)

try:
    my_object.age = "thirty"
except TypeError as e:
    print(e)

try:
    my_object.salary = 100
except TypeError as e:
    print(e)

try:
    my_object.name = 123
except TypeError as e:
    print(e)

这个示例使用描述符来进行类型检查。Typed 类是一个通用的描述符,它接受属性的名称和期望的类型作为参数。Integer, Float, String 类继承自 Typed 类,并指定了相应的类型。在 __set__ 方法中,它会检查值的类型是否与期望的类型匹配,如果不匹配,则抛出 TypeError 异常。

__set_name__ 方法 (Python 3.6+)

从 Python 3.6 开始,引入了一个新的描述符协议方法:__set_name__(self, owner, name)。 当描述符被添加到类中时,该方法会被调用。 owner 参数是拥有该描述符的类,name 参数是描述符属性的名称。 这使得描述符可以知道它在类中的名称,而无需显式传递。

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

    def __get__(self, instance, owner):
        return instance.__dict__[self.private_name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected a string, got {type(value)}')
        instance.__dict__[self.private_name] = value

class MyClass:
    name = ValidString()

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

obj = MyClass("Bob")
print(obj.name)

在这个例子中,ValidString 描述符使用 __set_name__ 方法来自动生成私有属性名称,避免了硬编码属性名称,提高了代码的可重用性和可维护性。

描述符和 property 的关系

property 是 Python 内置的描述符,它提供了一种更简洁的方式来创建属性的 getter、setter 和 deleter 方法。 实际上,property 对象内部也是通过描述符协议来实现的。

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

    def get_value(self):
        return self._value

    def set_value(self, value):
        self._value = value

    def del_value(self):
        del self._value

    value = property(get_value, set_value, del_value, "A property for managing value")

# 使用示例
obj = MyClass(10)
print(obj.value)  # 输出: 10
obj.value = 20
print(obj.value)  # 输出: 20
del obj.value

在这个例子中,我们使用 property 函数来创建一个名为 value 的属性,并将 get_valueset_valuedel_value 方法分别绑定到该属性的 getter、setter 和 deleter 操作。

等价于:

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

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value

    @value.deleter
    def value(self):
        del self._value

描述符的注意事项

  • 描述符只对类属性有效: 描述符必须是类属性才能生效。 如果将描述符对象作为实例属性,则它不会拦截属性访问。
  • 数据描述符优先于实例字典: 数据描述符会覆盖实例字典中的属性查找。 这意味着,即使实例字典中存在同名属性,也会优先调用描述符的 __get____set__ 方法。
  • 非数据描述符不覆盖实例字典: 非数据描述符不会覆盖实例字典中的属性查找。 如果实例字典中存在同名属性,则优先使用实例字典中的属性。
  • 理解属性查找的优先级: 掌握属性查找的优先级是理解描述符行为的关键。

描述符是高级特性,需要小心使用

描述符是一个强大的工具,但同时也增加了代码的复杂性。 在使用描述符时,需要仔细考虑其带来的好处和坏处,并确保代码的可读性和可维护性。 只有在确实需要自定义属性访问行为时,才应该使用描述符。

掌握描述符,编写更精妙的代码

通过今天的学习,我们深入了解了 Python 描述符的概念、类型、应用场景和工作原理。 掌握描述符可以帮助我们编写更灵活、更可控、更符合面向对象原则的代码。 希望大家在实际开发中能够灵活运用描述符,编写出更加精妙的 Python 程序。

发表回复

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