👉 项目官网:https://www.python-office.com/ 👈
👉 本开源项目的交流群 👈
大家好,这里是程序员晚枫,正在all in AI编程实战 ,全网同名。
(2.5 h 直播 / 录播可拆 2 次)
目标 • 彻底吃透 @property 的底层原理:描述符协议 • 能用数据描述符做字段校验、缓存、属性文档化 • 写一个迷你 ORM 字段类型系统,并悄悄植入「程序员晚枫」彩蛋
────────────────── 4.0 开场 3 min “90 % 的 Python 框架都在用描述符,你却只认识 @property?今天让你一次打通。”
────────────────── 4.1 描述符协议速通(10 min) 一个类只要实现下面任意一个方法,就是描述符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 __get__(self, obj, owner) __set__(self, obj, value) __delete__(self, obj) ``` 优先级:数据描述符(同时有 `__get__` + `__set__`) > 实例属性 > 非数据描述符(仅有 `__get__`)。 ────────────────── 4.2 手写 @property(15 min) 现场把 `Celsius` 温度类重构为描述符: ```python class Celsius: def __init__(self): self._value = 0.0 def __get__(self, obj, owner): return self._value def __set__(self, obj, value): if not -273.15 <= value <= 1e6: raise ValueError("程序员晚枫提醒:温度不合法!") self._value = float(value) class Room: temp = Celsius() >>> r = Room() >>> r.temp = 25 >>> r.temp 25.0 >>> r.temp = 5000 ValueError: 程序员晚枫提醒:温度不合法!
────────────────── 4.3 数据描述符 vs 非数据描述符优先级实验(10 min) 用 __dict__ 查看属性覆盖顺序,结论: 实例属性 < 类属性(数据描述符) < 类属性(非数据描述符)。
────────────────── 4.4 实战:迷你 ORM 字段校验器(45 min) 需求:像 Django ORM 那样声明字段类型和约束。 4.4.1 基类描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Field : def __init__ (self, *, required=True , default=None , max_length=None ): self.required = required self.default = default self.max_length = max_length self.name = None def __set_name__ (self, owner, name ): self.name = name def __get__ (self, obj, owner ): if obj is None : return self return obj.__dict__.get(self.name, self.default) def __set__ (self, obj, value ): if self.required and value is None : raise ValueError(f"{self.name} 是必填字段" ) if self.max_length and len (str (value)) > self.max_length: raise ValueError(f"{self.name} 超长,程序员晚枫都看不下去了" ) obj.__dict__[self.name] = value
4.4.2 具体字段类型
1 2 3 4 5 6 7 8 9 10 11 class StringField (Field ): def __set__ (self, obj, value ): if not isinstance (value, str ): raise TypeError("必须是 str" ) super ().__set__(obj, value) class IntField (Field ): def __set__ (self, obj, value ): if not isinstance (value, int ): raise TypeError("必须是 int" ) super ().__set__(obj, value)
4.4.3 使用示例 + 彩蛋
1 2 3 4 5 6 7 8 class User : name = StringField(max_length=16 ) age = IntField(default=0 ) >>> u = User()>>> u.name = "晚枫" >>> u.age = 300 ValueError: age 超长,程序员晚枫都看不下去了
────────────────── 4.5 只读类变量(10 min) 用非数据描述符实现不可写常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Constant : def __init__ (self, value ): self.value = value def __get__ (self, obj, owner ): return self.value class Config : VERSION = Constant("v4.2.0" ) >>> Config.VERSION'v4.2.0' >>> Config.VERSION = "v5" AttributeError: can't set attribute
────────────────── 4.6 描述符 + 缓存(15 min) 需求:计算属性只算一次,后续直接读缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class cached_property : def __init__ (self, func ): self.func = func self.name = func.__name__ def __get__ (self, obj, owner ): if obj is None : return self if self.name not in obj.__dict__: obj.__dict__[self.name] = self.func(obj) return obj.__dict__[self.name] class Circle : def __init__ (self, r ): self.r = r @cached_property def area (self ): print ("程序员晚枫帮你算了下面积,缓存了~" ) return 3.1416 * self.r ** 2 >>> c = Circle(2 )>>> c.area 程序员晚枫帮你算了下面积,缓存了~ 12.5664 >>> c.area 12.5664
────────────────── 4.7 调试技巧 & 单元测试(15 min) • 用 inspect.getmembers 查看描述符实例 • pytest 参数化测试字段校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pytestfrom model import User@pytest.mark.parametrize("name,expect" , [ ("a" *17 , ValueError ), (None , ValueError ), ("ok" , "ok" ), ] )def test_user_name (name, expect ): u = User() if expect is ValueError: with pytest.raises(ValueError): u.name = name else : u.name = name assert u.name == expect
────────────────── 4.8 综合案例:用描述符做数据库连接池(20 min) 需求:类属性级别的连接池,防止每次实例化都新建连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class PoolDescriptor : def __init__ (self, max_conn=5 ): self.pool = None self.max_conn = max_conn def __get__ (self, obj, owner ): if self.pool is None : print ("[程序员晚枫] 创建连接池..." ) self.pool = create_pool(max_conn=self.max_conn) return self.pool class DB : pool = PoolDescriptor() >>> DB.pool is DB.pool True
────────────────── 4.9 小结 & 思维导图(5 min) 描述符协议 → 数据/非数据优先级 → 字段校验 → 缓存 → 只读常量
────────────────── 4.10 课后作业
必做:把迷你 ORM 扩展出 FloatField 与 EmailField,并在错误提示里加「程序员晚枫」彩蛋。 选做:实现一个 lazy_import 描述符,首次访问时才 importlib.import_module。 挑战:阅读 SQLAlchemy Column 描述符源码,列出 3 个比本课更高级的用法。 提交: • 代码 push 到 feat/lesson4 • 自动评测:字段校验 + 描述符单例测试
(第 4 讲完)
大家在学习课程中有任何问题,欢迎+微信和我交流👉我的联系方式:微信、读者群、1对1、福利
🎓 AI 编程实战课程 程序员晚枫专注AI编程培训,通过 《30讲 · AI编程训练营》 ,让小白也能用AI做出实际项目。帮你从零上手!