おぼえがき

過ちては則ち改むるにうんぬんかんぬん

デコレーターについて(Python)

f:id:feci:20210726231508j:plain

Python3

 

Python3.8.5におけるデコレーターについて。

#①引数のない関数の実行
def a():
  def b():
    return 'bです。'
  return b

print(a)     #a関数オブジェクトの出力
print(a())   #b関数オブジェクトの出力
print(a()()) #b関数の実行
#print(b())   #インナー関数の直接的な実行はできない


#②引数のある関数の実行
def a(name):
  return 'hello!' + name

b = a('jobs')
print(b)


#③関数の引数に別の関数を指定して実行
def a(func):
  return func

def b(name):
  return 'hello!' + name

c = a(b)
print(a(b)('jobs'))  #どっちでも出力結果は同じ
print(c('jobs'))     #同上


#④関数Cの処理を関数aおよびインナー関数bで装飾して実行
def a(func):
  def b():
    print('スタート')
    func()
    print('おわり')
  return b

def c():
  print('C関数を実行しています。')

a(c)()
print(a(c)())  #関数内ですでにprintしてるからここでのprintはいらない


#⑤④をデコレーターで処理したパターン
def a(func):
  def b():
    print('スタート')
    func()
    print('おわり')
  return b

@a
def c():
  print('C関数を実行しています。')

c()         #a関数のデコレーターがあるのでa(c)()と同じ意味になる
print(c())  #関数内ですでにprintしてるからここでのprintはいらない


#⑥④のような関数の装飾処理をより複雑化したパターン
def aaa(func):                  #②関数dddを実行したはずなのになぜか最初に実行される
  def bbb(*args**kwargs):     #③関数dddへの引数を受け取るための変数が必要
    print('スタート')           #④スタートという文字列を出力
    ccc = func(*args**kwargs#⑤ここで関数dddを実行
    print('おわり')             #⑥おわりという文字列を出力
    return ccc                  #⑨関数dddの処理結果を返す
  return bbb                    #⑩関数dddの返り値をさらに返す

def ddd(xz):                  #⑦なぜか最初に実行されないし位置引数もここには入らない
  return x + z                  #⑧10+20=30

f = aaa(ddd)(1020)            #①funcに関数dddを入れつつdddの位置引数を指定してdddを実行
print(f)                        #⑪関数dddの処理結果の出力


#⑦⑥をデコレーターで処理したパターン
def aaa(func):
  def bbb(*args**kwargs):
    print('スタート')
    ccc = func(*args**kwargs)
    print('おわり')
    return ccc
  return bbb

@aaa
def ddd(xz):
  return x + z

f = ddd(2525)  #デコレーターを使っているのでこの記述でaaa(ddd)(25, 25)と同じ処理になる
print(f)         #関数dddの処理結果の出力

デコレーターとは、関数の定義時にその関数の機能を装飾(デコレーション)することを意味します。

別の言い方をするなら、対象の関数aに対して別の処理をする関数bの機能を付け加える、というような感じです。

 

「インナー関数(関数内関数)」と「クロージャ―(関数引数)」について理解していないと、デコレーターを理解するのはまず無理だと思います。

なので、そちらの基礎の部分もあわせて見ていきたいと思います。

 

 

上記のプログラムの出力結果は、それぞれ以下のようになります。

 

①の出力結果

<function a at 0x0000021B57B17160>
<function a.<locals>.b at 0x0000021B5813DF70>
bです。

 

②の出力結果

hello!jods

 

③の出力結果

hello!jods
hello!jods

 

④の出力結果

スタート
C関数を実行しています。
おわり
スタート
C関数を実行しています。
おわり
None

 

⑤の出力結果

スタート
C関数を実行しています。
おわり
スタート
C関数を実行しています。
おわり
None

 

⑥の出力結果

スタート
おわり
30

 

⑦の出力結果

スタート
おわり
50

 

 

①のプログラムについての説明。

#①引数のない関数の実行
def a():
  def b():
    return 'bです。'
  return b

print(a)     #a関数オブジェクトの出力
print(a())   #b関数オブジェクトの出力
print(a()()) #b関数の実行
#print(b())   #インナー関数の直接的な実行はできない

これは引数のないインナー関数のパターンです。

 

見てわかるように、関数aの中には関数bオブジェクトそのものが入っています。

そして関数bを実行すると、「bです。」という文字列が返ってきます。

 

なのでこのようなパターンでは、a() と記述して関数aを実行してやるだけでは、関数aの中に格納されている関数bオブジェクトそのものが出力されるだけであり、「bです。」という文字列は出力されません。

 

関数bを実行するには、a()() と記述する必要があります。

 

 

②のプログラムについての説明。

#②引数のある関数の実行
def a(name):
  return 'hello!' + name

b
 = a('jobs')
print(b)

これは関数aに指定されたnameという変数内に引数を入れてやるパターンです。

 

関数aを実行する際に () の中に値を記述することで、その値を引数として関数aが処理をおこなってくれます。

上記のプログラムでは「jods」と記述しているので、関数aの返り値は「hello!jods」となります。

 

 

③のプログラムについての説明。

#③関数の引数に別の関数を指定して実行
def a(func):
  return func

def b(name):
  return 'hello!' + name

c = a(b)
print(a(b)('jobs'))  #どっちでも出力結果は同じ
print(c('jobs'))     #同上

これは関数aの引数funcに関数bを入れたパターンです。

a(b) の部分でfuncの引数を指定しています。

 

ただこのパターンでは関数bにも変数nameへの引数が必要です。

なのでこのプログラムを実行するには、a(b)(nameの値) という形で関数aに格納された関数bを実行しつつ、関数bのnameへの引数も指定しないといけません。

 

上記のプログラムの出力結果は「hello!jobs」となります。

 

 

④のプログラムについての説明。

#④関数Cの処理を関数aおよびインナー関数bで装飾して実行
def a(func):
  def b():
    print('スタート')
    func()
    print('おわり')
  return b

def c():
  print('C関数を実行しています。')

a(c)()
print(a(c)())  #関数内ですでにprintしてるからここでのprintはいらない

これはデコレーターを使うべきプログラムを、デコレーターを使わずに処理しているパターンです。

 

このプログラムを解読できればデコレーターの概要は理解できます。

がんばりましょう。私はがんばりました。

 

このプログラムでは、関数cの処理をインナー関数bの中でおこなっています。

func()の部分ですね。

ここで関数aの中に格納されている関数cオブジェクトを実行しています。

 

a(c)() と記述して変数funcに関数cを入れつつ、すでに関数a内にあった関数bオブジェクトを実行します。

その結果関数bが実行されて、まず最初に「スタート」が出力されます。

次に func() で変数func内に格納されている関数cオブジェクトが実行されるので、「C関数を実行しています。」が出力されます。

 

最後に「おわり」が出力されて、このプログラムは終了します。

 

「なぜfuncに関数cを入れたのに値が上書きされずに最初から入っていた関数bが実行されるのか?」とかは聞かないでください。

そんなことは私が聞きたいくらいです。

 

さてものすごくややこしいですが、要はこのプログラムは、関数cの処理の前後を別の関数で装飾してやった状態であると言えます。

このような場合、次のパターン⑤のようなデコレーターが使えます。

 

 

⑤のプログラムについての説明。

#⑤④をデコレーターで処理したパターン
def a(func):
  def b():
    print('スタート')
    func()
    print('おわり')
  return b

@a
def c():
  print('C関数を実行しています。')

c
()         #a関数のデコレーターがあるのでa(c)()と同じ意味になる
print(c())  #関数内ですでにprintしてるからここでのprintはいらない

このプログラムは、④のプログラムをデコレーターで処理したパターンです。

 

関数cの上の行に @a と記述することで、関数cを関数aでデコレーションすることができます。

こうすることで、a(c)() とややこしい記述をせずにシンプルに c() と書いて関数cを実行してやるだけで、④のプログラムのように関数bを実行することができます。

 

このような @関数名 で関数を装飾してやる処理のことをデコレーターと言います。

 

 

⑥のプログラムについての説明。

#⑥④のような関数の装飾処理をより複雑化したパターン
def aaa(func):                  #②関数aaa内の関数bbbオブジェクトが実行される
  def bbb(*args**kwargs):     #③位置引数が格納される
    print('スタート')           #④スタートという文字列を出力
    ccc = func(*args**kwargs#⑤ここで関数dddを実行
    print('おわり')             #⑥おわりという文字列を出力
    return ccc                  #⑨関数dddの処理結果を返す
  return bbb

def ddd(xz):                  #⑦*argsの値と**kwargsの値がXとyに位置引数として入る
  return x + z                  #⑧10+20=30

f = aaa(ddd)(1020)            #①funcに関数dddを入れつつ関数dddの位置引数を指定して関数dddを実行
print(f)                        #⑩関数dddの処理結果の出力

このプログラムは、④のようなパターンのプログラムをより複雑化したものです。

 

上記のプログラムですが、処理の流れはコメントのとおりです。

まず aaa(bbb)(10, 20) として関数dddの位置引数を指定しつつ関数aaa内の関数bbbオブジェクトを実行します。

 

すると、*argsと**kwargsの中に位置引数として指定した10と20がそれぞれ格納されます。

そのあとで「スタート」という文字列が出力されます。

 

そして func(*args, **kwargs) の部分で、変数func内に格納されている関数dddオブジェクトを実行します。

このときの*argsと**kwargs内の値はそれぞれ位置引数としてxとzの中に格納されます。

そしてその x+z の処理結果が、返り値として変数cccの中に格納されます。

 

あとは、その変数cccの返り値を受け取った変数fの中身(50)をprint関数で出力して、このプログラムは終了します。

 

 

⑦のプログラムについての説明。

#⑦⑥をデコレーターで処理したパターン
def aaa(func):
  def bbb(*args**kwargs):
    print('スタート')
    ccc = func(*args**kwargs)
    print('おわり')
    return ccc
  return bbb

@aaa
def ddd(xz):
  return x + z

f = ddd(2525)  #デコレーターを使っているのでこの記述でaaa(ddd)(25, 25)と同じ処理になる
print(f)         #関数dddの処理結果の出力

⑥のプログラムの処理が理解できたのであれば、デコレーターという処理を理解することは可能だと思います。

 

関数dddの上の行に@aaaと記述することで、関数dddを関数aaaの処理で装飾します。

関数aaaに格納されている関数bbbオブジェクトを実行するには、ddd(第一引数 , 第二引数) と記述してやればOKです。

 

 

以上です。

 

 

●参考サイト

今回はUdemyで買ったの定価24000円のPython講座だけでは理解できなかったので、<【Python】初心者向けにデコレータの解説> という記事をがっつりと参考にさせていただきました。

 

すごくわかりやすくて、自分でもなんとかデコレーターの概要を理解することができました。

作者さん、本当にありがとうございました。