目录

Python 描述符与属性

学习目标

  • 理解 @property 装饰器的用法
  • 掌握 getter、setter、deleter 的实现
  • 理解描述符协议
  • 能够使用描述符实现属性验证和计算属性

一、@property 装饰器

@property 允许将方法当作属性来访问,同时可以控制属性的读写行为。

1.1 基本用法

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

    @property
    def radius(self):
        """getter:获取半径"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """setter:设置半径"""
        if value < 0:
            raise ValueError("半径不能为负数")
        self._radius = value

    @radius.deleter
    def radius(self):
        """deleter:删除半径"""
        print("删除半径")
        del self._radius

    @property
    def area(self):
        """计算属性:只读"""
        return 3.14159 * self._radius ** 2

    @property
    def circumference(self):
        """计算属性:只读"""
        return 2 * 3.14159 * self._radius


# 使用
c = Circle(5)
print(c.radius)         # 5(像属性一样访问)
c.radius = 10           # 像属性一样设置
print(c.area)           # 314.159(计算属性)
print(c.circumference)  # 62.8318

# c.area = 100          # AttributeError: 没有 setter,只读
# c.radius = -5         # ValueError: 半径不能为负数

1.2 属性验证

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age

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

    @name.setter
    def name(self, value):
        if not isinstance(value, str) or len(value.strip()) == 0:
            raise ValueError("姓名必须是有效的字符串")
        self._name = value.strip()

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise TypeError("年龄必须是整数")
        if not 0 <= value <= 150:
            raise ValueError("年龄必须在 0-150 之间")
        self._age = value


p = Person("Alice", 25)
print(p.name, p.age)    # Alice 25

p.name = "Bob"
p.age = 30
print(p.name, p.age)    # Bob 30

# p.age = -5            # ValueError
# p.age = 200           # ValueError
# p.name = ""           # ValueError

二、计算属性

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("宽度必须为正数")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("高度必须为正数")
        self._height = value

    @property
    def area(self):
        """面积 - 计算属性"""
        return self._width * self._height

    @property
    def perimeter(self):
        """周长 - 计算属性"""
        return 2 * (self._width + self._height)

    @property
    def diagonal(self):
        """对角线 - 计算属性"""
        return (self._width ** 2 + self._height ** 2) ** 0.5


r = Rectangle(3, 4)
print(f"面积: {r.area}")         # 12
print(f"周长: {r.perimeter}")    # 14
print(f"对角线: {r.diagonal:.2f}")  # 5.00

r.width = 6
r.height = 8
print(f"新面积: {r.area}")       # 48

三、描述符协议

描述符是实现了 __get____set____delete__ 方法的类,用于管理属性的访问。

3.1 描述符基础

class Validator:
    """属性验证描述符"""

    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.name = None

    def __set_name__(self, owner, name):
        """设置描述符的名称(Python 3.6+)"""
        self.name = name

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

    def __set__(self, instance, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 必须 >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 必须 <= {self.max_value}")
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]


class Person:
    age = Validator(min_value=0, max_value=150)
    score = Validator(min_value=0, max_value=100)

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


p = Person("Alice", 25, 85)
print(p.age, p.score)   # 25 85

p.age = 30
p.score = 95

# p.age = -5            # ValueError: age 必须 >= 0
# p.score = 101         # ValueError: score 必须 <= 100

3.2 类型检查描述符

class Typed:
    """类型检查描述符"""

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

    def __set_name__(self, owner, name):
        self.name = name

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

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} 必须是 {self.expected_type.__name__} 类型,"
                f"而不是 {type(value).__name__}"
            )
        instance.__dict__[self.name] = value


class TypedPerson:
    name = Typed(str)
    age = Typed(int)
    height = Typed(float)

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


person = TypedPerson("Alice", 25, 1.75)
print(person.name, person.age, person.height)

# person.age = "25"     # TypeError: age 必须是 int 类型
# person.name = 123     # TypeError: name 必须是 str 类型

四、使用描述符实现 ORM 风格

class Field:
    """数据库字段描述符"""

    def __init__(self, field_type="TEXT", default=None):
        self.field_type = field_type
        self.default = default
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

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

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

    def __repr__(self):
        return f"Field({self.field_type}, default={self.default})"


class ModelMeta(type):
    """元类:自动收集字段信息"""

    def __new__(mcs, name, bases, namespace):
        fields = {}
        for key, value in list(namespace.items()):
            if isinstance(value, Field):
                fields[key] = value
        namespace['_fields'] = fields
        return super().__new__(mcs, name, bases, namespace)


class Model(metaclass=ModelMeta):
    """ORM 基类"""

    def __init__(self, **kwargs):
        for name, field in self._fields.items():
            value = kwargs.get(name, field.default)
            setattr(self, name, value)

    def __repr__(self):
        attrs = ", ".join(
            f"{name}={getattr(self, name)!r}"
            for name in self._fields
        )
        return f"{self.__class__.__name__}({attrs})"

    def to_dict(self):
        return {name: getattr(self, name) for name in self._fields}


class User(Model):
    id = Field("INTEGER", default=0)
    name = Field("TEXT", default="")
    email = Field("TEXT", default="")
    age = Field("INTEGER", default=0)


# 使用
user = User(id=1, name="Alice", email="alice@example.com", age=25)
print(user)
# User(id=1, name='Alice', email='alice@example.com', age=25)

print(user.to_dict())
# {'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'age': 25}

五、@property vs 描述符

特性 @property 描述符
适用场景 单个类的特定属性 多个类/多个属性的通用逻辑
代码复用 难以复用 可复用描述符类
复杂度 简单 较复杂
灵活性 有限 高度灵活
# @property 适合单个属性
class MyClass:
    @property
    def value(self):
        return self._value

# 描述符适合通用逻辑
class Validator:
    def __set__(self, instance, value):
        # 验证逻辑...
        pass

class ClassA:
    x = Validator()
    y = Validator()

class ClassB:
    z = Validator()

六、综合示例

"""
温度类:支持摄氏度和华氏度自动转换
"""

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius

    @property
    def celsius(self):
        """摄氏度"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

    @property
    def fahrenheit(self):
        """华氏度 - 计算属性"""
        return self._celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        """设置华氏度时自动转换为摄氏度"""
        self._celsius = (value - 32) * 5 / 9

    @property
    def kelvin(self):
        """开尔文"""
        return self._celsius + 273.15

    @kelvin.setter
    def kelvin(self, value):
        self._celsius = value - 273.15

    def __str__(self):
        return f"{self.celsius:.1f}°C / {self.fahrenheit:.1f}°F / {self.kelvin:.1f}K"

    def __repr__(self):
        return f"Temperature(celsius={self.celsius})"


# 使用
t = Temperature(25)
print(t)                    # 25.0°C / 77.0°F / 298.1K

t.fahrenheit = 100
print(t.celsius)            # 37.777...
print(t)                    # 37.8°C / 100.0°F / 310.9K

t.kelvin = 0
print(t)                    # -273.1°C / -459.7°F / 0.0K

小结

  • @property 将方法转换为属性,支持 getter、setter、deleter
  • 计算属性是只读的 @property,根据其他属性动态计算
  • 描述符是实现 __get____set____delete__ 的类
  • 描述符适合在多个属性/类之间复用逻辑
  • @property 语法简洁,描述符更灵活可复用
  • Python 3.6+ 的 __set_name__ 让描述符使用更方便

练习

  1. BankAccount 类添加 balance 属性,确保余额不能为负数。
  2. 实现一个 RangeInt 描述符,限制整数属性的取值范围。
  3. 创建一个 Product 类,使用 @property 实现折扣价自动计算。
  4. 使用描述符实现一个 NonEmpty 验证器,确保字符串属性不为空。
  5. 实现一个 CachedProperty 描述符,只在第一次访问时计算,后续直接返回缓存值。