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 的优势

  1. 封装性:隐藏内部实现细节
  2. 验证:在设置值时进行数据验证
  3. 计算属性:动态计算属性值
  4. 向后兼容:可以将直接属性访问改为方法调用,而不影响外部代码
  5. 代码可读性:使用属性访问语法比方法调用更自然

二、学生信息验证完整案例

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 机制的核心价值

  1. 数据封装:隐藏内部实现,提供统一接口
  2. 输入验证:在赋值时自动验证数据有效性
  3. 代码优雅:使用属性访问语法,代码更自然
  4. 易于维护:验证逻辑集中在 setter 中
  5. 向后兼容:可以无缝替换直接属性访问

5.2 Builder 模式 + Property 的优势

  1. 职责分离:Builder 负责构建,Property 负责验证
  2. 可复用性:验证逻辑可以在多处使用
  3. 错误定位:能精确指出哪个字段验证失败
  4. 灵活扩展:易于添加新字段和验证规则

5.3 适用场景

  • 表单数据验证
  • API 请求参数验证
  • 数据库模型验证
  • 配置文件解析
  • 数据传输对象(DTO)

通过 Property 机制,我们可以构建健壮、易维护的数据验证系统,确保数据在进入业务逻辑之前已经过严格验证。

posted on 2026-02-10 14:37  江湖乄夜雨  阅读(26)  评论(0)    收藏  举报