總結Python中裝飾器的使用介紹

零下一度
發布: 2017-06-29 15:34:32
原創
1189 人瀏覽過

最近在學習python,以下是在Python學習小組上介紹的內容,現學現賣、多練習是好的學習方式,希望大家能夠喜歡

Python有大量強大又貼心的特性,如果要列出最受歡迎排行榜,那麼裝飾器絕對會在其中。

初識裝飾器,會感覺到優雅又神奇,想親手實現時卻總有距離感,就像深閨的冰美人一般。這往往是因為理解裝飾器時把其他的一些概念混雜在一起了。待我撫去層次面紗,你會看到純粹的裝飾器其實蠻簡單直率的。

裝飾器的原理

在解釋器下方跑裝飾器的例子,直覺地感受一下。
# make_bold就是裝飾器,實作方式這裡略去


>>> @make_bold
... def get_content():
...  return 'hello world'
...
>>> get_content()
&#39;<b>hello world</b>&#39;
登入後複製

被make_bold 裝飾的get_content ,呼叫後回傳結果會自動被b 標籤包住。怎麼做到的呢,簡單4步就能明白了。

1. 函數是物件

我們定義個 get_content 函數。這時 get_content 也是個對象,它可以做所有對象的操作。


def get_content():
  return &#39;hello world&#39;
登入後複製

它有 id ,有 type ,有值。


>>> id(get_content)
140090200473112
>>> type(get_content)
<class &#39;function&#39;>
>>> get_content
<function get_content at 0x7f694aa2be18>
登入後複製

跟其他物件一樣可以被賦值給其它變數。


>>> func_name = get_content
>>> func_name()
&#39;hello world&#39;
登入後複製

它可以當參數傳遞,也可以當回傳值


>>> def foo(bar):
...   print(bar())
...   return bar
...
>>> func = foo(get_content)
hello world
>>> func()
&#39;hello world&#39;
登入後複製

2 . 自訂函數物件

我們可以用class 來建構子物件。有成員函數 call 的就是函數物件了,函數物件被呼叫時正是呼叫的 call 。


class FuncObj(object):
  def init(self, name):
    print(&#39;Initialize&#39;)
    self.name= name

  def call(self):
    print(&#39;Hi&#39;, self.name)
登入後複製

我們來呼叫看看。可以看到, 函數物件的使用分兩步驟:構造和呼叫 (同學們注意了,這是考點)。


>>> fo = FuncObj(&#39;python&#39;)
Initialize
>>> fo()
Hi python
登入後複製

3. @ 是語法糖

裝飾器的@ 沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的程式碼。


@make_bold
def get_content():
  return &#39;hello world&#39;

# 上面的代码等价于下面的

def get_content():
  return &#39;hello world&#39;
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(&#39;Initialize&#39;)
    self.func = func

  def call(self):
    print(&#39;Call&#39;)
    return &#39;<b>{}</b>&#39;.format(self.func())
登入後複製

大功告成,看看能不能用。


>>> @make_bold
... def get_content():
...   return &#39;hello world&#39;
...
Initialize
>>> get_content()
Call
&#39;<b>hello world</b>&#39;
登入後複製
登入後複製

成功實作裝飾器!是不是很簡單?

這裡分析一下之前強調的 建構 和 呼叫 兩個過程。我們去掉 @ 文法糖好理解一些。
# 構造,使用裝飾器時建構函數對象,呼叫了init


#
>>> get_content = make_bold(get_content)
Initialize

# 调用,实际上直接调用的是make_bold构造出来的函数对象
>>> get_content()
Call
&#39;<b>hello world</b>&#39;
登入後複製

到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)

函數版裝飾器

閱讀原始碼時,經常見到用嵌套函數實現的裝飾器,怎麼理解?同樣僅需4步。

1. def 的函式物件初始化

用 class 實作的函式物件很容易看到什麼時候 建構 的,那 def 定義的函式物件什麼時候 建構 的呢?
# 這裡的全域變數刪除了無關的內容


>>> globals()
{}
>>> def func():
...   pass
...
>>> globals()
{&#39;func&#39;: <function func at 0x10f5baf28>}
登入後複製

不像一些編譯型語言,程式在啟動時函數已經建構那樣好了。上面的例子可以看到,執行到 def 會才建構出一個函數對象,並賦值給變數 make_bold 。

這段程式碼和下面的程式碼效果是很像的。


class NoName(object):
  def call(self):
    pass

func = NoName()
登入後複製

2. 巢狀函數

Python的函數可以巢狀定義。


def outer():
  print(&#39;Before def:&#39;, locals())
  def inner():
    pass
  print(&#39;After def:&#39;, locals())
  return inner
登入後複製

inner 是在 outer 內定義的,所以算 outer 的局部變數。執行到 def inner 時函數物件才創建,因此每次呼叫 outer 都會建立一個新的 inner 。下面可以看出,每次回傳的 inner 是不同的。


>>> outer()
Before def: {}
After def: {&#39;inner&#39;: <function outer.<locals>.inner at 0x7f0b18fa0048>}
<function outer.<locals>.inner at 0x7f0b18fa0048>
>>> outer()
Before def: {}
After def: {&#39;inner&#39;: <function outer.<locals>.inner at 0x7f0b18fa00d0>}
<function outer.<locals>.inner at 0x7f0b18fa00d0>
登入後複製

3. 閉包

#巢狀函數有什麼特別之處?因為有閉包。


def outer():
  msg = &#39;hello world&#39;
  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(&#39;Initialize&#39;)
  def wrapper():
    print(&#39;Call&#39;)
    return &#39;<b>{}</b>&#39;.format(func())
  return wrapper
登入後複製

用法跟类实现的装饰器一样。可以去掉 @ 语法糖分析下 构造 和 调用 的时机。


>>> @make_bold
... def get_content():
...   return &#39;hello world&#39;
...
Initialize
>>> get_content()
Call
&#39;<b>hello world</b>&#39;
登入後複製
登入後複製

因为返回的 wrapper 还在引用着,所以存在于 make_bold 命名空间的 func 不会消失。 make_bold 可以装饰多个函数, wrapper 不会调用混淆,因为每次调用 make_bold ,都会有创建新的命名空间和新的 wrapper 。

到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)

常见问题

1. 怎么实现带参数的装饰器?

带参数的装饰器,有时会异常的好用。我们看个例子。


>>> @make_header(2)
... def get_content():
...   return &#39;hello world&#39;
...
>>> get_content()
&#39;<h2>hello world</h2>&#39;
登入後複製

怎么做到的呢?其实这跟装饰器语法没什么关系。去掉 @ 语法糖会变得很容易理解。


@make_header(2)
def get_content():
  return &#39;hello world&#39;

# 等价于

def get_content():
  return &#39;hello world&#39;
unnamed_decorator = make_header(2)
get_content = unnamed_decorator(get_content)
登入後複製

上面代码中的 unnamed_decorator 才是真正的装饰器, make_header 是个普通的函数,它的返回值是装饰器。

来看一下实现的代码。


def make_header(level):
  print(&#39;Create decorator&#39;)

  # 这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level
  def decorator(func):
    print(&#39;Initialize&#39;)
    def wrapper():
      print(&#39;Call&#39;)
      return &#39;<h{0}>{1}</h{0}>&#39;.format(level, func())
    return wrapper

  # make_header返回装饰器
  return decorator
登入後複製

看了实现代码,装饰器的 构造 和 调用 的时序已经很清楚了。


>>> @make_header(2)
... def get_content():
...   return &#39;hello world&#39;
...
Create decorator
Initialize
>>> get_content()
Call
&#39;<h2>hello world</h2>&#39;
登入後複製

2. 如何装饰有参数的函数

为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。


@make_bold
def get_login_tip(name):
  return &#39;Welcome back, {}&#39;.format(name)
登入後複製

最直接的想法是把 get_login_tip 的参数透传下去。


class make_bold(object):
  def init(self, func):
    self.func = func

  def call(self, name):
    return &#39;<b>{}</b>&#39;.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 &#39;<b>{}</b>&#39;.format(self.func(*args, **kwargs))
登入後複製

当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。

3. 一个函数能否被多个装饰器装饰?

下面这么写合法吗?


@make_italic
@make_bold
def get_content():
  return &#39;hello world&#39;
登入後複製

合法。上面的的代码和下面等价,留意一下装饰的顺序。


def get_content():
  return &#39;hello world&#39;
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 &#39;<b>{}</b>&#39;.format(func(*args, **kwargs))
  return wrapper
登入後複製

对比一下效果。


>>> @make_bold
... def get_content():
...   &#39;&#39;&#39;Return page content&#39;&#39;&#39;
...   return &#39;hello world&#39;

# 不用functools.wraps的结果
>>> get_content.name
&#39;wrapper&#39;
>>> get_content.doc
>>>

# 用functools.wraps的结果
>>> get_content.name
&#39;get_content&#39;
>>> get_content.doc
&#39;Return page content&#39;
登入後複製

实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上 functools.wraps 吧。

这次是真·完结了,撒花吧~~~

以上是總結Python中裝飾器的使用介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!