方法修飾器


如果你要設計一個方法修飾器,則要注意的是,實例會作為方法的第一個引數傳入,所以你的方法修飾器,必須考慮到第一個參數是實例。例如:
def log(mth):
def wrapper(self, a, b):
print(self, a, b)
return mth(self, a, b)
return wrapper

class Some:
@log
def doIt(self, a, b):
return a + b

s = Some()
print(s.doIt(1, 2))

wrapper的第一個self參數在上例中不可以省略,因為要用來接受實例。你可以將上例設計的更通用一些,不僅可以接受兩個引數。例如:
def log(mth):
def wrapper(self, *args, **kwargs):
print(self, args, kwargs)
return mth(self, *args, **kwargs)
return wrapper

class Some:
@log
def doIt(self, a, b):
return a + b

s = Some()
print(s.doIt(1, 2))

這是方法修飾器的基本作法,現在考慮你想實作出 靜態方法、類別方法staticmethod與classmethod的功能。你需要額外的資訊,而不只是要被修飾的函式或方法本身的資訊,才可以實作出staticmethod與classmethod的功能,你可以搭配 描述器 來實作。例如實作staticmth來模擬staticmethod
class staticmth:
def __init__(self, mth):
self.mth = mth
 
def __get__(self, instance, owner):
return self.mth

class Some:
@staticmth # 相當於 doIt = staticmth(doIt)
def doIt(a, b):
print(a, b)

Some.doIt(1, 2) # 相當於 Some.__dict__['doIt'].__get__(None, Some)(1, 2)
s = Some()

# 以下相當於 Some.__dict__['doIt'].__get__(s, Some)(1)
# 所以以下會有錯 TypeError: doIt() takes exactly 2 positional arguments ..
s.doIt(1)

staticmth的__get__()僅傳回原本Some.doIt所參考的函式物件,而不會作為實例綁定方法,若打算將之作為實例方法使用,則第一個參數並不會被綁定為實例。

如果要實作classmethod的功能,則可以如下:
class classmth:
def __init__(self, mth):
self.mth = mth

def __get__(self, instance, owner):
def wrapper(*arg, **kwargs):
return self.mth(owner, *arg, **kwargs)
return wrapper

class Other:
@classmth # 相當於 doIt = classmth(doIt)
def doIt(clz, a, b):
print(clz, a, b)

Other.doIt(1, 2) # 相當 Other.__dict__['doIt'].__get__(None, Other)(1, 2)
o = Other()
o.doIt(1, 2) # 相當 Other.__dict__['doIt'].__get__(o, Other)(1, 2)

由於描述器協定中,__get__()的第三個參數總是接受類別實例,所以可以具此指定給原類別中的方法第一個參數,因此實作出classmethod的功能。

以上以實作staticmethod與classmethod的功能作為示範,總之,以方法修飾器的原理為出發點,結合各種物件協定,可以實作出更多不同的功能。