Python 的闭包和装饰器
翻譯:?TheLover_Z
Part I
原文地址:?http://blaag.haard.se/Python-Closures-and-Decorators–Pt–1/
回想起來,當初我做出了錯誤的選擇,把 Python 的課程削減到了4個小時以至于把裝飾器的部分搞砸了,我答應大家我稍后會對閉包和裝飾器做一個更好的解說 —— 我是這么打算的。
函數也是對象。實際上,在 Python 中函數是一級對象——也就是說,他們可以像其他對象一樣使用而沒有什么特別的限制。這給了我們一些有趣的選擇,我會由淺到深解釋這個問題。
關于函數就是對象的一個最常見的例子就是 C 中的函數指針;將函數傳遞到其他的將要使用它的函數。為了說明這一點,我們來看看一個重復函數的實現 —— 也就是,一個函數接受另外一個函數以及一個數字當作參數,并且重復調用指定函數指定次數:
>>> #A very simple function >>> def greeter(): … print("Hello") … >>> #An implementation of a repeat function >>> def repeat(fn, times): … for i in range(times): … fn() … >>> repeat(greeter, 3) Hello Hello Hello >>>這種模式在很多情況下都有用 —— 比如向一個排序算法傳遞比較函數,向一個語法分析器傳遞一個裝飾器函數,通常情況下這些做法可以使一個函數的行為?更專一化?,或者向已經抽象了工作流的函數傳遞一個待辦的特定部分(比如,?sort()?知道怎么排序,?compare()?知道怎么比較元素)。
函數也可以在其他函數的內部聲明,這給了我們另一個很重要的工具。在一般情況下,這可以用來隱藏實用函數的實現細節:
>>> def print_integers(values): … def is_integer(value): … try: … return value == int(value) … except: … return False … for v in values: … if is_integer(v): … print(v) … >>> print_integers([1,2,3,"4", "parrot", 3.14]) 1 2 3這可能是有用的,但它本身并不算是個強大的工具。相比函數可以當作參數被傳遞而言,我們可以將它們包裝(wrap)在另外的函數中,從而向已經構建好的函數增加新的行為。一個簡單的例子是向一個函數增加跟蹤輸出:
>>> def print_call(fn): … def fn_wrap(*args, **args): #take any arguments … print ("Calling %s" % (fn.func_name)) … return fn(*args, **kwargs) #pass any arguments to fn() … return fn_wrap … >>> greeter = print_call(greeter) #wrap greeter >>> repeat(greeter, 3) Calling fn_wrap Hello Calling fn_wrap Hello Calling fn_wrap Hello >>> >>> greeter.func_name 'fn_wrap'正如你看到的那樣,我們可以使用帶日志的函數來替換掉現有函數相應的部分,然后調用原來的函數。在例子的最后兩行,函數的名字已經反映出了它已經被改變,這個改變可能是我們想要的,也可能不是。如果我們想包裝一個函數同時保持它原來的名字,我們可以增加一行?print_call?函數,代碼如下:
>>> def print_call(fn): … def fn_wrap(*args, **kwargs): #take any arguments … print("Calling %s" % (fn.func_name)) … return fn(*args, **kwargs) #pass any arguments to fn() … fn_wrap.func_name = fn.func_name #Copy the original name … return fn_wrap因為這是一個很長的話題,我明天會來更新第二部分,我們會講講閉包,偏函數(partial),還有(終于到它了)裝飾器。
至此,如果這些你之前全部沒有接觸過,可以先用?print_call?函數作為基礎,來創建一個能夠在正常調用函數之前先打印出這個函數名字的一個修飾器。
Part II
原文地址:?http://blaag.haard.se/Python-Closures-and-Decorators–Pt–2/
在第一部分中,我們學習了以函數作為參數調用其他的函數,還有嵌套函數,最終我們把一個函數包裝在另外的函數中。我們先把第一部分的答案給出:
>>> def print_call(fn): … def fn_wrap(*args, **kwargs): … print("Calling %s with arguments: \n\targs: %s\n\tkwargs:%s" %fn.__name__, args, kwargs)) … retval = fn(*args, **kwargs) … print("%s returning '%s'" % (fn.func_name, retval)) … return retval … fn_wrap.func_name = fn.func_name … return fn_wrap … >>> def greeter(greeting, what='world'): … return "%s %s!" % (greeting, what) … >>> greeter = print_call(greeter) >>> greeter("Hi") Calling greeter with arguments: args: ('Hi',) kwargs:{} greeter returning 'Hi world!' 'Hi world!' >>> greeter("Hi", what="Python") Calling greeter with arguments: args: ('Hi',) kwargs:{'what': 'Python'} greeter returning 'Hi Python!' 'Hi Python!' >>>這稍微有那么點兒用了,但它可以變的更好!你可能聽說過或者沒有聽說過*閉包*,你可能聽說過成千上萬種閉包定義中的某一種或者某幾種 —— 我不會那么挑剔,我只是說閉包就是一個捕捉了(或者關閉)非本地變量(自由變量)的代碼塊(比如一個函數)。如果你不清楚我在說什么,你可能需要進修一下 CS 的相關課程,但是不要擔心 —— 我會給你演示例子。閉包的概念很簡單:一個可以引用在函數閉合范圍內變量的函數。
比如說,看一下這個代碼:
>>> a = 0 >>> def get_a(): … return a … >>> get_a() 0 >>> a = 3 >>> get_a() 3正如你看到的那樣,?get_a?函數可以取得?a?的值,并且可以讀取更新后的值。然而這里有一個限制 —— 被捕獲的變量(captured variable,下同)不能被寫入。
>>> def set_a(val): … a = val … >>> set_a(4) >>> a 3為什么會這樣?由于閉包不能寫入任何被捕獲的變量,?a?=?val?這個語句實際上寫入了本地變量?a?從而隱藏了模塊級別的?a?,這正是我們想寫入的內容。為了解決這個限制(也許這并不是一個好主意),我們可以用一個容器類型:
>>> class A(object): pass … >>> a = A() >>> a.value = 1 >>> def set_a(val): … a.value = val … >>> a.value 1 >>> set_a(5) >>> a.value 5因此,我們已經知道了函數從它的閉合范圍內捕捉變量,我們最終可以接觸到有趣的東西了,我們先實現一個偏函數(partial,下同)。一個偏函數是一個你已經填充了部分或者全部參數的函數的實例;比如說你有一個存儲了用戶名和密碼的會話,和一個查詢后端的函數,這個函數有不同的參數但是*總是*需要身份驗證。與其說每次都手動傳遞身份驗證信息,我們可以用偏函數來預填充那些信息。
>>> #Our 'backend' function … def get_stuff(user, pw, stuff_id): … """Here we would presumably fetch data using the … credentials and id""" … print("get_stuff called with user: %s, pw: %s, stuff_id: %s" % (user, pw, stuff_id)) >>> def partial(fn, *args, **kwargs): … def fn_part(*fn_args, **fn_kwargs): … kwargs.update(fn_kwargs) … return fn(*args + fn_args, **kwargs) … return fn_part … >>> my_stuff = partial(get_stuff, 'myuser', 'mypwd') >>> my_stuff(3) get_stuff called with user: myuser, pw: mypwd, stuff_id: 3 >>> my_stuff(67) get_stuff called with user: myuser, pw: mypwd, stuff_id: 67偏函數可以用在許多地方來消除代碼的重復。當然,你沒有必要自己手動實現它,只需要?from?functools?import?partial?就可以了。
最后,我們來看看函數裝飾器(未來可能有類裝飾器)。函數裝飾器接收一個函數作為參數然后返回一個新的函數。聽起來很熟悉吧?我們已經實現過一個?print_call?裝飾器了。
>>> @print_call … def will_be_logged(arg): … return arg*5 … >>> will_be_logged("!") Calling will_be_logged with arguments: args: ('!',) kwargs:{} will_be_logged returning '!!!!!' '!!!!!'使用@符號標記是一個很方便的方法。
>>> def will_be_logged(arg): … return arg*5 … >>> will_be_logged = print_call(will_be_logged)但是如果我們想要確定裝飾器的參數呢?在這種情況下,作為裝飾器的函數會接收參數,并且返回一個包裝(wrap)了裝飾器函數的函數。
>>> def require(role): … def wrapper(fn): … def new_fn(*args, **kwargs): … if not role in kwargs.get('roles', []): … print("%s not in %s" % (role, kwargs.get('roles', []))) … raise Exception("Unauthorized") … return fn(*args, **kwargs) … return new_fn … return wrapper … >>> @require('admin') … def get_users(**kwargs): … return ('Alice', 'Bob') … >>> get_users() admin not in [] Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 7, in new_fn Exception: Unauthorized >>> get_users(roles=['user', 'editor']) admin not in ['user', 'editor'] Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 7, in new_fn Exception: Unauthorized >>> get_users(roles=['user', 'admin']) ('Alice', 'Bob')就是這樣。你現在會寫裝飾器了,也許你會用這些知識去寫面向方面(aspect-oriented)的編程。加入?@cache,?@trace,?@throttle?都是微不足道的(在你添加?@cache?之前,一定要檢查?functools?,如果你用的是 Python 3 的話!)
from:?http://pycoders-weekly-chinese.readthedocs.io/en/latest/issue3/python-closures-and-decorators.html
總結
以上是生活随笔為你收集整理的Python 的闭包和装饰器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何过渡至 Python 3
- 下一篇: vi/vim使用入门: vimrc在哪儿