作者:袁首京 原創文章,轉載時請保留此聲明,並給出原文連接。 元編程並不象它聽起來那麼時髦和新奇。常用的 decorator 就可以認為是一種元編程。簡單來說,元編程就是編寫操作代碼的代碼。 有點繞,是吧?彆著急,咱們一點一點來討論。 註意:本文中的代碼適用於 Python 3.3 及以上。 元類 ...
作者:袁首京
原創文章,轉載時請保留此聲明,並給出原文連接。
元編程並不象它聽起來那麼時髦和新奇。常用的 decorator
就可以認為是一種元編程。簡單來說,元編程就是編寫操作代碼的代碼。
有點繞,是吧?彆著急,咱們一點一點來討論。
註意:本文中的代碼適用於 Python 3.3 及以上。
元類
多數編程語言中,一切東西都有類型。Python 也不例外,我們可以用 type()
函數獲取任意變數的類型。
num = 23
print("Type of num is:", type(num))
lst = [1, 2, 4]
print("Type of lst is:", type(lst))
name = "Atul"
print("Type of name is:", type(name))
執行結果是:
Type of num is: <class 'int'>
Type of lst is: <class 'list'>
Type of name is: <class 'str'>
Python 中的所有類型都是由 Class 定義的。這一條與其它編程語言,比如 Java、C++ 等等不同。在那些語言中,int、char、float 之類是基本數據類型,但是在 Python 中,它們是 int 類或 str 類的對象。
象其它 OOP 語言一樣,我們可以使用 class 定義新類型:
class Student:
pass
stu_obj = Student()
print("Type of stu_obj is:", type(stu_obj))
執行結果是:
Type of stu_obj is: <class '**main**.Student'>
一點兒也不意外,對吧?其實有意外,因為在 Python 中,類也是一個對象,就像任何其他對象一樣,它是元類的實例。即一個特殊的類,創建了 Class 這個特殊的類實例。看如下代碼:
class Student:
pass
print("Type of Student class is:", type(Student))
執行結果是:
Type of Student class is: <class 'type'>
既然類也是一個對象,所以以修改對象相同的方式修改它就順理成章。如下先定義一個沒有任何屬性和方法的類,然後在外部為其添加屬性和方法:
class test:
pass
test.x = 45
test.foo = lambda self: print('Hello')
myobj = test()
print(myobj.x)
myobj.foo()
執行結果是:
45
Hello
以上過程可以簡單概括為:
元類創建類,類創建實例
畫個圖象這樣:
元類 -> 類 -> 實例
因此,我們就可以編寫自定義的元類,執行額外的操作或者註入代碼,來改變類的生成過程。這在某些場景下很有用,主要是比如有些情況下使用元編程更簡單,另一些情況只有元編程才能解決問題。
創建自定義元類
創建自定義元類,有兩種方法。第一種是繼承 type
元類,並且覆寫兩個方法:
- new()
它在 init() 之前調用,生成類實例並返回。我們可以覆蓋此方法來控制對象的創建過程。
- init()
這個不多解釋,相信你都明白。
如下是個例子:
class MultiBases(type):
def __new__(cls, clsname, bases, clsdict):
if len(bases)>1:
raise TypeError("Inherited multiple base classes!!!")
return super().__new__(cls, clsname, bases, clsdict)
class Base(metaclass=MultiBases):
pass
class A(Base):
pass
class B(Base):
pass
class C(A, B):
pass
執行結果是:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 8, in **new**
TypeError: Inherited multiple base classes!!!
第二種方法是直接使用 type() 函數創建類。這個方法如果只用一個參數調用,它會返回該參數的類型,前文已經描述過。但是使用三個參數調用時,它會創建一個類。這三個參數如下:
- 類名稱;
- 繼承的父類的元組。你沒看錯,是元組,別忘了 Python 可以多繼承;
- 一個字典。定義類屬性和方法;
以下是示例:
def test_method(self):
print("This is Test class method!")
class Base:
def myfun(self):
print("This is inherited method!")
Test = type('Test', (Base, ), dict(x="atul", my_method=test_method))
print("Type of Test class: ", type(Test))
test_obj = Test()
print("Type of test_obj: ", type(test_obj))
test_obj.myfun()
test_obj.my_method()
print(test_obj.x)
執行結果是:
Type of Test class: <class 'type'>
Type of test_obj: <class '**main**.Test'>
This is inherited method!
This is Test class method!
atul
使用元類解決問題
瞭解了元類的創建方法後,可以來解決一些實際問題了。例如,如果我們想在每次調用類方法時,都先輸出一下它的全限定名,該怎麼辦呢?
最常用的方法是使用 decorator
,象這樣:
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Full name of this method:", func.__qualname__)
return func(*args, **kwargs)
return wrapper
def debugmethods(cls):
for key, val in vars(cls).items():
if callable(val):
setattr(cls, key, debug(val))
return cls
@debugmethods
class Calc:
def add(self, x, y):
return x+y
def mul(self, x, y):
return x\*y
def div(self, x, y):
return x/y
mycal = Calc()
print(mycal.add(2, 3))
print(mycal.mul(5, 2))
執行結果是:
Full name of this method: Calc.add
5
Full name of this method: Calc.mul
10
這個方案很漂亮。但是,如果變更一下需求,例如我們希望 Calc 的所有子類的方法執行時,都先輸出一下它的全限定名,該怎麼辦呢?
在每一個子類上加上 @debugmethods 是一種方案,但是有點啰嗦,是不是?
該基於元類的解決方案出場了,以下是個例子:
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Full name of this method:", func.__qualname__)
return func(*args, **kwargs)
return wrapper
def debugmethods(cls):
for key, val in vars(cls).items():
if callable(val):
setattr(cls, key, debug(val))
return cls
class debugMeta(type):
def __new__(cls, clsname, bases, clsdict):
obj = super().__new__(cls, clsname, bases, clsdict)
obj = debugmethods(obj)
return obj
class Base(metaclass=debugMeta):
pass
class Calc(Base):
def add(self, x, y):
return x+y
class Calc_adv(Calc):
def mul(self, x, y):
return x\*y
mycal = Calc_adv()
print(mycal.mul(2, 3))
執行結果是:
Full name of this method: Calc_adv.mul
6
何時使用元類
該說的基本說完了,剩下最好一件事。元編程算是 Python 的一個魔法,多數時候我們其實用不到。但是什麼時候需要呢?大概有三種情況:
- 如果我們想要一個特性,沿著繼承層次結構向下傳遞,可以用;
- 如果我們想在類創建後,能動態修改,可以用;
- 如果我們是在開發類庫或者 API,可能會用到;
原創文章,轉載時請保留此聲明,並給出原文連接。