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。
练习题:
- 创建一个
Email类,用property验证邮箱格式 - 实现一个
cached_property描述符(类似LazyProperty) - 创建一个
RangeValidator描述符,验证数值范围
思考题:
property和直接访问属性相比,性能如何?何时应该优先使用property?
本文代码示例:
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
