Python 的閉包和裝飾器

garfielder007發表於2016-05-05

翻譯: 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

相關文章