最近在學習python,以下是在Python學習小組上介紹的內容,現學現賣、多練習是好的學習方式,希望大家能夠喜歡
Python有大量強大又貼心的特性,如果要列出最受歡迎排行榜,那麼裝飾器絕對會在其中。
初識裝飾器,會感覺到優雅又神奇,想親手實現時卻總有距離感,就像深閨的冰美人一般。這往往是因為理解裝飾器時把其他的一些概念混雜在一起了。待我撫去層次面紗,你會看到純粹的裝飾器其實蠻簡單直率的。
裝飾器的原理
在解釋器下方跑裝飾器的例子,直覺地感受一下。
# make_bold就是裝飾器,實作方式這裡略去
>>> @make_bold ... def get_content(): ... return 'hello world' ... >>> get_content() '<b>hello world</b>'
被make_bold 裝飾的get_content ,呼叫後回傳結果會自動被b 標籤包住。怎麼做到的呢,簡單4步就能明白了。
1. 函數是物件
我們定義個 get_content 函數。這時 get_content 也是個對象,它可以做所有對象的操作。
def get_content(): return 'hello world'
它有 id ,有 type ,有值。
>>> id(get_content) 140090200473112 >>> type(get_content) <class 'function'> >>> get_content <function get_content at 0x7f694aa2be18>
跟其他物件一樣可以被賦值給其它變數。
>>> func_name = get_content >>> func_name() 'hello world'
它可以當參數傳遞,也可以當回傳值
>>> def foo(bar): ... print(bar()) ... return bar ... >>> func = foo(get_content) hello world >>> func() 'hello world'
2 . 自訂函數物件
我們可以用class 來建構子物件。有成員函數 call 的就是函數物件了,函數物件被呼叫時正是呼叫的 call 。
class FuncObj(object): def init(self, name): print('Initialize') self.name= name def call(self): print('Hi', self.name)
我們來呼叫看看。可以看到, 函數物件的使用分兩步驟:構造和呼叫 (同學們注意了,這是考點)。
>>> fo = FuncObj('python') Initialize >>> fo() Hi python
3. @ 是語法糖
裝飾器的@ 沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的程式碼。
@make_bold def get_content(): return 'hello world' # 上面的代码等价于下面的 def get_content(): return 'hello world' get_content = make_bold(get_content)
make_bold 是函數,要求入參是函數對象,回傳值是函數對象。 @ 的語法糖其實是省去了上面最後一行程式碼,讓可讀性更好。用了裝飾器後,每次呼叫 get_content ,真正呼叫的是 make_bold 傳回的函數物件。
4. 用類別實現裝飾器
入參是函數對象,返回是函數對象,如果第2步驟裡的類別的建構子改成入參是個函數對象,不就正好符合要求嗎?讓我們來試試實作 make_bold 。
class make_bold(object): def init(self, func): print('Initialize') self.func = func def call(self): print('Call') return '<b>{}</b>'.format(self.func())
大功告成,看看能不能用。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
成功實作裝飾器!是不是很簡單?
這裡分析一下之前強調的 建構 和 呼叫 兩個過程。我們去掉 @ 文法糖好理解一些。
# 構造,使用裝飾器時建構函數對象,呼叫了init
>>> get_content = make_bold(get_content) Initialize # 调用,实际上直接调用的是make_bold构造出来的函数对象 >>> get_content() Call '<b>hello world</b>'
到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)
函數版裝飾器
閱讀原始碼時,經常見到用嵌套函數實現的裝飾器,怎麼理解?同樣僅需4步。
1. def 的函式物件初始化
用 class 實作的函式物件很容易看到什麼時候 建構 的,那 def 定義的函式物件什麼時候 建構 的呢?
# 這裡的全域變數刪除了無關的內容
>>> globals() {} >>> def func(): ... pass ... >>> globals() {'func': <function func at 0x10f5baf28>}
不像一些編譯型語言,程式在啟動時函數已經建構那樣好了。上面的例子可以看到,執行到 def 會才建構出一個函數對象,並賦值給變數 make_bold 。
這段程式碼和下面的程式碼效果是很像的。
class NoName(object): def call(self): pass func = NoName()
2. 巢狀函數
Python的函數可以巢狀定義。
def outer(): print('Before def:', locals()) def inner(): pass print('After def:', locals()) return inner
inner 是在 outer 內定義的,所以算 outer 的局部變數。執行到 def inner 時函數物件才創建,因此每次呼叫 outer 都會建立一個新的 inner 。下面可以看出,每次回傳的 inner 是不同的。
>>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa0048>} <function outer.<locals>.inner at 0x7f0b18fa0048> >>> outer() Before def: {} After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>} <function outer.<locals>.inner at 0x7f0b18fa00d0>
3. 閉包
#巢狀函數有什麼特別之處?因為有閉包。
def outer(): msg = 'hello world' def inner(): print(msg) return inner
下面的試驗表明, inner 可以存取到 outer 的局部變數 msg 。
>>> func = outer() >>> func() hello world
閉包有2個特點
1. inner 能存取outer 及其祖先函數的命名空間內的變數(局部變量,函數參數)。
2. 呼叫 outer 已經回傳了,但是它的命名空間被傳回的 inner 物件引用,所以還不會被回收。
這部分想深入可以去了解Python的LEGB規則。
4. 用函數實作裝飾器
裝飾器要求入參是函數對象,返回值是函數對象,巢狀函數完全能勝任。
def make_bold(func): print('Initialize') def wrapper(): print('Call') return '<b>{}</b>'.format(func()) return wrapper
用法跟类实现的装饰器一样。可以去掉 @ 语法糖分析下 构造 和 调用 的时机。
>>> @make_bold ... def get_content(): ... return 'hello world' ... Initialize >>> get_content() Call '<b>hello world</b>'
因为返回的 wrapper 还在引用着,所以存在于 make_bold 命名空间的 func 不会消失。 make_bold 可以装饰多个函数, wrapper 不会调用混淆,因为每次调用 make_bold ,都会有创建新的命名空间和新的 wrapper 。
到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)
常见问题
1. 怎么实现带参数的装饰器?
带参数的装饰器,有时会异常的好用。我们看个例子。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... >>> get_content() '<h2>hello world</h2>'
怎么做到的呢?其实这跟装饰器语法没什么关系。去掉 @ 语法糖会变得很容易理解。
@make_header(2) def get_content(): return 'hello world' # 等价于 def get_content(): return 'hello world' unnamed_decorator = make_header(2) get_content = unnamed_decorator(get_content)
上面代码中的 unnamed_decorator 才是真正的装饰器, make_header 是个普通的函数,它的返回值是装饰器。
来看一下实现的代码。
def make_header(level): print('Create decorator') # 这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level def decorator(func): print('Initialize') def wrapper(): print('Call') return '<h{0}>{1}</h{0}>'.format(level, func()) return wrapper # make_header返回装饰器 return decorator
看了实现代码,装饰器的 构造 和 调用 的时序已经很清楚了。
>>> @make_header(2) ... def get_content(): ... return 'hello world' ... Create decorator Initialize >>> get_content() Call '<h2>hello world</h2>'
2. 如何装饰有参数的函数?
为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。
@make_bold def get_login_tip(name): return 'Welcome back, {}'.format(name)
最直接的想法是把 get_login_tip 的参数透传下去。
class make_bold(object): def init(self, func): self.func = func def call(self, name): return '<b>{}</b>'.format(self.func(name))
如果被装饰的函数参数是明确固定的,这么写是没有问题的。但是 make_bold 明显不是这种场景。它既需要装饰没有参数的 get_content ,又需要装饰有参数的 get_login_tip 。这时候就需要可变参数了。
class make_bold(object): def init(self, func): self.func = func def call(self, *args, **kwargs): return '<b>{}</b>'.format(self.func(*args, **kwargs))
当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。
3. 一个函数能否被多个装饰器装饰?
下面这么写合法吗?
@make_italic @make_bold def get_content(): return 'hello world'
合法。上面的的代码和下面等价,留意一下装饰的顺序。
def get_content(): return 'hello world' get_content = make_bold(get_content) # 先装饰离函数定义近的 get_content = make_italic(get_content)
4. functools.wraps 有什么用?
Python的装饰器倍感贴心的地方是对调用方透明。调用方完全不知道也不需要知道调用的函数被装饰了。这样我们就能在调用方的代码完全不改动的前提下,给函数patch功能。
为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。伪装得越像,对调用方来说差异越小。有时光伪装函数名和参数是不够的,因为Python的函数对象有一些元信息调用方可能读取了。为了连这些元信息也伪装上, functools.wraps 出场了。它能用于把被调用函数的 module , name , qualname , doc , annotations 赋值给装饰器返回的函数对象。
import functools def make_bold(func): @functools.wraps(func) def wrapper(*args, **kwargs): return '<b>{}</b>'.format(func(*args, **kwargs)) return wrapper
对比一下效果。
>>> @make_bold ... def get_content(): ... '''Return page content''' ... return 'hello world' # 不用functools.wraps的结果 >>> get_content.name 'wrapper' >>> get_content.doc >>> # 用functools.wraps的结果 >>> get_content.name 'get_content' >>> get_content.doc 'Return page content'
实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上 functools.wraps 吧。
这次是真·完结了,撒花吧~~~
以上是總結Python中裝飾器的使用介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!