Python 使元編程成為可能,不過每個(gè)版本的 Python 都有一些細(xì)微的區(qū)別(并且不是完全兼容),這使我們實(shí)現(xiàn)元編程的道路變得更加崎嶇。一類函數(shù)對象的使用由來已久,同樣還有一些技術(shù)用于探索和實(shí)現(xiàn)魔術(shù)般的屬性。在版本 2.2 中,Python 增加了一種很有幫助的定制元類機(jī)制,但是其代價(jià)就是令用戶絞盡腦汁。最近,在 2.4 版本中,Python 增加了 “decorator” ,這是適于執(zhí)行大部分元編程的最新方式 —— 也是到目前為止對用戶最友好的方式。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
Decorator 與 Python 之前引入的元編程抽象有著某些共同之處:即使沒有這些技術(shù),您也一樣可以實(shí)現(xiàn)它們所提供的功能。正如 Michele Simionato 和我在 可愛的 Python 專欄的早期文章 中指出的那樣,即使在 Python 1.5 中,也可以實(shí)現(xiàn) Python 類的創(chuàng)建,而不需要使用 “元類” 掛鉤。
Decorator 根本上的平庸與之非常類似。Decorator 所實(shí)現(xiàn)的功能就是修改緊接 Decorator 之后定義的函數(shù)和方法。這總是可能的,但這種功能主要是由 Python 2.2 中引入的
classmethod()
和
staticmethod()
內(nèi)置函數(shù)驅(qū)動(dòng)的。在舊式風(fēng)格中,您可以調(diào)用
classmethod()
,如下所示:
清單 1. 典型的 “舊式” classmethod
class C:
def foo(cls, y):
print "classmethod", cls, y
foo = classmethod(foo)
|
雖然
classmethod()
是內(nèi)置函數(shù),但并無獨(dú)特之處;您也可以使用自己的方法轉(zhuǎn)換函數(shù)。例如:
清單 2. 典型的 “舊式” 方法的轉(zhuǎn)換
def enhanced(meth):
def new(self, y):
print "I am enhanced"
return meth(self, y)
return new
class C:
def bar(self, x):
print "some method says:", x
bar = enhanced(bar)
|
decorator 所做的一切就是使您避免重復(fù)使用方法名,并且將 decorator 放在方法定義中第一處提及其名稱的地方。例如:
清單 3. 典型的 “舊式” classmethod
class C:
@classmethod
def foo(cls, y):
print "classmethod", cls, y
@enhanced
def bar(self, x):
print "some method says:", x
|
decorator 也可以用于正則函數(shù),采用的是與類中的方法相同的方式。令人驚奇的是,這一切是如此簡單(嚴(yán)格來說,甚至有些不必要),只需要對語法進(jìn)行簡單修改,所有東西就可以工作得更好,并且使得程序的論證更加輕松。通過在方法定義的函數(shù)之前列出多個(gè) decorator,即可將 decorator 鏈接在一起;良好的判斷可以有助于防止將 過多 decorator 鏈接在一起,不過有時(shí)候?qū)讉€(gè) decorator 鏈接在一起是有意義的:
清單 4. 鏈接 decorator
@synchronized
@logging
def myfunc(arg1, arg2, ...):
# ...do something
# decorators are equivalent to ending with:
# myfunc = synchronized(logging(myfunc))
# Nested in that declaration order
|
Decorator 只是一個(gè)語法糖,如果您過于急切,那么它就會(huì)使您搬起石頭砸了自己的腳。decorator 其實(shí)就是一個(gè)至少具有一個(gè)參數(shù)的函數(shù) —— 程序員要負(fù)責(zé)確保 decorator 的返回內(nèi)容仍然是一個(gè)有意義的函數(shù)或方法,并且實(shí)現(xiàn)了原函數(shù)為使連接有用而做的事情。例如,下面就是 decorator 兩個(gè)不正確的用法:
清單 5. 沒有返回函數(shù)的錯(cuò)誤 decorator
>>> def spamdef(fn):
... print "spam, spam, spam"
...
>>> @spamdef
... def useful(a, b):
... print a**2 + b**2
...
spam, spam, spam
>>> useful(3, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: 'NoneType' object is not callable
|
decorator 可能會(huì)返回一個(gè)函數(shù),但這個(gè)函數(shù)與未修飾的函數(shù)之間不存在有意義的關(guān)聯(lián):
清單 6. 忽略傳入函數(shù)的 decorator
>>> def spamrun(fn):
... def sayspam(*args):
... print "spam, spam, spam"
... return sayspam
...
>>> @spamrun
... def useful(a, b):
... print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam
|
最后,一個(gè)表現(xiàn)更良好的 decorator 可以在某些方面增強(qiáng)或修改未修飾函數(shù)的操作:
清單 7. 修改未修飾函數(shù)行為的 decorator
>>> def addspam(fn):
... def new(*args):
... print "spam, spam, spam"
... return fn(*args)
... return new
...
>>> @addspam
... def useful(a, b):
... print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam
25
|
您可能會(huì)質(zhì)疑,
useful()
到底有多么有用?
addspam()
真的是那樣出色的
增強(qiáng)
嗎?但這種機(jī)制至少符合您通常能在有用的 decorator 中看到的那種模式。
|
|
|
根據(jù)我的經(jīng)驗(yàn),元類應(yīng)用最多的場合就是在類實(shí)例化之后對類中的方法進(jìn)行修改。decorator 目前并不允許您修改類實(shí)例化
本身
,但是它們可以修改依附于類的方法。這并不能讓您在實(shí)例化過程中動(dòng)態(tài)添加或刪除方法或類屬性,但是它讓這些方法可以在運(yùn)行時(shí)根據(jù)環(huán)境的條件來變更其行為。現(xiàn)在從技術(shù)上來說,decorator 是在運(yùn)行
class
語句時(shí)應(yīng)用的,對于頂級類來說,它更接近于 “編譯時(shí)” 而非 “運(yùn)行時(shí)”。但是安排 decorator 的運(yùn)行時(shí)決策與創(chuàng)建類工廠一樣簡單。例如:
清單 8. 健壯但卻深度嵌套的 decorator
def arg_sayer(what):
def what_sayer(meth):
def new(self, *args, **kws):
print what
return meth(self, *args, **kws)
return new
return what_sayer
def FooMaker(word):
class Foo(object):
@arg_sayer(word)
def say(self): pass
return Foo()
foo1 = FooMaker('this')
foo2 = FooMaker('that')
print type(foo1),; foo1.say() # prints: <class '__main__.Foo'> this
print type(foo2),; foo2.say() # prints: <class '__main__.Foo'> that
|
@arg_sayer()
繞了很多彎路,但只獲得非常有限的結(jié)果,不過對于它所闡明的幾方面來說,這是值得的:
-
Foo.say()方法對于不同的實(shí)例有不同的行為。在這個(gè)例子中,不同之處只是一個(gè)數(shù)據(jù)值,可以輕松地通過其他方式改變這個(gè)值;不過原則上來說,decorator 可以根據(jù)運(yùn)行時(shí)的決策來徹底重寫這個(gè)方法。
-
本例中未修飾的
Foo.say()方法是一個(gè)簡單的占位符,其整個(gè)行為都是由 decorator 決定的。然而,在其他情況下,decorator 可能會(huì)將未修飾的方法與一些新功能相 結(jié)合 。
-
正如我們已經(jīng)看到的一樣,
Foo.say()的修改是通過FooMaker()類工廠在運(yùn)行時(shí)嚴(yán)格確定的。可能更加典型的情況是在頂級定義類中使用 decorator,這些類只依賴于編譯時(shí)可用的條件(這通常就足夠了)。
-
decorator 都是參數(shù)化的。或者更確切地說,
arg_sayer()本身根本就不是一個(gè)真正的 decorator;arg_sayer()所返回的 函數(shù) ——what_sayer()就是一個(gè)使用了閉包來封裝其數(shù)據(jù)的 decorator 函數(shù)。參數(shù)化的 decorator 較為常見,但是它們將所需的函數(shù)嵌套為三層。
|
|
|
正如上一節(jié)中介紹的一樣,decorator 并不能完全取代元類掛鉤,因?yàn)樗鼈冎恍薷牧朔椒ǎ刺砑踊騽h除方法。實(shí)際上,這樣說并不完全正確。作為一個(gè) Python 函數(shù),decorator 完全可以實(shí)現(xiàn)其他 Python 代碼所實(shí)現(xiàn)的任何功能。通過修飾一個(gè)類的
.__new__()
方法(甚至是其占位符版本),您實(shí)際上可以更改附加到該類的方法。盡管尚未在現(xiàn)實(shí)中看到這種模式,不過我認(rèn)為它有著某種必然性,甚至可以作為
_metaclass_
指派的一項(xiàng)改進(jìn):
清單 9. 添加和刪除方法的 decorator
def flaz(self): return 'flaz' # Silly utility method
def flam(self): return 'flam' # Another silly method
def change_methods(new):
"Warning: Only decorate the __new__() method with this decorator"
if new.__name__ != '__new__':
return new # Return an unchanged method
def __new__(cls, *args, **kws):
cls.flaz = flaz
cls.flam = flam
if hasattr(cls, 'say'): del cls.say
return super(cls.__class__, cls).__new__(cls, *args, **kws)
return __new__
class Foo(object):
@change_methods
def __new__(): pass
def say(self): print "Hi me:", self
foo = Foo()
print foo.flaz() # prints: flaz
foo.say() # AttributeError: 'Foo' object has no attribute 'say'
|
在
change_methods()
decorator 示例中,我們添加并刪除了幾個(gè)固定的方法,不過這是毫無意義的。在更現(xiàn)實(shí)的情況中,應(yīng)使用上一節(jié)中提到的幾個(gè)模式。例如,參數(shù)化的 decorator 可以接受一個(gè)能表示要添加或刪除的方法的數(shù)據(jù)結(jié)構(gòu);或者由數(shù)據(jù)庫查詢之類的某些環(huán)境特性做出這一決策。這種對附加方法的操作也可以像之前一樣打包到一個(gè)函數(shù)工廠中,這將使最終決策延遲到運(yùn)行時(shí)。這些新興技術(shù)也許比
_metaclass_
指派更加萬能。例如,您可以調(diào)用一個(gè)增強(qiáng)了的
change_methods()
,如下所示:
清單 10. 增強(qiáng)的 change_methods()
class Foo(object):
@change_methods(add=(foo, bar, baz), remove=(fliz, flam))
def __new__(): pass
|
|
|
|
您將看到,有關(guān) decorator 的最典型的例子可能是使一個(gè)函數(shù)或方法來實(shí)現(xiàn) “其他功能”,同時(shí)完成其基本工作。例如,在諸如 Python Cookbook Web 站點(diǎn)(請參見
參考資料
中的鏈接)之類的地方,您可以看到 decorator 添加了諸如跟蹤、日志記錄、存儲(chǔ)/緩存、線程鎖定以及輸出重定向之類的功能。與這些修改相關(guān)(但實(shí)質(zhì)略有區(qū)別)的是修飾 “之前” 和 “之后”。對于修飾之前/之后來說,一種有趣的可能性就是檢查傳遞給函數(shù)的參數(shù)和函數(shù)返回值的類型。如果這些類型并非如我們預(yù)期的一樣,那么這種
type_check()
decorator 就可能會(huì)觸發(fā)一個(gè)異常,或者采取一些糾正操作。
與這種 decorator 前/后類似的情況,我想到了 R 編程語言和
NumPy
特有的函數(shù)的 “elementwise” 應(yīng)用。在這些語言中,數(shù)學(xué)函數(shù)通常應(yīng)用于元素序列中的
每個(gè)元素
,但也會(huì)應(yīng)用于單個(gè)數(shù)字。
當(dāng)然,
map()
函數(shù)、列表內(nèi)涵(list-comprehension)和最近的生成器內(nèi)涵(generator-comprehension 都可以讓您實(shí)現(xiàn) elementwise 應(yīng)用。但是這需要較小的工作區(qū)來獲得類似于 R 語言的行為:
map()
所返回的序列類型通常是一個(gè)列表;如果您傳遞的是單個(gè)元素而不是一個(gè)序列,那么調(diào)用將失敗。例如:
清單 11. map() 調(diào)用失敗
>>> from math import sqrt
>>> map(sqrt, (4, 16, 25))
[2.0, 4.0, 5.0]
>>> map(sqrt, 144)
TypeError: argument 2 to map() must support iteration
|
創(chuàng)建一個(gè)可以 “增強(qiáng)” 普通數(shù)值函數(shù)的 decorator 并不困難:
清單 12. 將函數(shù)轉(zhuǎn)換成 elementwise 函數(shù)
def elementwise(fn):
def newfn(arg):
if hasattr(arg,'__getitem__'): # is a Sequence
return type(arg)(map(fn, arg))
else:
return fn(arg)
return newfn
@elementwise
def compute(x):
return x**3 - 1
print compute(5) # prints: 124
print compute([1,2,3]) # prints: [0, 7, 26]
print compute((1,2,3)) # prints: (0, 7, 26)
|
當(dāng)然,簡單地編寫一個(gè)具有不同返回類型的
compute()
函數(shù)并不困難;畢竟 decorator 只需占據(jù)幾行。但是作為對面向方面編程的一種認(rèn)可,這個(gè)例子讓我們可以
分離
那些在不同層次上運(yùn)作的關(guān)注事項(xiàng)。我們可以編寫各種數(shù)值計(jì)算函數(shù),希望它們都可轉(zhuǎn)換成 elementwise 調(diào)用模型,而不用考慮參數(shù)類型測試和返回值類型強(qiáng)制轉(zhuǎn)換的細(xì)節(jié)。
對于那些對單個(gè)事物或事物序列(此時(shí)要保留序列類型)進(jìn)行操作的函數(shù)來說,
elementwise()
decorator 均可同樣出色地發(fā)揮作用。作為一個(gè)練習(xí),您可嘗試去解決如何允許相同的修飾后調(diào)用來接受和返回迭代器(提示:如果您只是想迭代一次完整的 elementwise 計(jì)算,那么當(dāng)且僅當(dāng)傳入的是一個(gè)迭代對象時(shí),才能這樣簡化一些。)
您將碰到的大多數(shù)優(yōu)秀的 decorator 都在很大程度上采用了這種組合正交關(guān)注的范例。傳統(tǒng)的面向?qū)ο缶幊蹋绕涫窃谥T如 Python 之類允許多重繼承的語言中,都會(huì)試圖使用一個(gè)繼承層次結(jié)構(gòu)來模塊化關(guān)注事項(xiàng)。然而,這僅會(huì)從一個(gè)祖先那里獲取一些方法,而從其他祖先那里獲取其他方法,因此需要采用一種概念,使關(guān)注事項(xiàng)比在面向方面的思想中更加分散。要充分利用生成器,就要考慮一些與混搭方法不同的問題:可以處于方法本身的 “核心” 之外的關(guān)注事項(xiàng)為依據(jù),使 各 方法以不同方式工作。
|
|
|
|
|
|
在結(jié)束本文之前,我想為您介紹一種確實(shí)非常出色的 Python 模塊,名為
decorator
,它是由與我合著過一些圖書的 Michele Simionato 編寫的。該模塊使 decorator 的開發(fā)變得更加美妙。
decorator
模塊的主要組件具有某種自反式的優(yōu)雅,它是一個(gè)稱為
decorator()
的 decorator。與未修飾的函數(shù)相比,使用
@decorator
修飾過的函數(shù)可以通過一種更簡單的方式編寫。(相關(guān)資料請參看
參考資料
)。
Michele 已經(jīng)為自己的模塊編寫了很好的文檔,因此這里不再贅述;不過我非常樂意介紹一下它所解決的基本問題。
decorator
模塊有兩大主要優(yōu)勢。一方面,它使您可以編寫出嵌套層次更少的 decorator,如果沒有這個(gè)模塊,您就只能使用更多層次(“平面優(yōu)于嵌套”);但更加有趣的是這樣一個(gè)事實(shí):它使得修飾過的函數(shù)可以真正地與其在元數(shù)據(jù)中未修飾的版本相匹配,這是我的例子中沒有做到的。例如,回想一下我們上面使用過的簡單 “跟蹤” decorator
addspam()
:
清單 13. 一個(gè)簡單的 decorator 是如何造成元數(shù)據(jù)崩潰的
>>> def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> from inspect import getargspec
>>> getargspec(useful)
(['a', 'b'], None, None, None)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'new'
>>> getargspec(useful)
([], 'args', None, None)
|
盡管這個(gè)修飾過的函數(shù)
的確完成
了自己增強(qiáng)過的工作,但若進(jìn)一步了解,就會(huì)發(fā)現(xiàn)這并不是完全正確的,尤其是對于那些關(guān)心這種細(xì)節(jié)的代碼分析工具或 IDE 來說更是如此。使用
decorator
,我們就可以改進(jìn)這些問題:
清單 14. decorator 更聰明的用法
>>> from decorator import decorator
>>> @decorator
... def addspam(f, *args, **kws):
... print "spam, spam, spam"
... return f(*args, **kws)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> getargspec(useful)
(['a', 'b'], None, None, None)
|
這對于編寫 decorator 更加有利,同時(shí),其保留行為的元數(shù)據(jù)的也更出色了。當(dāng)然,閱讀 Michele 開發(fā)這個(gè)模塊所使用的全部資料會(huì)使您回到大腦混沌的世界,我們將這留給 Simionato 博士一樣的宇宙學(xué)家好了。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

