《Python高階特性(1):Iterators、Generators和itertools》
裝飾器(Decorators)
裝飾器是這樣一種設計模式:如果一個類希望新增其他類的一些功能,而不希望通過繼承或是直接修改原始碼實現,那麼可以使用裝飾器模式。簡單來說Python中的裝飾器就是指某些函式或其他可呼叫物件,以函式或類作為可選輸入引數,然後返回函式或類的形式。通過這個在Python2.6版本中被新加入的特性可以用來實現裝飾器設計模式。
順便提一句,在繼續閱讀之前,如果你對Python中的閉包(Closure)概念不清楚,請檢視本文結尾後的附錄,如果沒有閉包的相關概念,很難恰當的理解Python中的裝飾器。
在Python中,裝飾器被用於用@語法糖修辭的函式或類。現在讓我們用一個簡單的裝飾器例子來演示如何做一個函式呼叫日誌記錄器。在這個例子中,裝飾器將時間格式作為輸入引數,在呼叫被這個裝飾器裝飾的函式時列印出函式呼叫的時間。這個裝飾器當你需要手動比較兩個不同演算法或實現的效率時很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def logged(time_format): def decorator(func): def decorated_func(*args, **kwargs): print "- Running '%s' on %s " % ( func.__name__, time.strftime(time_format) ) start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print "- Finished '%s', execution time = %0.3fs " % ( func.__name__, end_time - start_time ) return result decorated_func.__name__ = func.__name__ return decorated_func return decorator |
來看一個例子,在這裡add1和add2函式被logged修飾,下面給出了一個輸出示例。請注意在這裡時間格式引數是儲存在被返回的裝飾器函式中(decorated_func)。這就是為什麼理解閉包對於理解裝飾器來說很重要的原因。同樣也請注意返回函式的名字是如何被替換為原函式名的,以防萬一如果它還要被使用到,這是為了防止混淆。Python預設可不會這麼做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@logged("%b %d %Y - %H:%M:%S") def add1(x, y): time.sleep(1) return x + y @logged("%b %d %Y - %H:%M:%S") def add2(x, y): time.sleep(2) return x + y print add1(1, 2) print add2(1, 2) # Output: - Running 'add1' on Jul 24 2013 - 13:40:47 - Finished 'add1', execution time = 1.001s 3 - Running 'add2' on Jul 24 2013 - 13:40:48 - Finished 'add2', execution time = 2.001s 3 |
如果你足夠細心,你可能會注意到我們對於返回函式的名字__name__有著特別的處理,但對其他的注入__doc__或是__module__則沒有如此。所以如果,在這個例子中add函式有一個doc字串的話,它就會被丟棄。那麼該如何處理呢?我們當然可以像處理__name__那樣對待所有的欄位,不過如果在每個裝飾器內都這麼做的話未免太繁冗了。這就是為何functools模組提供了一個名為wraps的裝飾器的原因,那正是為了處理這種情況。可能在理解裝飾器的過程中會被迷惑,不過當你把裝飾器看成是一個接收函式名作為輸入引數並且返回一個函式,這樣就很好理解了。我們將在下個例子中使用wraps裝飾器而不是手動去處理__name__或其他屬性。
下個例子會有點複雜,我們的任務是將一個函式呼叫的返回結果快取一段時間,輸入引數決定快取時間。傳遞給函式的輸入引數必須是可雜湊的物件,因為我們使用包含呼叫輸入引數的tuple作為第一個引數,第二個引數則為一個frozenset物件,它包含了關鍵詞項kwargs,並且作為cache key。每個函式都會有一個唯一的cache字典儲存在函式的閉包內。
【譯註】set和frozenset為Python的兩種內建集合,其中前者為可變物件(mutable),其元素可以使用add()或remove()進行變更,而後者為不可變物件(imutable)並且是可雜湊的(hashable),在建立之後元素不可變,他可以作為字典的key或是另一個集合的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import time from functools import wraps def cached(timeout, logged=False): """Decorator to cache the result of a function call. Cache expires after timeout seconds. """ def decorator(func): if logged: print "-- Initializing cache for", func.__name__ cache = {} @wraps(func) def decorated_function(*args, **kwargs): if logged: print "-- Called function", func.__name__ key = (args, frozenset(kwargs.items())) result = None if key in cache: if logged: print "-- Cache hit for", func.__name__, key (cache_hit, expiry) = cache[key] if time.time() - expiry < timeout: result = cache_hit elif logged: print "-- Cache expired for", func.__name__, key elif logged: print "-- Cache miss for", func.__name__, key # No cache hit, or expired if result is None: result = func(*args, **kwargs) cache[key] = (result, time.time()) return result return decorated_function return decorator |
來看看它的用法。我們使用裝飾器裝飾一個很基本的斐波拉契數生成器。這個cache裝飾器將對程式碼使用備忘錄模式(Memoize Pattern)。請注意fib函式的閉包是如何存放cache字典、一個指向原fib函式的引用、logged引數的值以及timeout引數的最後值的。dump_closure將在文末定義。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
>>> @cached(10, True) ... def fib(n): ... """Returns the n'th Fibonacci number.""" ... if n == 0 or n == 1: ... return 1 ... return fib(n - 1) + fib(n - 2) ... -- Initializing cache for fib >>> dump_closure(fib) 1. Dumping function closure for fib: -- cell 0 = {} -- cell 1 = -- cell 2 = True -- cell 3 = 10 >>> >>> print "Testing - F(4) = %d" % fib(4) -- Called function fib -- Cache miss for fib ((4,), frozenset([])) -- Called function fib -- Cache miss for fib ((3,), frozenset([])) -- Called function fib -- Cache miss for fib ((2,), frozenset([])) -- Called function fib -- Cache miss for fib ((1,), frozenset([])) -- Called function fib -- Cache miss for fib ((0,), frozenset([])) -- Called function fib -- Cache hit for fib ((1,), frozenset([])) -- Called function fib -- Cache hit for fib ((2,), frozenset([])) Testing - F(4) = 5 |
Class Decorators
在之前的小節中,我們看了一些函式裝飾器和一些使用的小技巧,接下來我們來看看類裝飾器。類裝飾器將一個class作為輸入引數(Python中的一種類型別物件),並且返回一個修改過的class。
第一個例子是一個簡單的數學問題。當給定一個有序集合P,我們定義Pd為P的反序集合P(x,y) <-> Pd(x,y),也就是說兩個有序集合的元素順序互為相反的,這在Python中該如何實現?假定一個類定義了__lt__以及__le__或其他方法來實現有序。那麼我們可以通過寫一個類裝飾器來替換這些方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
def make_dual(relation): @wraps(relation, ['__name__', '__doc__']) def dual(x, y): return relation(y, x) return dual def dual_ordering(cls): """Class decorator that reverses all the orderings""" for func in ['__lt__', '__gt__', '__ge__', '__le__']: if hasattr(cls, func): setattr(cls, func, make_dual(getattr(cls, func))) return cls |
下面是將這個裝飾器用以str型別的例子,建立一個名為rstr的新類,使用反字典序(opposite lexicographic)為其順序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@dual_ordering class rstr(str): pass x = rstr("1") y = rstr("2") print x < y print x <= y print x > y print x >= y # Output: False False True True |
來看一個更復雜的例子。假定我們希望前面所說的logged裝飾器能夠被用於某個類的所有方法。一個方案是在每個類方法上都加上裝飾器。另一個方案是寫一個類裝飾器自動完成這些工作。在動手之前,我將把前例中的logged裝飾器拿出來做一些小改進。首先,它使用functools提供的wraps裝飾器完成固定__name__的工作。第二,一個_logged_decorator屬性被引入(設定為True的布林型變數),用來指示這個方法是否已經被裝飾器裝飾過,因為這個類可能會被繼承而子類也許會繼續使用裝飾器。最後,name_prefix引數被加入用來設定列印的日誌資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
def logged(time_format, name_prefix=""): def decorator(func): if hasattr(func, '_logged_decorator') and func._logged_decorator: return func @wraps(func) def decorated_func(*args, **kwargs): start_time = time.time() print "- Running '%s' on %s " % ( name_prefix + func.__name__, time.strftime(time_format) ) result = func(*args, **kwargs) end_time = time.time() print "- Finished '%s', execution time = %0.3fs " % ( name_prefix + func.__name__, end_time - start_time ) return result decorated_func._logged_decorator = True return decorated_func return decorator |
好的,讓我們開始寫類裝飾器:
1 2 3 4 5 6 7 8 9 10 11 |
def log_method_calls(time_format): def decorator(cls): for o in dir(cls): if o.startswith('__'): continue a = getattr(cls, o) if hasattr(a, '__call__'): decorated_a = logged(time_format, cls.__name__ + ".")(a) setattr(cls, o, decorated_a) return cls return decorator |
下面是使用方法,注意被繼承的或被重寫的方法是如何處理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@log_method_calls("%b %d %Y - %H:%M:%S") class A(object): def test1(self): print "test1" @log_method_calls("%b %d %Y - %H:%M:%S") class B(A): def test1(self): super(B, self).test1() print "child test1" def test2(self): print "test2" b = B() b.test1() b.test2() # Output: - Running 'B.test1' on Jul 24 2013 - 14:15:03 - Running 'A.test1' on Jul 24 2013 - 14:15:03 test1 - Finished 'A.test1', execution time = 0.000s child test1 - Finished 'B.test1', execution time = 1.001s - Running 'B.test2' on Jul 24 2013 - 14:15:04 test2 - Finished 'B.test2', execution time = 2.001s |
我們第一個類裝飾器的例子是類的反序方法。一個相似的裝飾器,可以說是相當有用的,實現__lt__、__le__、__gt__、__ge__和__eq__中的一個,能夠實現類的全排序麼?這也就是functools.total_ordering裝飾器所做的工作。詳情請見參考文件。
Flask中的一些例子
讓我們來看看Flask中用到的一些有趣的裝飾器。
假定你希望讓某些函式在特定的呼叫時刻輸出警告資訊,例如僅僅在debug模式下。而你又不希望每個函式都加入控制的程式碼,那麼你就能夠使用裝飾器來實現。以下就是Flask的app.py中定義的裝飾器的工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def setupmethod(f): """Wraps a method so that it performs a check in debug mode if the first request was already handled. """ def wrapper_func(self, *args, **kwargs): if self.debug and self._got_first_request: raise AssertionError('A setup function was called after the ' 'first request was handled. This usually indicates a bug ' 'in the application where a module was not imported ' 'and decorators or other functionality was called too late.\n' 'To fix this make sure to import all your view modules, ' 'database models and everything related at a central place ' 'before the application starts serving requests.') return f(self, *args, **kwargs) return update_wrapper(wrapper_func, f) |
來看一個更有趣的例子,這個例子是Flask的route裝飾器,在Flask類中定義。注意到裝飾器可以是類中的一個方法,將self作為第一個引數。完整的程式碼在app.py中。請注意裝飾器簡單的將被裝飾過的函式註冊成為一個URL控制程式碼,這是通過呼叫add_url_rule函式來實現的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def route(self, rule, **options): """A decorator that is used to register a view function for a given URL rule. This does the same thing as :meth:`add_url_rule` but is intended for decorator usage:: @app.route('/') def index(): return 'Hello World' For more information refer to :ref:`url-route-registrations`. :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as endpoint :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. A change to Werkzeug is handling of method options. methods is a list of methods this rule should be limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly `HEAD`). Starting with Flask 0.6, `OPTIONS` is implicitly added and handled by the standard request handling. """ def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator |
擴充套件閱讀
2. metaprogramming in Python 3
附錄:閉包
一個函式閉包是一個函式和一個引用集合的組合,這個引用集合指向這個函式被定義的作用域的變數。後者通常指向一個引用環境(referencing environment),這使得函式能夠在它被定義的區域之外執行。在Python中,這個引用環境被儲存在一個cell的tuple中。你能夠通過func_closure或Python 3中的__closure__屬性訪問它。要銘記的一點是引用及是引用,而不是物件的深度拷貝。當然了,對於不可變物件而言,這並不是問題,然而對可變物件(list)這點就必須注意,隨後會有一個例子說明。請注意函式在定義的地方也有__globals__欄位來儲存全域性引用環境。
來看一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
>>> def return_func_that_prints_s(s): ... def f(): ... print s ... return f ... >>> g = return_func_that_prints_s("Hello") >>> h = return_func_that_prints_s("World") >>> g() Hello >>> h() World >>> g is h False >>> h.__closure__ (,) >>> print [str(c.cell_contents) for c in g.__closure__] ['Hello'] >>> print [str(c.cell_contents) for c in h.__closure__] ['World'] |
一個稍複雜的例子。確保明白為什麼會這麼執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> def return_func_that_prints_list(z): ... def f(): ... print z ... return f ... >>> z = [1, 2] >>> g = return_func_that_prints_list(z) >>> g() [1, 2] >>> z.append(3) >>> g() [1, 2, 3] >>> z = [1] >>> g() [1, 2, 3] |
【譯者】:z.append(3)時,g()內部的引用和z仍然指向一個變數,而z=[1]之後,兩者就不再指向一個變數了。
最後,來看看程式碼中使用到的dump_closure方法的定義。
1 2 3 4 5 6 7 |
def dump_closure(f): if hasattr(f, "__closure__") and f.__closure__ is not None: print "- Dumping function closure for %s:" % f.__name__ for i, c in enumerate(f.__closure__): print "-- cell %d = %s" % (i, c.cell_contents) else: print " - %s has no closure!" % f.__name__ |