ホームページ  >  記事  >  バックエンド開発  >  Python でのデコレータの使用の概要

Python でのデコレータの使用の概要

零下一度
零下一度オリジナル
2017-06-29 15:34:321209ブラウズ

私は最近 Python を学習しています。以下は Python 学習グループで紹介されている内容です。今すぐ学習してさらに練習するのが良い方法です。

Python には強力で思いやりのある機能がたくさんあります。最も人気のあるランキングをリストしたい場合、デコレータは間違いなくその中に含まれます。

初めてデコレーターに会ったときは、エレガントで魔法のように感じますが、それを自分の手で実現したいと思うと、プルダの氷の美しさのように、常に距離感を感じます。これは多くの場合、デコレータを理解するときに他の概念が混在していることが原因です。ベールの層を取り除くと、純粋な装飾が実際には非常にシンプルで単純であることがわかります。

デコレータの原理

デコレータのサンプルをインタプリタ下で実行して直感的に感じてみましょう。
# make_bold はデコレータです。実装メソッドはここでは省略します。


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

get_content を呼び出した後、返された結果は自動的に b タグでラップされます。 4つの簡単なステップでそれを理解できます。

1. 関数はオブジェクトです

get_content 関数を定義します。このとき、get_content もオブジェクトであり、すべてのオブジェクトに対して操作を実行できます。



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

id 、 type 、および value を持ちます。



>>> 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 FuncObj(object):
  def init(self, name):
    print(&#39;Initialize&#39;)
    self.name= name

  def call(self):
    print(&#39;Hi&#39;, self.name)

電話して見てみましょう。ご覧のとおり、関数オブジェクトの使用は、構築と呼び出しの 2 つのステップに分かれています (学生の皆さん、注意してください。これがテスト ポイントです)。


>>> 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;

デコレーターが正常に実装されました!シンプルではないでしょうか?

ここでは、以前に強調した構築と呼び出しの2つのプロセスを分析します。理解しやすくするために、@ 構文のシュガーを削除しましょう。

# 構築、デコレーターを使用すると、関数オブジェクトが構築され、init が呼び出されます



>>> get_content = make_bold(get_content)
Initialize

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

この時点で、花を完成させたら、Web ページを閉じることができます~~~(デコレータの原理を知りたいだけです)

デコレータの関数版

ソースコードを読んでいると、ネストされた関数で実装されているデコレータをよく見かけます。必要な手順は 4 つだけです。

1. defの関数オブジェクトの初期化

クラスで実装された関数オブジェクトがいつ構築されるかはわかりやすいのですが、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 が作成されます。以下に示すように、毎回返されるインナーは異なります。


>>> 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 が external のローカル変数 msg にアクセスできることを示しています。


>>> func = outer()
>>> func()
hello world

Closure には 2 つの特徴があります
1. inner は、outer 関数とその祖先関数の名前空間内の変数 (ローカル変数、

関数パラメーター

) にアクセスできます。
2. 外部への呼び出しは返されましたが、その名前空間は返された内部オブジェクトによって参照されているため、まだリサイクルされません。 この部分をさらに詳しく知りたい場合は、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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。