Python的property机制及在Builder验证场景中的使用
Python 的 property 机制及在 Builder 验证场景中的使用
一、Python Property 机制详解
1.1 什么是 Property?
Property 是 Python 中的一个内置装饰器,它允许我们将方法调用伪装成属性访问。通过 property,我们可以在获取或设置属性值时执行额外的逻辑(如验证、计算、日志记录等)。
1.2 基本语法
class Person:
def __init__(self):
self._age = 0
@property
def age(self):
"""getter 方法"""
return self._age
@age.setter
def age(self, value):
"""setter 方法"""
if value < 0:
raise ValueError("年龄不能为负数")
self._age = value
1.3 简单示例
class Temperature:
def __init__(self):
self._celsius = 0
@property
def celsius(self):
"""获取摄氏温度"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""设置摄氏温度,带验证"""
if value < -273.15:
raise ValueError("温度不能低于绝对零度 (-273.15°C)")
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
# 使用示例
temp = Temperature()
temp.celsius = 25 # 使用 setter
print(f"摄氏温度: {temp.celsius}°C") # 使用 getter
print(f"华氏温度: {temp.fahrenheit}°F")
temp.fahrenheit = 100
print(f"摄氏温度: {temp.celsius}°C")
# 尝试设置无效值
try:
temp.celsius = -300 # 会抛出异常
except ValueError as e:
print(f"错误: {e}")
输出:
摄氏温度: 25°C
华氏温度: 77.0°F
摄氏温度: 37.77777777777778°C
错误: 温度不能低于绝对零度 (-273.15°C)
1.4 Property 的优势
- 封装性:隐藏内部实现细节
- 验证:在设置值时进行数据验证
- 计算属性:动态计算属性值
- 向后兼容:可以将直接属性访问改为方法调用,而不影响外部代码
- 代码可读性:使用属性访问语法比方法调用更自然
二、学生信息验证完整案例
2.1 需求分析
创建一个学生信息验证系统,要求:
- 姓名:不能为空,最多 10 个字符
- 学号:不能为空,必须在 100 到 10000 之间
- 年龄:不能为空,必须在 20 到 60 之间
- 分数:不能为空,必须在 0 到 100 之间
- 班级名称:不能为空,最多 10 个字符
2.2 完整实现代码
from dataclasses import dataclass
from typing import Optional
class ValidationError(Exception):
"""自定义验证异常"""
pass
@dataclass
class StudentInfoBuilder:
"""学生信息构建器,使用 property 进行字段级验证"""
# 私有属性,存储实际值
_name: str = ''
_student_id: int = 0
_age: int = 0
_score: float = 0.0
_class_name: str = ''
# 定义需要验证的属性列表
property_list = ['name', 'student_id', 'age', 'score', 'class_name']
# ==================== 姓名验证 ====================
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str):
if not value:
raise ValidationError("姓名不能为空")
if not isinstance(value, str):
raise ValidationError("姓名必须是字符串类型")
if len(value) > 10:
raise ValidationError(f"姓名不能超过10个字符,当前长度: {len(value)}")
self._name = value.strip()
# ==================== 学号验证 ====================
@property
def student_id(self) -> int:
return self._student_id
@student_id.setter
def student_id(self, value):
if value is None:
raise ValidationError("学号不能为空")
try:
value = int(value)
except (ValueError, TypeError):
raise ValidationError("学号必须是数字")
if value <= 100:
raise ValidationError(f"学号必须大于100,当前值: {value}")
if value >= 10000:
raise ValidationError(f"学号必须小于10000,当前值: {value}")
self._student_id = value
# ==================== 年龄验证 ====================
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, value):
if value is None:
raise ValidationError("年龄不能为空")
try:
value = int(value)
except (ValueError, TypeError):
raise ValidationError("年龄必须是数字")
if value <= 20:
raise ValidationError(f"年龄必须大于20岁,当前值: {value}")
if value >= 60:
raise ValidationError(f"年龄必须小于60岁,当前值: {value}")
self._age = value
# ==================== 分数验证 ====================
@property
def score(self) -> float:
return self._score
@score.setter
def score(self, value):
if value is None:
raise ValidationError("分数不能为空")
try:
value = float(value)
except (ValueError, TypeError):
raise ValidationError("分数必须是数字")
if value < 0:
raise ValidationError(f"分数不能小于0,当前值: {value}")
if value > 100:
raise ValidationError(f"分数不能大于100,当前值: {value}")
self._score = value
# ==================== 班级名称验证 ====================
@property
def class_name(self) -> str:
return self._class_name
@class_name.setter
def class_name(self, value: str):
if not value:
raise ValidationError("班级名称不能为空")
if not isinstance(value, str):
raise ValidationError("班级名称必须是字符串类型")
if len(value) > 10:
raise ValidationError(f"班级名称不能超过10个字符,当前长度: {len(value)}")
self._class_name = value.strip()
# ==================== 构建方法 ====================
def build(self, data: dict) -> dict:
"""
验证并构建学生信息
Args:
data: 包含学生信息的字典
Returns:
验证后的学生信息字典
Raises:
ValidationError: 当验证失败时
"""
# 第一步:遍历所有属性,触发 setter 进行验证
for key in self.property_list:
value = data.get(key)
try:
setattr(self, key, value)
except ValidationError as e:
raise ValidationError(f"字段 '{key}' 验证失败: {str(e)}")
# 第二步:确保所有必填字段都已设置
try:
assert self._name, "姓名未设置"
assert self._student_id > 0, "学号未设置"
assert self._age > 0, "年龄未设置"
assert self._score >= 0, "分数未设置"
assert self._class_name, "班级名称未设置"
except AssertionError as e:
raise ValidationError(f"学生信息不完整: {str(e)}")
# 第三步:返回验证后的数据
return {
'name': self.name,
'student_id': self.student_id,
'age': self.age,
'score': self.score,
'class_name': self.class_name
}
def __repr__(self):
return (f"StudentInfoBuilder(name='{self.name}', "
f"student_id={self.student_id}, "
f"age={self.age}, "
f"score={self.score}, "
f"class_name='{self.class_name}')")
# ==================== 学生服务类 ====================
class StudentService:
"""学生信息服务类,模拟 CustomBaseInfoService"""
def __init__(self):
self.students = [] # 模拟数据库
def create_student(self, student_data: dict) -> dict:
"""
创建学生信息(类似 base_info_collect 方法)
Args:
student_data: 学生信息字典
Returns:
创建成功的学生信息
"""
print(f"\n开始创建学生信息: {student_data}")
# 使用 Builder 验证数据
try:
validated_data = StudentInfoBuilder().build(data=student_data)
print(f"✓ 数据验证通过")
except ValidationError as e:
print(f"✗ 数据验证失败: {e}")
raise
# 模拟保存到数据库
student_record = {
'id': len(self.students) + 1,
**validated_data
}
self.students.append(student_record)
print(f"✓ 学生信息已保存,ID: {student_record['id']}")
return student_record
def get_all_students(self):
"""获取所有学生"""
return self.students
# ==================== 使用示例 ====================
def main():
service = StudentService()
print("=" * 60)
print("学生信息验证系统演示")
print("=" * 60)
# 测试用例 1: 正确的数据
print("\n【测试 1】正确的学生信息")
student1 = {
'name': '张三',
'student_id': 1001,
'age': 25,
'score': 95.5,
'class_name': '计算机1班'
}
try:
result = service.create_student(student1)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 2: 姓名过长
print("\n【测试 2】姓名超过10个字符")
student2 = {
'name': '这是一个非常非常长的名字超过十个字',
'student_id': 1002,
'age': 30,
'score': 88.0,
'class_name': '软件2班'
}
try:
result = service.create_student(student2)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 3: 学号超出范围
print("\n【测试 3】学号超出范围")
student3 = {
'name': '李四',
'student_id': 50, # 小于100
'age': 28,
'score': 92.0,
'class_name': '数学3班'
}
try:
result = service.create_student(student3)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 4: 年龄不在范围内
print("\n【测试 4】年龄超出范围")
student4 = {
'name': '王五',
'student_id': 2001,
'age': 18, # 小于20
'score': 85.0,
'class_name': '物理4班'
}
try:
result = service.create_student(student4)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 5: 分数超出范围
print("\n【测试 5】分数超出范围")
student5 = {
'name': '赵六',
'student_id': 3001,
'age': 35,
'score': 105.0, # 大于100
'class_name': '化学5班'
}
try:
result = service.create_student(student5)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 6: 缺少必填字段
print("\n【测试 6】缺少必填字段")
student6 = {
'name': '孙七',
'student_id': 4001,
'age': 40,
# 缺少 score
'class_name': '生物6班'
}
try:
result = service.create_student(student6)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 测试用例 7: 另一个正确的数据
print("\n【测试 7】另一个正确的学生信息")
student7 = {
'name': '周八',
'student_id': 5001,
'age': 45,
'score': 78.5,
'class_name': '英语7班'
}
try:
result = service.create_student(student7)
print(f"创建成功: {result}")
except ValidationError as e:
print(f"创建失败: {e}")
# 显示所有成功创建的学生
print("\n" + "=" * 60)
print("所有成功创建的学生信息:")
print("=" * 60)
for student in service.get_all_students():
print(f"ID: {student['id']}, "
f"姓名: {student['name']}, "
f"学号: {student['student_id']}, "
f"年龄: {student['age']}, "
f"分数: {student['score']}, "
f"班级: {student['class_name']}")
if __name__ == "__main__":
main()
2.3 运行结果
============================================================
学生信息验证系统演示
============================================================
【测试 1】正确的学生信息
开始创建学生信息: {'name': '张三', 'student_id': 1001, 'age': 25, 'score': 95.5, 'class_name': '计算机1班'}
✓ 数据验证通过
✓ 学生信息已保存,ID: 1
创建成功: {'id': 1, 'name': '张三', 'student_id': 1001, 'age': 25, 'score': 95.5, 'class_name': '计算机1班'}
【测试 2】姓名超过10个字符
开始创建学生信息: {'name': '这是一个非常非常长的名字超过十个字', 'student_id': 1002, 'age': 30, 'score': 88.0, 'class_name': '软件2班'}
✗ 数据验证失败: 字段 'name' 验证失败: 姓名不能超过10个字符,当前长度: 17
创建失败: 字段 'name' 验证失败: 姓名不能超过10个字符,当前长度: 17
【测试 3】学号超出范围
开始创建学生信息: {'name': '李四', 'student_id': 50, 'age': 28, 'score': 92.0, 'class_name': '数学3班'}
✗ 数据验证失败: 字段 'student_id' 验证失败: 学号必须大于100,当前值: 50
创建失败: 字段 'student_id' 验证失败: 学号必须大于100,当前值: 50
【测试 4】年龄超出范围
开始创建学生信息: {'name': '王五', 'student_id': 2001, 'age': 18, 'score': 85.0, 'class_name': '物理4班'}
✗ 数据验证失败: 字段 'age' 验证失败: 年龄必须大于20岁,当前值: 18
创建失败: 字段 'age' 验证失败: 年龄必须大于20岁,当前值: 18
【测试 5】分数超出范围
开始创建学生信息: {'name': '赵六', 'student_id': 3001, 'age': 35, 'score': 105.0, 'class_name': '化学5班'}
✗ 数据验证失败: 字段 'score' 验证失败: 分数不能大于100,当前值: 105.0
创建失败: 字段 'score' 验证失败: 分数不能大于100,当前值: 105.0
【测试 6】缺少必填字段
开始创建学生信息: {'name': '孙七', 'student_id': 4001, 'age': 40, 'class_name': '生物6班'}
✗ 数据验证失败: 字段 'score' 验证失败: 分数不能为空
创建失败: 字段 'score' 验证失败: 分数不能为空
【测试 7】另一个正确的学生信息
开始创建学生信息: {'name': '周八', 'student_id': 5001, 'age': 45, 'score': 78.5, 'class_name': '英语7班'}
✓ 数据验证通过
✓ 学生信息已保存,ID: 2
创建成功: {'id': 2, 'name': '周八', 'student_id': 5001, 'age': 45, 'score': 78.5, 'class_name': '英语7班'}
============================================================
所有成功创建的学生信息:
============================================================
ID: 1, 姓名: 张三, 学号: 1001, 年龄: 25, 分数: 95.5, 班级: 计算机1班
ID: 2, 姓名: 周八, 学号: 5001, 年龄: 45, 分数: 78.5, 班级: 英语7班
三、与 BaseInfoBuilder 的对比
3.1 相似之处
| 特性 | BaseInfoBuilder | StudentInfoBuilder |
|---|---|---|
| 使用 @dataclass | ✓ | ✓ |
| 使用 @property 验证 | ✓ | ✓ |
| 私有属性存储 | ✓ | ✓ |
| property_list 定义 | ✓ | ✓ |
| build() 方法 | ✓ | ✓ |
| 自定义异常 | DefaultException | ValidationError |
3.2 验证流程对比
# BaseInfoBuilder 的验证流程
def build(self, data):
# 1. 设置属性(触发验证)
for k in self.property_list:
v = data.get(k)
setattr(self, k, v)
# 2. 断言必填字段
try:
assert self._email
assert self._income
# ...
except AssertionError:
raise DefaultException(sc.E_PARAM_NO_EMAIL, "基本信息不完整")
# 3. 返回字典
return {...}
# StudentInfoBuilder 的验证流程(相同模式)
def build(self, data):
# 1. 设置属性(触发验证)
for key in self.property_list:
value = data.get(key)
setattr(self, key, value)
# 2. 断言必填字段
try:
assert self._name
assert self._student_id > 0
# ...
except AssertionError as e:
raise ValidationError(f"学生信息不完整: {str(e)}")
# 3. 返回字典
return {...}
四、Property 机制的最佳实践
4.1 命名约定
class Example:
def __init__(self):
self._value = 0 # 私有属性用下划线前缀
@property
def value(self): # 公开属性不用下划线
return self._value
@value.setter
def value(self, val):
self._value = val
4.2 验证模式
@property
def field(self):
return self._field
@field.setter
def field(self, value):
# 1. 空值检查
if not value:
raise ValueError("字段不能为空")
# 2. 类型检查
if not isinstance(value, str):
raise TypeError("字段必须是字符串")
# 3. 格式验证
if not re.match(pattern, value):
raise ValueError("字段格式不正确")
# 4. 范围验证
if len(value) > 10:
raise ValueError("字段长度不能超过10")
# 5. 赋值
self._field = value
4.3 只读属性
class Student:
def __init__(self, student_id):
self._student_id = student_id
@property
def student_id(self):
"""只读属性,不提供 setter"""
return self._student_id
4.4 计算属性
class Student:
def __init__(self, score):
self._score = score
@property
def grade(self):
"""根据分数计算等级"""
if self._score >= 90:
return 'A'
elif self._score >= 80:
return 'B'
elif self._score >= 70:
return 'C'
elif self._score >= 60:
return 'D'
else:
return 'F'
五、总结
5.1 Property 机制的核心价值
- 数据封装:隐藏内部实现,提供统一接口
- 输入验证:在赋值时自动验证数据有效性
- 代码优雅:使用属性访问语法,代码更自然
- 易于维护:验证逻辑集中在 setter 中
- 向后兼容:可以无缝替换直接属性访问
5.2 Builder 模式 + Property 的优势
- 职责分离:Builder 负责构建,Property 负责验证
- 可复用性:验证逻辑可以在多处使用
- 错误定位:能精确指出哪个字段验证失败
- 灵活扩展:易于添加新字段和验证规则
5.3 适用场景
- 表单数据验证
- API 请求参数验证
- 数据库模型验证
- 配置文件解析
- 数据传输对象(DTO)
通过 Property 机制,我们可以构建健壮、易维护的数据验证系统,确保数据在进入业务逻辑之前已经过严格验证。
浙公网安备 33010602011771号