难度等级: 高级
适合读者: 有 Python 基础的开发者,准备面试的中高级工程师
前置知识: 第 03 篇《面向对象编程进阶》、第 05 篇《Python 数据模型与标准库精选》
导读
你一定写过 @property,但你是否知道它本质上就是一个描述符(Descriptor) ?你是否想过,当你写 obj.attr 时,Python 到底经历了怎样的查找过程?为什么 classmethod、staticmethod、property 这些"装饰器"能改变属性的访问行为?
在 Python 中,属性访问是一个远比表面复杂的过程。它涉及三个层次的机制:
__getattribute__:每次属性访问的入口,无论属性是否存在都会被调用- 描述符协议 :定义了
__get__、__set__、__delete__的对象可以"劫持"属性访问 __getattr__:只在常规查找失败时才被调用的"后备方案"
这三者的优先级和交互关系,构成了 Python 属性访问的完整图景。理解它们,你就能理解 Django ORM 的 Field 是如何工作的、SQLAlchemy 的 Column 为什么能自动映射数据库字段、以及 functools.cached_property 的线程安全陷阱。
本文将从 CPython 源码层面剖析属性访问的完整链路,系统讲解描述符协议,并通过 ORM 字段、参数验证、延迟加载等实战案例展示描述符的强大能力。
学习目标
读完本文后,你将能够:
- 完整描述 Python 属性访问的查找链路,包括
__getattribute__、描述符、实例字典的优先级关系 - 区分数据描述符和非数据描述符,理解它们在属性查找中的不同优先级
- 实现自定义描述符,包括类型检查描述符、延迟计算描述符和缓存属性描述符
- 深入理解
@property的底层实现,能用纯 Python 复现其行为 - 掌握
functools.cached_property的使用场景与线程安全注意事项 - 在面试中准确回答描述符协议、属性访问机制等高频问题
- 能实现类 ORM 框架的字段定义、参数验证等生产级描述符应用
一、Python 属性访问机制
1.1 属性访问的完整链路
当你写 obj.attr 时,Python 的属性查找远非"从对象的字典里取值"这么简单。CPython 中 object.__getattribute__ 的查找逻辑(简化版)如下:
python
# 伪代码:object.__getattribute__ 的查找逻辑
def __getattribute__(obj, name):
# Step 1: 从类(及其 MRO)中查找同名属性
cls = type(obj)
cls_attr = None
for base in cls.__mro__:
if name in base.__dict__:
cls_attr = base.__dict__[name]
break
# Step 2: 如果类属性是数据描述符(定义了 __get__ + __set__ 或 __delete__),优先调用
if cls_attr is not None and hasattr(type(cls_attr), '__set__') or hasattr(type(cls_attr), '__delete__'):
# 数据描述符优先于实例字典
if hasattr(type(cls_attr), '__get__'):
return type(cls_attr).__get__(cls_attr, obj, cls)
# Step 3: 查找实例字典
if hasattr(obj, '__dict__') and name in obj.__dict__:
return obj.__dict__[name]
# Step 4: 如果类属性是非数据描述符(只定义了 __get__),调用它
if cls_attr is not None:
if hasattr(type(cls_attr), '__get__'):
return type(cls_attr).__get__(cls_attr, obj, cls)
return cls_attr # 普通类属性
# Step 5: 都没找到,触发 __getattr__(如果定义了的话)
raise AttributeError(f"'{cls.__name__}' object has no attribute '{name}'")
关键优先级(这是面试的核心考点):
数据描述符 > 实例字典 > 非数据描述符 > 普通类属性 > __getattr__
我们用代码来验证这个优先级:
python
class DataDescriptor:
"""数据描述符:定义了 __get__ 和 __set__"""
def __get__(self, obj, objtype=None):
if obj is None:
return self
return "data_descriptor_value"
def __set__(self, obj, value):
pass # 只要定义了 __set__,就是数据描述符
class NonDataDescriptor:
"""非数据描述符:只定义了 __get__"""
def __get__(self, obj, objtype=None):
if obj is None:
return self
return "non_data_descriptor_value"
class MyClass:
data_attr = DataDescriptor()
non_data_attr = NonDataDescriptor()
obj = MyClass()
# 数据描述符 vs 实例字典
obj.__dict__["data_attr"] = "instance_value"
print(obj.data_attr) # data_descriptor_value -- 数据描述符胜出!
print(obj.__dict__["data_attr"]) # instance_value -- 实例字典中的值仍然存在
# 非数据描述符 vs 实例字典
obj.__dict__["non_data_attr"] = "instance_value"
print(obj.non_data_attr) # instance_value -- 实例字典胜出!
这就是核心区别 :数据描述符的优先级高于 实例字典,而非数据描述符的优先级低于 实例字典。这个设计让 property(数据描述符)能够拦截所有属性访问,同时让 functools.cached_property(非数据描述符)能够在首次计算后将值写入实例字典,后续直接从实例字典读取。
1.2 __getattr__ vs __getattribute__ vs __get__
这三个方法名字相似,但角色完全不同:
python
class TrackedAccess:
"""演示 __getattribute__ 和 __getattr__ 的调用时机"""
def __init__(self):
self.existing_attr = "I exist"
def __getattribute__(self, name):
"""每次属性访问都会调用,无论属性是否存在"""
print(f" __getattribute__ called for '{name}'")
return super().__getattribute__(name)
def __getattr__(self, name):
"""只在常规查找失败时调用(__getattribute__ 抛出 AttributeError 后)"""
print(f" __getattr__ called for '{name}'")
return f"default_value_for_{name}"
obj = TrackedAccess()
# 访问存在的属性
print("--- 访问 existing_attr ---")
print(obj.existing_attr)
# __getattribute__ called for 'existing_attr'
# I exist
# 访问不存在的属性
print("\n--- 访问 missing_attr ---")
print(obj.missing_attr)
# __getattribute__ called for 'missing_attr'
# __getattr__ called for 'missing_attr'
# default_value_for_missing_attr
三者的角色对比:
| 方法 | 定义在 | 调用时机 | 典型用途 |
|---|---|---|---|
__getattribute__ |
实例的类 | 每次 obj.attr 都调用 |
拦截所有属性访问(慎用,易递归) |
__getattr__ |
实例的类 | 仅在常规查找失败时 | 动态属性、代理模式、属性默认值 |
__get__ |
描述符类 | 通过描述符协议触发 | 控制属性的获取行为 |
常见陷阱 -- __getattribute__ 中的无限递归:
python
class BadExample:
def __init__(self):
self.data = {"key": "value"}
def __getattribute__(self, name):
# 危险!self.data 又触发 __getattribute__,导致无限递归
# if name in self.data: # RecursionError!
# return self.data[name]
# 正确做法:用 super().__getattribute__ 或 object.__getattribute__
return object.__getattribute__(self, name)
# __getattr__ 的实际应用:动态代理
class APIClient:
"""动态代理模式:将方法调用转化为 API 请求"""
def __init__(self, base_url: str):
self._base_url = base_url
self._calls: list = []
def __getattr__(self, name: str):
"""未定义的属性访问转化为 API 路径段"""
def method_handler(*args, **kwargs):
self._calls.append((name, args, kwargs))
return f"API call: {self._base_url}/{name}"
return method_handler
client = APIClient("https://api.example.com")
result = client.get_users()
print(result) # API call: https://api.example.com/get_users
print(client._calls) # [('get_users', (), {})]
1.3 属性赋值的机制
属性赋值(obj.attr = value)同样受描述符影响:
python
class LoggedDescriptor:
"""记录所有赋值操作的描述符"""
def __init__(self, name: str = ""):
self.name = name
self.log: list = []
def __set_name__(self, owner, name):
"""Python 3.6+:类创建时自动调用,获取属性名"""
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(f"_{self.name}", None)
def __set__(self, obj, value):
old = obj.__dict__.get(f"_{self.name}")
self.log.append({"old": old, "new": value})
obj.__dict__[f"_{self.name}"] = value
def __delete__(self, obj):
obj.__dict__.pop(f"_{self.name}", None)
class Config:
host = LoggedDescriptor()
port = LoggedDescriptor()
cfg = Config()
cfg.host = "localhost"
cfg.host = "10.0.0.1"
cfg.port = 5432
print(cfg.host) # 10.0.0.1
print(cfg.port) # 5432
print(Config.host.log) # [{'old': None, 'new': 'localhost'}, {'old': 'localhost', 'new': '10.0.0.1'}]
print(Config.port.log) # [{'old': None, 'new': 5432}]
__set_name__ 的重要性 (Python 3.6+):在 Python 3.6 之前,描述符无法自动知道自己被赋给了哪个属性名,需要在 __init__ 中显式传入。__set_name__(self, owner, name) 在类创建时由元类自动调用,解决了这个问题。owner 是拥有描述符的类,name 是属性名。
二、描述符协议(Descriptor Protocol)
2.1 数据描述符 vs 非数据描述符
描述符是实现了 __get__、__set__、__delete__ 中至少一个方法的对象。根据实现的方法不同,分为两类:
| 类型 | 定义 | 优先级 | 典型例子 |
|---|---|---|---|
| 数据描述符 | 定义了 __get__ + (__set__ 或 __delete__) |
高于实例字典 | property、ORM Field |
| 非数据描述符 | 只定义了 __get__ |
低于实例字典 | 函数(function)、staticmethod、classmethod |
python
class DataDesc:
"""数据描述符"""
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get("_data_val", "default")
def __set__(self, obj, value):
obj.__dict__["_data_val"] = value
class NonDataDesc:
"""非数据描述符"""
def __get__(self, obj, objtype=None):
if obj is None:
return self
return "computed_value"
class Demo:
data = DataDesc()
non_data = NonDataDesc()
d = Demo()
# 数据描述符:__set__ 拦截赋值
d.data = 42
print(d.data) # 42 -- __get__ 从 __dict__ 取值
print("_data_val" in d.__dict__) # True
# 非数据描述符:赋值直接写入实例字典,遮盖描述符
d.non_data = "override"
print(d.non_data) # override -- 实例字典优先
del d.non_data # 删除实例字典中的值
print(d.non_data) # computed_value -- 描述符重新生效
2.2 函数也是描述符
Python 的函数对象实现了 __get__ 方法(是非数据描述符),这就是"绑定方法"的底层原理:
python
class MyClass:
def greet(self):
return f"Hello from {self}"
# 函数对象本身
print(type(MyClass.__dict__["greet"])) # <class 'function'>
# 通过实例访问时,函数描述符的 __get__ 被调用,返回绑定方法
obj = MyClass()
print(type(obj.greet)) # <class 'method'>
# 手动调用函数的 __get__ 方法
func = MyClass.__dict__["greet"]
bound_method = func.__get__(obj, MyClass)
print(bound_method()) # Hello from <__main__.MyClass object at 0x...>
# 这就是为什么实例方法的第一个参数 self 会被自动填充
# obj.greet() 实际上等价于 MyClass.__dict__["greet"].__get__(obj, MyClass)()
2.3 用纯 Python 实现 property
理解了描述符,我们可以用纯 Python 复现 property 的完整行为:
python
class MyProperty:
"""用纯 Python 复现 property 的行为"""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self # 通过类访问时返回描述符本身
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
# 使用我们自己的 property
class Circle:
def __init__(self, radius: float):
self._radius = radius
@MyProperty
def radius(self) -> float:
"""The radius of the circle."""
return self._radius
@radius.setter
def radius(self, value: float) -> None:
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@MyProperty
def area(self) -> float:
"""Computed area (read-only)."""
import math
return math.pi * self._radius ** 2
c = Circle(5.0)
print(c.radius) # 5.0
c.radius = 10.0
print(c.radius) # 10.0
print(f"{c.area:.2f}") # 314.16
# 验证 read-only
try:
c.area = 100
except AttributeError as e:
print(f"Error: {e}") # Error: can't set attribute
# 验证校验
try:
c.radius = -1
except ValueError as e:
print(f"Error: {e}") # Error: Radius must be positive
为什么 property 是数据描述符? 因为它同时实现了 __get__、__set__ 和 __delete__。即使你没有定义 setter,property.__set__ 仍然存在(只是会抛出 AttributeError)。这保证了数据描述符的优先级 -- 任何对属性的赋值都会被 property 拦截,不会意外写入实例字典。
三、@property 的进阶使用
3.1 getter/setter/deleter 的完整用法
python
class User:
"""property 的完整用法示例"""
def __init__(self, first_name: str, last_name: str, email: str):
self.first_name = first_name
self.last_name = last_name
self.email = email # 触发 setter 进行校验
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str) -> None:
if "@" not in value:
raise ValueError(f"Invalid email: {value}")
self._email = value.lower().strip()
@email.deleter
def email(self) -> None:
self._email = ""
@property
def full_name(self) -> str:
"""只读计算属性"""
return f"{self.first_name} {self.last_name}"
u = User("Yu", "Fei", "Test@Example.COM")
print(u.email) # test@example.com
print(u.full_name) # Yu Fei
# deleter
del u.email
print(u.email) # (empty string)
# setter 校验
try:
u.email = "invalid"
except ValueError as e:
print(f"Validation: {e}") # Validation: Invalid email: invalid
3.2 functools.cached_property:缓存计算属性
functools.cached_property(Python 3.8+)是一个非数据描述符,它在首次访问时计算值并将结果写入实例字典。由于非数据描述符的优先级低于实例字典,后续访问会直接从实例字典读取,不再触发计算。
python
import functools
import time
class DataAnalyzer:
"""使用 cached_property 缓存昂贵的计算"""
def __init__(self, data: list):
self._data = data
@functools.cached_property
def statistics(self) -> dict:
"""首次访问时计算,结果缓存在实例字典中"""
print(" Computing statistics... (expensive operation)")
time.sleep(0.01) # 模拟昂贵计算
n = len(self._data)
total = sum(self._data)
mean = total / n if n > 0 else 0
sorted_data = sorted(self._data)
median = sorted_data[n // 2] if n > 0 else 0
return {"count": n, "sum": total, "mean": mean, "median": median}
analyzer = DataAnalyzer([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
# 第一次访问:触发计算
print(analyzer.statistics)
# Computing statistics... (expensive operation)
# {'count': 10, 'sum': 39, 'mean': 3.9, 'median': 4}
# 第二次访问:直接从实例字典读取,不再计算
print(analyzer.statistics) # 直接返回结果,无 "Computing..." 输出
# 验证:值确实在实例字典中
print("statistics" in analyzer.__dict__) # True
# 清除缓存的方式:删除实例字典中的属性
del analyzer.__dict__["statistics"]
# 下次访问会重新计算
print("After cache clear:", "statistics" in analyzer.__dict__) # False
cached_property 的原理:
python
# cached_property 的简化实现
class SimpleCachedProperty:
"""非数据描述符:只有 __get__,没有 __set__"""
def __init__(self, func):
self.func = func
self.attrname = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 计算值并写入实例字典
value = self.func(obj)
obj.__dict__[self.attrname] = value # 后续访问直接走实例字典
return value
线程安全注意事项:
在 Python 3.12 之前,functools.cached_property 不是线程安全的 。如果多个线程同时首次访问同一个 cached_property,计算函数可能被执行多次。在 Python 3.12 中,cached_property 加入了锁机制来保证线程安全。对于 3.8~3.11,如果需要线程安全的缓存属性,需要自行加锁:
python
import threading
import functools
class ThreadSafeCachedProperty:
"""线程安全的缓存属性描述符"""
_MISSING = object()
def __init__(self, func):
self.func = func
self.attrname = func.__name__
self.lock = threading.Lock()
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 先检查实例字典(无锁,快速路径)
val = obj.__dict__.get(self.attrname, self._MISSING)
if val is not self._MISSING:
return val
# 未找到,加锁计算
with self.lock:
# Double-checked locking
val = obj.__dict__.get(self.attrname, self._MISSING)
if val is not self._MISSING:
return val
val = self.func(obj)
obj.__dict__[self.attrname] = val
return val
class Service:
@ThreadSafeCachedProperty
def db_connection(self):
print(" Creating DB connection...")
return {"host": "localhost", "port": 5432, "connected": True}
svc = Service()
print(svc.db_connection) # Creating DB connection...
print(svc.db_connection) # 直接返回,不再创建
3.3 property vs cached_property 对比
| 特性 | property |
cached_property |
|---|---|---|
| 描述符类型 | 数据描述符 | 非数据描述符 |
| 每次访问是否重新计算 | 是 | 否(首次计算后缓存) |
| 支持 setter | 是 | 否 |
| 缓存位置 | 无缓存 | 实例的 __dict__ |
与 __slots__ 兼容 |
是 | 否(需要 __dict__) |
| 线程安全(3.8~3.11) | 不涉及 | 不安全 |
| 适用场景 | 需要校验/计算的属性 | 昂贵的一次性计算 |
四、实战应用
4.1 类型检查描述符(参数验证)
描述符最经典的应用之一是参数验证。以下是一个可复用的类型+范围检查描述符:
python
class Validated:
"""通用验证描述符基类"""
def __init__(self, **kwargs):
self.validators = kwargs
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, None)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.storage_name, value)
def validate(self, value):
if "type" in self.validators:
expected = self.validators["type"]
if not isinstance(value, expected):
raise TypeError(
f"'{self.name}' must be {expected.__name__}, got {type(value).__name__}"
)
if "min_value" in self.validators:
if value < self.validators["min_value"]:
raise ValueError(
f"'{self.name}' must be >= {self.validators['min_value']}, got {value}"
)
if "max_value" in self.validators:
if value > self.validators["max_value"]:
raise ValueError(
f"'{self.name}' must be <= {self.validators['max_value']}, got {value}"
)
if "min_length" in self.validators:
if len(value) < self.validators["min_length"]:
raise ValueError(
f"'{self.name}' length must be >= {self.validators['min_length']}"
)
if "max_length" in self.validators:
if len(value) > self.validators["max_length"]:
raise ValueError(
f"'{self.name}' length must be <= {self.validators['max_length']}"
)
class Product:
name = Validated(type=str, min_length=1, max_length=100)
price = Validated(type=(int, float), min_value=0)
quantity = Validated(type=int, min_value=0)
def __init__(self, name: str, price: float, quantity: int):
self.name = name
self.price = price
self.quantity = quantity
def __repr__(self) -> str:
return f"Product(name={self.name!r}, price={self.price}, qty={self.quantity})"
# 正常使用
p = Product("Widget", 9.99, 100)
print(p) # Product(name='Widget', price=9.99, qty=100)
# 类型检查
try:
Product(123, 9.99, 100)
except TypeError as e:
print(f"TypeError: {e}") # TypeError: 'name' must be str, got int
# 范围检查
try:
Product("Widget", -1.0, 100)
except ValueError as e:
print(f"ValueError: {e}") # ValueError: 'price' must be >= 0, got -1.0
# 长度检查
try:
Product("", 9.99, 100)
except ValueError as e:
print(f"ValueError: {e}") # ValueError: 'name' length must be >= 1
4.2 ORM 字段描述符(类 Django Model Field)
ORM 框架的核心就是描述符。以下简化实现展示了 Django Model Field 的底层原理:
python
class Field:
"""ORM 字段基类 -- 描述符实现"""
field_type = "UNKNOWN"
def __init__(self, primary_key: bool = False, nullable: bool = False,
default=None, max_length: int = None):
self.primary_key = primary_key
self.nullable = nullable
self.default = default
self.max_length = max_length
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_field_{name}"
# 注册到 owner 类的字段列表中
if not hasattr(owner, "_fields"):
owner._fields = {}
owner._fields[name] = self
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, self.default)
def __set__(self, obj, value):
if value is None and not self.nullable:
raise ValueError(f"Field '{self.name}' is not nullable")
if value is not None:
value = self.to_python(value)
setattr(obj, self.storage_name, value)
def to_python(self, value):
"""子类覆写:类型转换"""
return value
def to_sql(self) -> str:
"""生成 SQL 列定义"""
sql = f"{self.name} {self.field_type}"
if self.primary_key:
sql += " PRIMARY KEY"
if not self.nullable:
sql += " NOT NULL"
return sql
class IntegerField(Field):
field_type = "INTEGER"
def to_python(self, value):
if not isinstance(value, int):
try:
return int(value)
except (ValueError, TypeError):
raise ValueError(f"Cannot convert {value!r} to integer")
return value
class CharField(Field):
field_type = "VARCHAR"
def to_python(self, value):
value = str(value)
if self.max_length and len(value) > self.max_length:
raise ValueError(
f"Field '{self.name}' max length is {self.max_length}, got {len(value)}"
)
return value
def to_sql(self) -> str:
base = f"{self.name} {self.field_type}"
if self.max_length:
base = f"{self.name} {self.field_type}({self.max_length})"
if self.primary_key:
base += " PRIMARY KEY"
if not self.nullable:
base += " NOT NULL"
return base
class FloatField(Field):
field_type = "REAL"
def to_python(self, value):
if not isinstance(value, (int, float)):
try:
return float(value)
except (ValueError, TypeError):
raise ValueError(f"Cannot convert {value!r} to float")
return float(value)
class Model:
"""ORM Model 基类"""
_fields: dict
def __init__(self, **kwargs):
for name, field in self.__class__._fields.items():
if name in kwargs:
setattr(self, name, kwargs[name])
elif field.default is not None:
setattr(self, name, field.default)
@classmethod
def create_table_sql(cls) -> str:
columns = []
for name, f in cls._fields.items():
columns.append(f.to_sql())
return f"CREATE TABLE {cls.__name__.lower()} (\n " + ",\n ".join(columns) + "\n);"
def __repr__(self) -> str:
fields = ", ".join(
f"{name}={getattr(self, name)!r}" for name in self.__class__._fields
)
return f"{self.__class__.__name__}({fields})"
class UserModel(Model):
id = IntegerField(primary_key=True)
name = CharField(max_length=50)
email = CharField(max_length=100, nullable=True)
score = FloatField(default=0.0)
# 使用
user = UserModel(id=1, name="Alice", email="alice@example.com", score=95.5)
print(user) # UserModel(id=1, name='Alice', email='alice@example.com', score=95.5)
# 自动类型转换
user.score = "88.5" # 字符串自动转 float
print(user.score) # 88.5
print(type(user.score)) # <class 'float'>
# 生成建表 SQL
print(UserModel.create_table_sql())
# CREATE TABLE usermodel (
# id INTEGER PRIMARY KEY NOT NULL,
# name VARCHAR(50) NOT NULL,
# email VARCHAR(100),
# score REAL NOT NULL
# );
# nullable 校验
try:
user.name = None
except ValueError as e:
print(f"Nullable check: {e}") # Nullable check: Field 'name' is not nullable
4.3 延迟加载(Lazy Loading)描述符
在 Web 应用中,延迟加载可以避免不必要的数据库查询或 API 调用:
python
class LazyLoad:
"""延迟加载描述符:首次访问时才执行加载函数"""
def __init__(self, loader_func):
self.loader_func = loader_func
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_lazy_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 检查是否已加载
if not hasattr(obj, self.storage_name):
# 首次访问:执行加载
value = self.loader_func(obj)
setattr(obj, self.storage_name, value)
return getattr(obj, self.storage_name)
def __set__(self, obj, value):
"""允许手动设置值(覆盖延迟加载)"""
setattr(obj, self.storage_name, value)
def __delete__(self, obj):
"""删除缓存,下次访问重新加载"""
if hasattr(obj, self.storage_name):
delattr(obj, self.storage_name)
def _load_profile(user):
"""模拟从数据库加载用户 profile"""
print(f" Loading profile for user {user.user_id}...")
return {"avatar": "default.png", "bio": "Hello!", "user_id": user.user_id}
def _load_permissions(user):
"""模拟从缓存/数据库加载权限"""
print(f" Loading permissions for user {user.user_id}...")
return ["read", "write", "admin"]
class UserEntity:
profile = LazyLoad(_load_profile)
permissions = LazyLoad(_load_permissions)
def __init__(self, user_id: int, name: str):
self.user_id = user_id
self.name = name
# 创建用户不会触发任何加载
user = UserEntity(1001, "Alice")
print(f"User created: {user.name}")
# 首次访问 profile 触发加载
print(user.profile)
# Loading profile for user 1001...
# {'avatar': 'default.png', 'bio': 'Hello!', 'user_id': 1001}
# 再次访问不会重新加载
print(user.profile["avatar"]) # default.png(无 "Loading..." 输出)
# 首次访问 permissions 触发加载
print(user.permissions)
# Loading permissions for user 1001...
# ['read', 'write', 'admin']
# 可以手动设置值
user.profile = {"avatar": "custom.png", "bio": "Updated"}
print(user.profile["avatar"]) # custom.png
# 删除缓存,下次访问重新加载
del user.profile
print(user.profile)
# Loading profile for user 1001...
# {'avatar': 'default.png', 'bio': 'Hello!', 'user_id': 1001}
4.4 描述符的高级模式:链式验证
python
class ValidatorChain:
"""支持链式验证的描述符"""
def __init__(self):
self._validators: list = []
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_{name}"
def type_check(self, expected_type):
"""添加类型检查"""
def validator(value):
if not isinstance(value, expected_type):
raise TypeError(
f"'{self.name}' expects {expected_type.__name__}, "
f"got {type(value).__name__}"
)
self._validators.append(validator)
return self
def range_check(self, min_val=None, max_val=None):
"""添加范围检查"""
def validator(value):
if min_val is not None and value < min_val:
raise ValueError(f"'{self.name}' must be >= {min_val}")
if max_val is not None and value > max_val:
raise ValueError(f"'{self.name}' must be <= {max_val}")
self._validators.append(validator)
return self
def custom(self, func):
"""添加自定义验证"""
self._validators.append(func)
return self
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.storage_name)
def __set__(self, obj, value):
for validator in self._validators:
validator(value)
obj.__dict__[self.storage_name] = value
class Order:
amount = ValidatorChain().type_check((int, float)).range_check(min_val=0)
status = ValidatorChain().type_check(str).custom(
lambda v: None if v in ("pending", "paid", "shipped", "delivered")
else (_ for _ in ()).throw(ValueError(f"Invalid status: {v}"))
)
def __init__(self, amount: float, status: str = "pending"):
self.amount = amount
self.status = status
def __repr__(self) -> str:
return f"Order(amount={self.amount}, status={self.status!r})"
上面的 status 验证器使用了 lambda 技巧来抛出异常,这在实际代码中可读性不佳。让我们用更清晰的方式实现:
python
class StatusValidator:
"""更清晰的链式验证方式"""
VALID_STATUSES = ("pending", "paid", "shipped", "delivered")
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(f"_{self.name}")
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"Status must be str, got {type(value).__name__}")
if value not in self.VALID_STATUSES:
raise ValueError(f"Invalid status '{value}', must be one of {self.VALID_STATUSES}")
obj.__dict__[f"_{self.name}"] = value
class OrderV2:
amount = Validated(type=(int, float), min_value=0)
status = StatusValidator()
def __init__(self, amount: float, status: str = "pending"):
self.amount = amount
self.status = status
def __repr__(self) -> str:
return f"OrderV2(amount={self.amount}, status={self.status!r})"
order = OrderV2(99.99, "pending")
print(order) # OrderV2(amount=99.99, status='pending')
order.status = "paid"
print(order.status) # paid
try:
order.status = "cancelled"
except ValueError as e:
print(f"Status validation: {e}")
# Invalid status 'cancelled', must be one of ('pending', 'paid', 'shipped', 'delivered')
try:
order.amount = -10
except ValueError as e:
print(f"Amount validation: {e}") # 'amount' must be >= 0, got -10
五、面试高频题汇总
Q1:描述符协议的工作原理是什么?
A:描述符是实现了 __get__、__set__、__delete__ 中至少一个方法的对象。当一个类的类属性是描述符实例时,通过该类的实例访问这个属性会触发描述符协议,而非普通的字典查找。
描述符分两种:
- 数据描述符 :同时实现了
__get__和__set__(或__delete__),优先级高于实例字典 - 非数据描述符 :只实现了
__get__,优先级低于实例字典
完整的属性查找优先级是:数据描述符 > 实例 __dict__ > 非数据描述符 / 类属性 > __getattr__。
__set_name__(self, owner, name) 是 Python 3.6 新增的钩子,在类创建时自动调用,让描述符知道自己被绑定到了哪个属性名上。
Q2:@property 的底层实现机制是怎样的?
A:property 是一个数据描述符 ,它同时实现了 __get__、__set__、__delete__:
__get__调用fget函数(getter)__set__调用fset函数(setter),如果没有定义 setter 则抛出AttributeError__delete__调用fdel函数
@property 是语法糖,@prop.setter 和 @prop.deleter 分别返回一个新的 property 对象(替换了对应的函数)。因为 property 是数据描述符,它的优先级高于实例字典,所以即使实例的 __dict__ 中有同名键,访问时仍然走 property 的 __get__。
Q3:数据描述符和非数据描述符的优先级差异?
A:属性查找时,数据描述符的优先级高于实例字典,而非数据描述符的优先级低于实例字典。
这个设计有深远的实际影响:
property是数据描述符,所以它能可靠地拦截所有的属性访问和赋值functools.cached_property是非数据描述符,所以首次计算后将值写入实例字典,后续访问直接走实例字典(绕过描述符),实现了缓存效果- 普通函数也是非数据描述符(实现了
__get__),这就是为什么实例方法通过obj.method访问时会变成绑定方法
Q4:__getattr__ 和 __getattribute__ 的区别?
A:__getattribute__ 在每次 属性访问时都会被调用,它是属性访问的入口。__getattr__ 只在常规查找失败 时才会被调用(即 __getattribute__ 抛出 AttributeError 后才触发)。
使用 __getattribute__ 要极其小心,因为在其内部访问 self 的任何属性都会再次触发 __getattribute__,很容易导致无限递归。正确做法是使用 object.__getattribute__(self, name) 来避免递归。
__getattr__ 更安全也更常用,适合实现动态属性、代理模式、属性默认值等场景。
Q5:functools.cached_property 和 property 有什么区别?使用时需要注意什么?
A:核心区别在于描述符类型:property 是数据描述符(每次访问都重新计算),cached_property 是非数据描述符(首次计算后写入实例字典缓存)。
使用 cached_property 需要注意三点:
- 不兼容
__slots__:它需要实例有__dict__来存储缓存值 - 线程安全:Python 3.8~3.11 中不是线程安全的,多线程可能导致重复计算。3.12 之后加入了锁
- 缓存失效 :只能通过
del obj.__dict__['attr_name']手动清除缓存
本章总结
本文从 CPython 实现层面,系统性地剖析了 Python 属性访问的完整机制:
-
属性访问链路 :
obj.attr的查找顺序是 -- 数据描述符 > 实例__dict__> 非数据描述符/类属性 >__getattr__。__getattribute__是整个流程的入口,每次属性访问都会经过它。理解这个优先级链是掌握描述符的关键。 -
描述符协议 :描述符是实现了
__get__/__set__/__delete__的对象。数据描述符(有__set__或__delete__)优先于实例字典,非数据描述符(只有__get__)低于实例字典。这个设计让property能拦截所有属性操作,让cached_property能实现缓存机制。 -
property 进阶 :
property本质上是一个数据描述符,可以用纯 Python 复现其实现。functools.cached_property是非数据描述符,适合缓存昂贵计算,但在 Python 3.12 之前不是线程安全的。 -
实战应用 :描述符在 ORM 字段定义(Django Model Field)、参数验证、延迟加载等场景中有广泛应用。通过
__set_name__(Python 3.6+)自动获取属性名,描述符的定义变得更加简洁。
核心原则 :描述符是 Python 中 property、classmethod、staticmethod、super 的底层实现机制,也是理解 ORM、表单验证等框架的关键。掌握描述符协议,你就理解了 Python 对象模型最深层的运作方式。
下一篇预告
第 07 篇:元类与类的创建过程 -- Python 最深层的魔法
下一篇文章将进入 Python 元编程的核心地带。你将了解:
- 类的创建过程 :
type是所有类的元类,type(name, bases, namespace)动态创建类的完整流程 - 元类(Metaclass) :
metaclass=参数的传播规则,__init_subclass__作为轻量替代 - 类装饰器 vs 元类:各自的适用场景对比与选型建议
abc.ABCMeta与抽象基类 :抽象方法、虚拟子类注册,以及Protocol的结构化子类型
如果说描述符控制的是"属性的行为",那么元类控制的是"类的行为"。理解元类,你就站在了 Python 对象模型的最高点。
Python 后端开发技术博客专栏 | 作者:耿雨飞
本文为专栏第 06 篇,共 25 篇。完整目录请参阅《Python技术博客专栏大纲》。