👉 项目官网:https://www.python-office.com/ 👈

👉 本开源项目的交流群 👈

github star gitee star atomgit star

AI编程 AI交流群

大家好,这里是程序员晚枫,正在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
# 没有 __set__ → 只读

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 pytest
from 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 课后作业

  1. 必做:把迷你 ORM 扩展出 FloatFieldEmailField,并在错误提示里加「程序员晚枫」彩蛋。
  2. 选做:实现一个 lazy_import 描述符,首次访问时才 importlib.import_module
  3. 挑战:阅读 SQLAlchemy Column 描述符源码,列出 3 个比本课更高级的用法。

提交:
• 代码 push 到 feat/lesson4
• 自动评测:字段校验 + 描述符单例测试

(第 4 讲完)


大家在学习课程中有任何问题,欢迎+微信和我交流👉我的联系方式:微信、读者群、1对1、福利

扫一扫,领红包

美团红包

程序员晚枫专注AI编程培训,小白看完他和图灵社区合作的教程《30讲 · AI编程训练营》就能上手做AI项目。

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!