Python 中的 __set__ 與 __set_name__ 的具體使用
這兩個方法都屬于描述符協(xié)議(Descriptor Protocol),是 Python 元編程的核心機制之一。
一、描述符協(xié)議基礎(chǔ)
描述符是實現(xiàn)了以下至少一個方法的類:
| 方法 | 觸發(fā)時機 | 類型 |
|---|---|---|
| __get__ | 讀取屬性 | 非數(shù)據(jù)描述符(若單獨使用) |
| __set__ | 設(shè)置屬性 | 數(shù)據(jù)描述符(含此方法) |
| __delete__ | 刪除屬性 | 數(shù)據(jù)描述符(含此方法) |
| __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 # 觸發(fā) __set__
print(m.attr) # 觸發(fā) __get__
輸出:
__set__ called, value=42
__get__ called, obj=<__main__.MyClass object>
42
?? 數(shù)據(jù)描述符 vs 非數(shù)據(jù)描述符
屬性查找優(yōu)先級(MRO 之后):
數(shù)據(jù)描述符 > 實例 __dict__ > 非數(shù)據(jù)描述符
class NonData:
"""只有 __get__,非數(shù)據(jù)描述符"""
def __get__(self, obj, objtype=None):
return "from descriptor"
class Data:
"""有 __set__,數(shù)據(jù)描述符"""
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" ← 數(shù)據(jù)描述符勝出!
實戰(zhàn):類型驗證描述符
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__ 出現(xiàn)之前,描述符不知道自己在宿主類中叫什么名字,只能手動傳入:
# 舊方式(繁瑣且容易出錯)
class Person:
name = TypedField(str, 'name') # 必須手動重復(fù)寫名字
age = TypedField(int, 'age')
簽名
def __set_name__(self, owner, name):
...
self:描述符實例owner:擁有該描述符的類(不是實例?。?/li>name:該描述符在類中的屬性名
觸發(fā)時機
__set_name__ 在類定義完成時由元類 type.__new__ 自動調(diào)用,早于任何實例的創(chuàng)建。
class Descriptor:
def __set_name__(self, owner, name):
print(f"__set_name__ 被調(diào)用: owner={owner.__name__}, name='{name}'")
self.name = name
class MyClass:
foo = Descriptor() # 類定義時立即觸發(fā)
bar = Descriptor()
# 輸出:
# __set_name__ 被調(diào)用: owner=MyClass, name='foo'
# __set_name__ 被調(diào)用: owner=MyClass, name='bar'
與__set__聯(lián)動:完整的描述符
class Validated:
"""通用驗證描述符,自動感知自身名稱"""
def __set_name__(self, owner, name):
self.public_name = name # 外部訪問名,如 'age'
self.private_name = '_' + name # 內(nèi)部存儲名,如 '_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"必須是正整數(shù),得到: {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. 動態(tài)添加描述符(手動調(diào)用__set_name__)
如果在類定義之后動態(tài)添加描述符,需要手動調(diào)用 __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
# 動態(tài)掛載
desc = MyDesc()
MyClass.new_attr = desc
desc.__set_name__(MyClass, 'new_attr') # ? 必須手動調(diào)用!
m = MyClass()
m.new_attr = 10
print(m.new_attr) # 20
五、與property的對比
property 本身就是一個描述符,但描述符更靈活,可以復(fù)用:
# property:每個類屬性都要寫一次
class Circle:
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, v):
if v < 0: raise ValueError
self._radius = v
# 描述符:一次定義,到處復(fù)用
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()
六、總結(jié)
__set__(self, obj, value)
└─ 在實例賦值時觸發(fā)(obj.attr = value)
└─ 使描述符成為"數(shù)據(jù)描述符",優(yōu)先級高于實例 __dict__
└─ 存儲值時用 obj.__dict__ 或 setattr(obj, private_name, value)
__set_name__(self, owner, name)
└─ 在類定義完成時由元類自動調(diào)用(一次性)
└─ 提供描述符在類中的名稱,解決自我引用問題
└─ 也可感知宿主類,用于注冊、元數(shù)據(jù)收集等
└─ 動態(tài)掛載時需手動調(diào)用
兩者配合使用是構(gòu)建可復(fù)用、類型安全的屬性系統(tǒng)的標準模式,也是 dataclasses、SQLAlchemy、Django ORM 等框架底層的核心機制。
到此這篇關(guān)于Python 中的 __set__ 與 __set_name__ 的具體使用的文章就介紹到這了,更多相關(guān)Python __set__ 與 __set_name__ 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
pymongo實現(xiàn)多結(jié)果進行多列排序的方法
這篇文章主要介紹了pymongo實現(xiàn)多結(jié)果進行多列排序的方法,涉及Python排序的相關(guān)技巧,需要的朋友可以參考下2015-05-05
PyTorch解決ModuleNotFoundError: No module named
本文主要介紹了PyTorch解決ModuleNotFoundError: No module named ‘torch’,這個錯誤意味著我們的Python環(huán)境中沒有安裝PyTorch庫,無法正常使用其功能,下面就來具體介紹一下2024-03-03
python如何求數(shù)組連續(xù)最大和的示例代碼
這篇文章主要介紹了python如何求數(shù)組連續(xù)最大和的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02
使用__init__.py將文件夾設(shè)置成Python模塊示例詳解
這篇文章主要為大家介紹了使用__init__.py將文件夾設(shè)置成Python模塊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09

