Python 元類(中):攔截類的創(chuàng)建方法
上篇我們知道了:type 是一個類工廠,所有類都是它創(chuàng)建的。class 語句本質(zhì)上就是一次 type() 調(diào)用。
那如果我們自己寫一個類工廠,替掉 type,會怎樣?
第一個元類
元類就是一個繼承了 type 的類。你重寫它的 __new__ 方法,就能在類創(chuàng)建的時候插一腳:
class SimpleMeta(type):
def __new__(mcs, name, bases, namespace):
print(f" 正在創(chuàng)建類: {name}")
cls = super().__new__(mcs, name, bases, namespace)
return cls
class MyClass(metaclass=SimpleMeta):
pass
# 輸出: 正在創(chuàng)建類: MyClass
print(type(MyClass)) # <class '__main__.SimpleMeta'> —— 不再是 type就這么簡單。metaclass=SimpleMeta 告訴 Python:「創(chuàng)建 MyClass 的時候,別用默認(rèn)的 type(),用我的 SimpleMeta()」。
你可以多創(chuàng)建幾個類,看看效果:
class Another(metaclass=SimpleMeta):
pass
class AndAnother(metaclass=SimpleMeta):
pass
# 輸出:
# 正在創(chuàng)建類: Another
# 正在創(chuàng)建類: AndAnother每一個用 SimpleMeta 創(chuàng)建的類,都會經(jīng)過 __new__。這就是元類的核心——攔截類的創(chuàng)建過程。
參數(shù)是什么意思?
__new__ 里的四個參數(shù),和 type() 的三個參數(shù)一一對應(yīng):
| 參數(shù) | 含義 | 來源 |
|---|---|---|
mcs | 元類本身 | Python 自動傳入 |
name | 正在創(chuàng)建的類名 | 對應(yīng) type() 的第一個參數(shù) |
bases | 父類元組 | 對應(yīng) type() 的第二個參數(shù) |
namespace | 類體中定義的所有屬性和方法的字典 | 對應(yīng) type() 的第三個參數(shù) |
你可以把 namespace 打印出來看看:
class DebugMeta(type):
def __new__(mcs, name, bases, namespace):
print(f" namespace = {namespace}")
return super().__new__(mcs, name, bases, namespace)
class Sample(metaclass=DebugMeta):
x = 10
def hello(self):
return "hi"
# 輸出:
# namespace = {'__module__': '__main__', '__qualname__': 'Sample', 'x': 10, 'hello': <function Sample.hello at 0x...>}x = 10 和 def hello 都在字典里。你可以讀它、改它、甚至拒絕創(chuàng)建這個類。
實戰(zhàn)一:自動注入屬性
理解了參數(shù),我們來做點實用的事情。假設(shè)你有一批類都需要一個 version 屬性,但你不想每個類都手動寫:
class AutoAttrMeta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if not hasattr(cls, 'version'):
cls.version = "1.0"
return cls
class Service(metaclass=AutoAttrMeta):
pass
class AdvancedService(Service):
version = "2.0" # 自己定義了,不會被覆蓋
class BasicService(metaclass=AutoAttrMeta):
pass
print(Service.version) # 1.0 ← 自動加上了
print(AdvancedService.version) # 2.0 ← 自己定義的,沒被覆蓋
print(BasicService.version) # 1.0 ← 也自動加上了邏輯很簡單:類創(chuàng)建完之后,檢查一下有沒有 version,沒有就補(bǔ)上。
這就是元類最典型的用法——在類創(chuàng)建的那一刻,自動做一些事情。Django 的 Model 類就是這么干的:你寫 class User(models.Model): 的時候,元類在背后自動幫你加了 __tablename__、__fields__ 一堆東西,你根本不用操心。
實戰(zhàn)二:在類定義時就攔住錯誤
元類還能做校驗。與其讓代碼跑到一半才發(fā)現(xiàn)字段名寫錯了,不如在類定義的時候就報錯:
class LowercaseMeta(type):
def __new__(mcs, name, bases, namespace):
for attr_name in namespace:
if not attr_name.startswith('_') and attr_name != attr_name.lower():
raise TypeError(
f"類 '{name}' 中的屬性 '{attr_name}' 必須全小寫!"
)
return super().__new__(mcs, name, bases, namespace)
class GoodClass(metaclass=LowercaseMeta):
my_field = 1 # ? 小寫
another_field = 2 # ? 小寫
print(GoodClass.my_field) # 1 —— 沒問題
try:
class BadClass(metaclass=LowercaseMeta):
BadField = 1 # ? 大寫了!
except TypeError as e:
print(e) # 類 'BadClass' 中的屬性 'BadField' 必須全小寫!BadField 在類定義的那一刻就被攔下來了。這個錯誤不會等到你調(diào)用某個方法時才冒出來——它在模塊被 import 的時候就會發(fā)生,非常早。
實戰(zhàn)三:單例模式
單例的意思是:一個類只能有一個實例。不管你調(diào)用多少次 Database(),拿到的都是同一個對象。
要實現(xiàn)這個,我們需要攔截的不是類的創(chuàng)建,而是實例的創(chuàng)建。
上篇我們講過,當(dāng)你寫 Foo() 的時候,Python 實際上是在調(diào)用 type.__call__()。這個 __call__ 方法會依次調(diào)用類的 __new__ 和 __init__。元類可以重寫這個 __call__,在實例化時插入自己的邏輯:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
print(f" 創(chuàng)建 {cls.__name__} 的唯一實例")
cls._instances[cls] = super().__call__(*args, **kwargs)
else:
print(f" 返回已有實例")
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
self.connection_id = id(self)
db1 = Database() # 創(chuàng)建 Database 的唯一實例
db2 = Database() # 返回已有實例
print(db1 is db2) # True —— 同一個對象
print(db1.connection_id == db2.connection_id) # True調(diào)用鏈?zhǔn)沁@樣的:
Database()
│
▼
SingletonMeta.__call__(Database) ← 元類攔截
│
├─ 第一次 → super().__call__() → Database.__new__() → Database.__init__()
│
└─ 之后 → 直接返回緩存的實例注意:元類的 __call__ 攔截的是 類() 這個調(diào)用,而類的 __new__ 和 __init__ 負(fù)責(zé)實際創(chuàng)建和初始化實例。元類站在它們「上面」,決定要不要調(diào)用它們。
實戰(zhàn)四:插件自動注冊
最后一個實戰(zhàn)例子——插件自動注冊。你定義一個子類,它就自動出現(xiàn)在注冊表里,不需要手動調(diào)用任何注冊函數(shù):
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != "Base": # 跳過基類本身
mcs.registry[name] = cls
return cls
class Base(metaclass=PluginMeta):
pass
class AuthPlugin(Base):
def authenticate(self):
return "authenticated"
class LogPlugin(Base):
def log(self, msg):
return f"LOG: {msg}"
# 不需要手動注冊,定義類的時候就完成了
print(list(PluginMeta.registry.keys())) # ['AuthPlugin', 'LogPlugin']
# 可以通過名字動態(tài)實例化
plugin = PluginMeta.registry["AuthPlugin"]()
print(plugin.authenticate()) # authenticated這種模式在實際項目中非常常見:插件系統(tǒng)、策略模式、序列化框架按類型名找處理器……本質(zhì)上都是「定義即注冊」。
一個關(guān)鍵的細(xì)節(jié):執(zhí)行時機(jī)
元類的代碼,在什么時候跑?
在類定義的時候,不是實例化的時候。
class TraceMeta(type):
def __new__(mcs, name, bases, namespace):
print(f" [元類] 正在創(chuàng)建 {name}")
return super().__new__(mcs, name, bases, namespace)
print("開始定義類...")
class A(metaclass=TraceMeta):
pass
print("類定義完成")
# 輸出:
# 開始定義類...
# [元類] 正在創(chuàng)建 A
# 類定義完成模塊被 import 的那一刻,元類就已經(jīng)跑完了。之后你 A() 實例化的時候,元類的 __new__ 不會再執(zhí)行(但 __call__ 會,如果你重寫了的話)。
回顧一下
到這里你已經(jīng)知道了:
- 元類就是一個繼承
type的類工廠,重寫__new__可以在類創(chuàng)建時做任何事情 - 重寫
__call__可以攔截實例化,控制__new__和__init__是否被調(diào)用 - 元類代碼在類定義時就執(zhí)行,不是實例化時
但元類的能力不止于此。它還能控制類的命名空間、自定義 isinstance 的行為、處理繼承鏈上的元類沖突……
下一篇,我們講這些進(jìn)階內(nèi)容,以及一個更重要的問題:什么時候不該用元類。
到此這篇關(guān)于Python 元類(中):攔截類的創(chuàng)建方法的文章就介紹到這了,更多相關(guān)Python攔截類的創(chuàng)建內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python利用appium實現(xiàn)模擬手機(jī)滑動操控的操作
Appium 是一個開源、跨平臺的自動化測試工具,用于測試原生和輕量移動應(yīng)用,支持 iOS, Android 和 FirefoxOS 平臺。本文將利用appium實現(xiàn)模擬手機(jī)滑動操控的操作,感興趣的可以了解一下2022-07-07
使用Pycharm為項目創(chuàng)建一個虛擬環(huán)境完整圖文教程
這篇文章主要給大家介紹了關(guān)于使用Pycharm為項目創(chuàng)建一個虛擬環(huán)境的相關(guān)資料,我們在使用pycharm做項目時,最好給每一個工程都創(chuàng)建一個虛擬環(huán)境,將對應(yīng)的安裝包放在該虛擬環(huán)境中,避免項目與項目之間產(chǎn)生關(guān)系或沖突,便于管理,需要的朋友可以參考下2023-09-09
關(guān)于Python3 lambda函數(shù)的深入淺出
今天小編就為大家分享一篇關(guān)于Python3 lambda函數(shù)的深入淺出,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11
Python實現(xiàn)壓縮文件夾與解壓縮zip文件的方法
這篇文章主要介紹了Python實現(xiàn)壓縮文件夾與解壓縮zip文件的方法,涉及Python使用zipfile模塊進(jìn)行zip文件壓縮與解壓縮相關(guān)操作技巧,需要的朋友可以參考下2018-09-09
Python使用keras和tensorflow遇到的問題及解決
這篇文章主要介紹了Python使用keras和tensorflow遇到的問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03

