Python 中的 __set__ 与 __set_name__ 的具体使用
作者:无风听海
这两个方法都属于描述符协议(Descriptor Protocol),是 Python 元编程的核心机制之一。
一、描述符协议基础
描述符是实现了以下至少一个方法的类:
| 方法 | 触发时机 | 类型 |
|---|---|---|
| __get__ | 读取属性 | 非数据描述符(若单独使用) |
| __set__ | 设置属性 | 数据描述符(含此方法) |
| __delete__ | 删除属性 | 数据描述符(含此方法) |
| __set_name__ | 类定义时 | 辅助方法(Python 3.6+) |
二、__set__详解
签名
def __set__(self, obj, value):
...
self:描述符实例本身obj:拥有该描述符的类的实例value:被赋予的值
核心机制
class Descriptor:
def __get__(self, obj, objtype=None):
print(f"__get__ called, obj={obj}")
return obj.__dict__.get('_value')
def __set__(self, obj, value):
print(f"__set__ called, value={value}")
obj.__dict__['_value'] = value # 注意:不能用 obj.attr = value,会无限递归!
class MyClass:
attr = Descriptor()
m = MyClass()
m.attr = 42 # 触发 __set__
print(m.attr) # 触发 __get__
输出:
__set__ called, value=42
__get__ called, obj=<__main__.MyClass object>
42
⚠️ 数据描述符 vs 非数据描述符
属性查找优先级(MRO 之后):
数据描述符 > 实例 __dict__ > 非数据描述符
class NonData:
"""只有 __get__,非数据描述符"""
def __get__(self, obj, objtype=None):
return "from descriptor"
class Data:
"""有 __set__,数据描述符"""
def __get__(self, obj, objtype=None):
return "from descriptor"
def __set__(self, obj, value):
pass
class A:
x = NonData()
y = Data()
a = A()
a.__dict__['x'] = "from instance"
a.__dict__['y'] = "from instance"
print(a.x) # "from instance" ← 实例 __dict__ 胜出
print(a.y) # "from descriptor" ← 数据描述符胜出!
实战:类型验证描述符
class TypedField:
def __init__(self, expected_type):
self.expected_type = expected_type
self.storage_name = None # 后面由 __set_name__ 填充
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"'{self.storage_name}' 期望 {self.expected_type.__name__},"
f"得到 {type(value).__name__}"
)
obj.__dict__[self.storage_name] = value
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.storage_name)
class Person:
name = TypedField(str)
age = TypedField(int)
p = Person()
p.name = "Alice" # ✅
p.age = 30 # ✅
p.age = "thirty" # ❌ TypeError
三、__set_name__详解(Python 3.6+)
为什么需要它?
在 __set_name__ 出现之前,描述符不知道自己在宿主类中叫什么名字,只能手动传入:
# 旧方式(繁琐且容易出错)
class Person:
name = TypedField(str, 'name') # 必须手动重复写名字
age = TypedField(int, 'age')
签名
def __set_name__(self, owner, name):
...
self:描述符实例owner:拥有该描述符的类(不是实例!)name:该描述符在类中的属性名
触发时机
__set_name__ 在类定义完成时由元类 type.__new__ 自动调用,早于任何实例的创建。
class Descriptor:
def __set_name__(self, owner, name):
print(f"__set_name__ 被调用: owner={owner.__name__}, name='{name}'")
self.name = name
class MyClass:
foo = Descriptor() # 类定义时立即触发
bar = Descriptor()
# 输出:
# __set_name__ 被调用: owner=MyClass, name='foo'
# __set_name__ 被调用: owner=MyClass, name='bar'
与__set__联动:完整的描述符
class Validated:
"""通用验证描述符,自动感知自身名称"""
def __set_name__(self, owner, name):
self.public_name = name # 外部访问名,如 'age'
self.private_name = '_' + name # 内部存储名,如 '_age'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
value = self.validate(value)
setattr(obj, self.private_name, value)
def validate(self, value):
raise NotImplementedError
class PositiveInt(Validated):
def validate(self, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f"必须是正整数,得到: {value!r}")
return value
class NonEmptyStr(Validated):
def validate(self, value):
if not isinstance(value, str) or not value.strip():
raise ValueError(f"不能为空字符串,得到: {value!r}")
return value
class Product:
name = NonEmptyStr()
price = PositiveInt()
stock = PositiveInt()
p = Product()
p.name = "Python Book"
p.price = 99
p.stock = 10
print(p.name, p.price, p.stock) # Python Book 99 10
print(p.__dict__) # {'_name': 'Python Book', '_price': 99, '_stock': 10}
四、__set_name__的高级用法
1. 感知宿主类(owner)
class LoggedField:
def __set_name__(self, owner, name):
self.name = name
# 在宿主类上注册所有被追踪的字段
if not hasattr(owner, '_tracked_fields'):
owner._tracked_fields = []
owner._tracked_fields.append(name)
def __set__(self, obj, value):
print(f"[LOG] {type(obj).__name__}.{self.name} = {value!r}")
obj.__dict__[self.name] = value
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
class Config:
host = LoggedField()
port = LoggedField()
timeout = LoggedField()
print(Config._tracked_fields) # ['host', 'port', 'timeout']
c = Config()
c.host = "localhost" # [LOG] Config.host = 'localhost'
c.port = 8080 # [LOG] Config.port = 8080
2. 动态添加描述符(手动调用__set_name__)
如果在类定义之后动态添加描述符,需要手动调用 __set_name__:
class MyDesc:
def __set_name__(self, owner, name):
self.name = name
def __set__(self, obj, value):
obj.__dict__[self.name] = value * 2
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name) if obj else self
# 动态挂载
desc = MyDesc()
MyClass.new_attr = desc
desc.__set_name__(MyClass, 'new_attr') # ⬅ 必须手动调用!
m = MyClass()
m.new_attr = 10
print(m.new_attr) # 20
五、与property的对比
property 本身就是一个描述符,但描述符更灵活,可以复用:
# property:每个类属性都要写一次
class Circle:
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, v):
if v < 0: raise ValueError
self._radius = v
# 描述符:一次定义,到处复用
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
def __set__(self, obj, value):
if value < 0: raise ValueError(f"{self.name} 不能为负")
obj.__dict__[self.name] = value
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name) if obj else self
class Circle:
radius = PositiveNumber()
class Rectangle:
width = PositiveNumber()
height = PositiveNumber()
六、总结
__set__(self, obj, value)
└─ 在实例赋值时触发(obj.attr = value)
└─ 使描述符成为"数据描述符",优先级高于实例 __dict__
└─ 存储值时用 obj.__dict__ 或 setattr(obj, private_name, value)
__set_name__(self, owner, name)
└─ 在类定义完成时由元类自动调用(一次性)
└─ 提供描述符在类中的名称,解决自我引用问题
└─ 也可感知宿主类,用于注册、元数据收集等
└─ 动态挂载时需手动调用
两者配合使用是构建可复用、类型安全的属性系统的标准模式,也是 dataclasses、SQLAlchemy、Django ORM 等框架底层的核心机制。
到此这篇关于Python 中的 __set__ 与 __set_name__ 的具体使用的文章就介绍到这了,更多相关Python __set__ 与 __set_name__ 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
