Python教程36:属性(Property)与描述符

“细节决定成败。”

Python提供了强大的属性管理机制,让我们能够优雅地控制属性的访问。今天我们学习@property装饰器和描述符协议,掌握Python属性的高级用法。

1. 为什么需要Property

问题场景

直接访问属性缺乏控制:

 1class Person:
 2    def __init__(self, age):
 3        self.age = age  # 公开属性
 4
 5p = Person(25)
 6print(p.age)  # 25
 7
 8# 问题:可以设置非法值
 9p.age = -10   # 负数年龄?
10p.age = "abc"  # 字符串年龄?

传统解决方法:getter/setter

 1class Person:
 2    def __init__(self, age):
 3        self._age = age  # 私有属性
 4    
 5    def get_age(self):
 6        """获取年龄"""
 7        return self._age
 8    
 9    def set_age(self, value):
10        """设置年龄(带验证)"""
11        if not isinstance(value, int):
12            raise TypeError("年龄必须是整数")
13        if value < 0 or value > 150:
14            raise ValueError("年龄不合理")
15        self._age = value
16
17p = Person(25)
18print(p.get_age())  # 25
19p.set_age(30)       # OK
20# p.set_age(-10)    # ValueError

问题

  • 使用不方便:p.get_age()而不是p.age
  • 破坏现有代码:如果后来添加验证,需要修改所有调用

Python解决方案:@property

 1class Person:
 2    def __init__(self, age):
 3        self._age = age
 4    
 5    @property
 6    def age(self):
 7        """
 8        getter方法
 9        - 像访问属性一样调用:p.age
10        - 但实际调用的是方法
11        """
12        return self._age
13    
14    @age.setter
15    def age(self, value):
16        """
17        setter方法
18        - 像赋值一样调用:p.age = 30
19        - 可以添加验证逻辑
20        """
21        if not isinstance(value, int):
22            raise TypeError("年龄必须是整数")
23        if value < 0 or value > 150:
24            raise ValueError("年龄不合理")
25        self._age = value
26
27# 使用
28p = Person(25)
29print(p.age)    # 调用getter:25
30p.age = 30      # 调用setter:OK
31# p.age = -10   # ValueError

优势

  • 接口不变:像普通属性一样使用
  • 可控访问:添加验证逻辑
  • 向后兼容:后期添加property不破坏现有代码
  • Pythonic:符合Python风格

2. @property详解

基本用法

 1class Temperature:
 2    """温度类"""
 3    
 4    def __init__(self, celsius):
 5        self._celsius = celsius
 6    
 7    @property
 8    def celsius(self):
 9        """获取摄氏温度"""
10        return self._celsius
11    
12    @celsius.setter
13    def celsius(self, value):
14        """设置摄氏温度"""
15        if value < -273.15:
16            raise ValueError("温度不能低于绝对零度")
17        self._celsius = value
18    
19    @property
20    def fahrenheit(self):
21        """
22        计算华氏温度
23        - 只读属性(没有setter)
24        - 根据celsius动态计算
25        """
26        return self._celsius * 9/5 + 32
27    
28    @fahrenheit.setter
29    def fahrenheit(self, value):
30        """通过华氏温度设置"""
31        self._celsius = (value - 32) * 5/9
32
33# 使用
34t = Temperature(25)
35print(t.celsius)     # 25
36print(t.fahrenheit)  # 77.0
37
38t.celsius = 30   # 设置摄氏度
39print(t.fahrenheit)  # 86.0
40
41t.fahrenheit = 50    # 设置华氏度
42print(t.celsius)     # 10.0

只读属性

 1class Circle:
 2    """圆类"""
 3    
 4    def __init__(self, radius):
 5        self._radius = radius
 6    
 7    @property
 8    def radius(self):
 9        """半径(可读写)"""
10        return self._radius
11    
12    @radius.setter
13    def radius(self, value):
14        if value <= 0:
15            raise ValueError("半径必须大于0")
16        self._radius = value
17    
18    @property
19    def area(self):
20        """
21        面积(只读)
22        - 没有setter,不能设置
23        - 根据半径动态计算
24        """
25        import math
26        return math.pi * self._radius ** 2
27    
28    @property
29    def circumference(self):
30        """周长(只读)"""
31        import math
32        return 2 * math.pi * self._radius
33
34# 使用
35c = Circle(5)
36print(c.radius)        # 5
37print(c.area)          # 78.54
38print(c.circumference) # 31.42
39
40c.radius = 10  # OK
41# c.area = 100  # AttributeError: can't set attribute

deleter:删除属性

 1class Person:
 2    def __init__(self, name):
 3        self._name = name
 4    
 5    @property
 6    def name(self):
 7        return self._name
 8    
 9    @name.setter
10    def name(self, value):
11        if not value:
12            raise ValueError("名字不能为空")
13        self._name = value
14    
15    @name.deleter
16    def name(self):
17        """
18        删除属性
19        - del p.name调用
20        - 可以执行清理操作
21        """
22        print(f"删除名字:{self._name}")
23        del self._name
24
25# 使用
26p = Person("Alice")
27print(p.name)  # Alice
28del p.name     # 删除名字:Alice
29# print(p.name)  # AttributeError

3. property()函数

@property是装饰器语法糖,本质是property()函数:

 1class Person:
 2    def __init__(self, age):
 3        self._age = age
 4    
 5    def get_age(self):
 6        return self._age
 7    
 8    def set_age(self, value):
 9        if value < 0:
10            raise ValueError("年龄不能为负")
11        self._age = value
12    
13    def del_age(self):
14        del self._age
15    
16    # 使用property()函数
17    age = property(get_age, set_age, del_age, "年龄属性")
18    #              getter   setter   deleter  doc
19
20# 使用方式相同
21p = Person(25)
22print(p.age)  # 调用get_age
23p.age = 30    # 调用set_age

两种写法对比

 1# 方式1:装饰器(推荐)
 2@property
 3def age(self):
 4    return self._age
 5
 6@age.setter
 7def age(self, value):
 8    self._age = value
 9
10# 方式2:property()函数
11def get_age(self):
12    return self._age
13
14def set_age(self, value):
15    self._age = value
16
17age = property(get_age, set_age)

4. 描述符协议

描述符是property的底层机制:

什么是描述符

描述符(Descriptor)

  • 实现了描述符协议的对象
  • 控制属性访问的行为
  • property就是描述符的一种

描述符协议

 1class Descriptor:
 2    def __get__(self, instance, owner):
 3        """获取属性值"""
 4        pass
 5    
 6    def __set__(self, instance, value):
 7        """设置属性值"""
 8        pass
 9    
10    def __delete__(self, instance):
11        """删除属性"""
12        pass

自定义描述符

 1class PositiveNumber:
 2    """
 3    正数描述符
 4    - 确保属性值为正数
 5    - 可复用的验证逻辑
 6    """
 7    
 8    def __init__(self, name):
 9        self.name = name  # 属性名
10    
11    def __get__(self, instance, owner):
12        """
13        获取属性
14        - instance: 实例对象
15        - owner: 类对象
16        """
17        if instance is None:
18            return self
19        return instance.__dict__.get(self.name, 0)
20    
21    def __set__(self, instance, value):
22        """
23        设置属性
24        - instance: 实例对象
25        - value: 要设置的值
26        """
27        if not isinstance(value, (int, float)):
28            raise TypeError(f"{self.name}必须是数字")
29        if value <= 0:
30            raise ValueError(f"{self.name}必须大于0")
31        instance.__dict__[self.name] = value
32
33class Product:
34    """商品类"""
35    # 使用描述符
36    price = PositiveNumber("price")
37    weight = PositiveNumber("weight")
38    
39    def __init__(self, name, price, weight):
40        self.name = name
41        self.price = price    # 调用PositiveNumber.__set__
42        self.weight = weight
43
44# 使用
45p = Product("苹果", 5.5, 0.5)
46print(p.price)   # 5.5
47print(p.weight)  # 0.5
48
49# p.price = -10  # ValueError: price必须大于0
50# p.price = "abc"  # TypeError: price必须是数字

数据描述符 vs 非数据描述符

 1class DataDescriptor:
 2    """
 3    数据描述符
 4    - 同时定义__get__和__set__
 5    - 优先级高于实例字典
 6    """
 7    def __get__(self, instance, owner):
 8        return "data descriptor"
 9    
10    def __set__(self, instance, value):
11        print(f"Setting: {value}")
12
13class NonDataDescriptor:
14    """
15    非数据描述符
16    - 只定义__get__
17    - 优先级低于实例字典
18    """
19    def __get__(self, instance, owner):
20        return "non-data descriptor"
21
22class MyClass:
23    data_desc = DataDescriptor()
24    non_data_desc = NonDataDescriptor()
25
26obj = MyClass()
27
28# 数据描述符优先
29print(obj.data_desc)  # data descriptor
30obj.data_desc = "new"  # Setting: new(调用__set__)
31
32# 非数据描述符可被实例属性覆盖
33print(obj.non_data_desc)  # non-data descriptor
34obj.non_data_desc = "new"  # 直接设置实例属性
35print(obj.non_data_desc)  # new(实例属性优先)

5. 实战示例

示例1:字段验证

 1class Validator:
 2    """通用验证描述符"""
 3    
 4    def __init__(self, min_value=None, max_value=None):
 5        self.min_value = min_value
 6        self.max_value = max_value
 7    
 8    def __set_name__(self, owner, name):
 9        """
10        Python 3.6+特性
11        - 自动获取属性名
12        - 不需要手动传name
13        """
14        self.name = name
15    
16    def __get__(self, instance, owner):
17        if instance is None:
18            return self
19        return instance.__dict__.get(self.name)
20    
21    def __set__(self, instance, value):
22        if self.min_value is not None and value < self.min_value:
23            raise ValueError(f"{self.name}不能小于{self.min_value}")
24        if self.max_value is not None and value > self.max_value:
25            raise ValueError(f"{self.name}不能大于{self.max_value}")
26        instance.__dict__[self.name] = value
27
28class Student:
29    """学生类"""
30    age = Validator(min_value=0, max_value=150)
31    score = Validator(min_value=0, max_value=100)
32    
33    def __init__(self, name, age, score):
34        self.name = name
35        self.age = age
36        self.score = score
37
38# 使用
39s = Student("Alice", 20, 85)
40print(s.age, s.score)  # 20 85
41
42# s.age = -10   # ValueError
43# s.score = 101  # ValueError

示例2:懒加载属性

 1class LazyProperty:
 2    """
 3    懒加载描述符
 4    - 第一次访问时计算
 5    - 之后直接返回缓存值
 6    """
 7    
 8    def __init__(self, function):
 9        self.function = function
10        self.name = function.__name__
11    
12    def __get__(self, instance, owner):
13        if instance is None:
14            return self
15        
16        # 计算并缓存
17        value = self.function(instance)
18        setattr(instance, self.name, value)
19        return value
20
21class DataSet:
22    """数据集类"""
23    
24    def __init__(self, data):
25        self.data = data
26    
27    @LazyProperty
28    def mean(self):
29        """
30        平均值(懒加载)
31        - 第一次访问时计算
32        - 结果被缓存
33        """
34        print("计算平均值...")
35        return sum(self.data) / len(self.data)
36    
37    @LazyProperty
38    def variance(self):
39        """方差(懒加载)"""
40        print("计算方差...")
41        mean = self.mean
42        return sum((x - mean) ** 2 for x in self.data) / len(self.data)
43
44# 使用
45ds = DataSet([1, 2, 3, 4, 5])
46
47print(ds.mean)  # 计算平均值... 3.0
48print(ds.mean)  # 3.0(直接返回缓存,不再计算)
49
50print(ds.variance)  # 计算方差... 2.0
51print(ds.variance)  # 2.0(缓存)

示例3:类型检查

 1class TypedProperty:
 2    """类型检查描述符"""
 3    
 4    def __init__(self, expected_type):
 5        self.expected_type = expected_type
 6    
 7    def __set_name__(self, owner, name):
 8        self.name = name
 9    
10    def __get__(self, instance, owner):
11        if instance is None:
12            return self
13        return instance.__dict__.get(self.name)
14    
15    def __set__(self, instance, value):
16        if not isinstance(value, self.expected_type):
17            raise TypeError(
18                f"{self.name}必须是{self.expected_type.__name__}类型"
19            )
20        instance.__dict__[self.name] = value
21
22class Person:
23    """使用类型检查的Person类"""
24    name = TypedProperty(str)
25    age = TypedProperty(int)
26    salary = TypedProperty(float)
27    
28    def __init__(self, name, age, salary):
29        self.name = name
30        self.age = age
31        self.salary = salary
32
33# 使用
34p = Person("Alice", 25, 50000.0)
35
36# p.age = "25"  # TypeError: age必须是int类型
37# p.salary = 50000  # TypeError: salary必须是float类型

6. 最佳实践

1. 何时使用Property

 1# 使用property的场景:
 2# - 需要验证输入
 3# - 计算派生值
 4# - 延迟加载
 5# - 保持接口向后兼容
 6
 7class Rectangle:
 8    def __init__(self, width, height):
 9        self._width = width
10        self._height = height
11    
12    @property
13    def area(self):
14        """计算派生值"""
15        return self._width * self._height
16    
17    @property
18    def width(self):
19        return self._width
20    
21    @width.setter
22    def width(self, value):
23        """验证输入"""
24        if value <= 0:
25            raise ValueError("宽度必须大于0")
26        self._width = value

2. 命名约定

1# 私有属性用单下划线
2class Example:
3    @property
4    def value(self):
5        return self._value  # _value是私有的
6    
7    @value.setter
8    def value(self, v):
9        self._value = v

3. 文档字符串

 1class Person:
 2    @property
 3    def age(self):
 4        """
 5        年龄属性
 6        
 7        Returns:
 8            int: 年龄值(0-150)
 9        """
10        return self._age

7. 小结

今天我们学习了属性和描述符:

  • @property:优雅的属性访问控制
  • getter/setter/deleter:属性的三个方法
  • 只读属性:只提供getter
  • 描述符协议__get____set____delete__
  • 自定义描述符:可复用的属性逻辑
  • 实战应用:验证、懒加载、类型检查

Property和描述符是Python高级特性,让属性管理更优雅、更Pythonic。


练习题

  1. 创建一个Email类,用property验证邮箱格式
  2. 实现一个cached_property描述符(类似LazyProperty)
  3. 创建一个RangeValidator描述符,验证数值范围

思考题

property和直接访问属性相比,性能如何?何时应该优先使用property?


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读