LoginSignup
192
185

More than 5 years have passed since last update.

【長編ポエム】関数型言語が分からんお前でもPythonなら分かるやろ:第2回 関数を生成する関数はすごいよ編

Last updated at Posted at 2015-05-18

はじめに

これは、関数型プログラミングの特徴を Python で説明した長編ポエムです。
Python を知らなくてもなんとなく分かるように書いたので、PHP や Java や JavaScript の人も読んでみてください。

【ゴール】

【執筆動機】

  • 関数型な人による関数型の説明がつらいため
    (関数型の利点を関数型言語で説明されても、関数型言語を知らん人には伝わらんわな)
  • 関数型界隈の騒動を利用して漁夫の利を狙うため
    (関数型で騒動が起きる → 関数型に注目が集まる → 関数型を勉強するニワカが増える → SICP や OCaml や Haskell に挑む → みんな挫折する → もっとわかりやすく説明してくれ → さっそうと Python が登場!)

【対象読者】

  • 関数型言語は知らないけど関数型プログラミングの初歩を勉強したい人
  • IQ145 な美少女の説明が理解できなかった人

【連載記事】

関数を生成する関数

おさらい:高階関数とは

高階関数とは、関数をデータとして扱うような関数です。具体的には:

  • 関数を受け取る関数(←前回説明済み)
    • 計算式/変換式を受けとる
    • 条件式/判定式を受けとる
    • 処理/手続きを受けとる
  • 関数を生成する関数(←今回説明します)

関数の中で別の関数を生成する

前回説明したように、Python では関数もオブジェクトなので、データとして扱えます。そのため、関数の中で関数を生成できます。たとえば:

def create_func():
  def newfunc(i):      # 関数の中で新しい関数を生成し、
    if i % 2 == 0:
      return "even"
    else:
      return "odd"
  return newfunc       # それを返す

## 使ってみる
fn = create_func()     # 新しい関数を生成して、変数 fn に代入する
print(fn(2))           # 2 は偶数なので "even" が出力される
print(fn(3))           # 3 は奇数なので "odd" が出力される

このように、関数を生成して返す関数も、高階関数とよばれます。また関数の中で作成される関数を「関数内関数」とよぶことがあります (以降ではより分かりやすく「内側の関数」とよんでます)。

なお、ほかの本だと「関数を生成する関数」ではなく「関数を返す関数」と説明されています。しかし大事なのは関数を返すことよりも、新しく関数を生成することです。なので、本ポエムでは「関数を生成する関数」という説明をします。

内側から外側のローカル変数を参照する

次の 2 つの関数は、どちらもよく似ています。違いは、偶数のときに返す値と、奇数のときに返す値だけです。

def create_func1():
  def newfunc(i):
    if i % 2 == 0:
      return "even"    # 偶数のとき "even" を返す
    else:
      return "odd"     # 奇数のとき "odd" を返す
  return newfunc

def create_func2():
  def newfunc(i):
    if i % 2 == 0:
      return "#fcc"    # 偶数のとき "#fcc" (薄い赤) を返す
    else:
      return "#ccf"    # 奇数のとき "#ccf" (薄い青) を返す
  return newfunc

これらのよく似た 2 つの関数を、共通化してみましょう。

def create_func(even_val, odd_val):  # 外側の関数に引数を追加
  def newfunc(i):      # 内側の関数 (生成される関数) は…
    if i % 2 == 0:
      return even_val  # 偶数のとき even_val を返すよう変更
    else:
      return odd_val   # 奇数のとき odd_val を返すよう変更
  return newfunc

ポイントは次の 2 つです。

  • 外側の関数に、引数を追加する (even_val と odd_val)
  • 内側の関数が、それらを参照する

こうすることで、次のように挙動が少しずつ異なる関数をいくつも生成できます

## "even" と "odd" を返す関数を生成する
fn1 = create_func("even", "odd")
print(fn1(0))    #=> even
print(fn1(1))    #=> odd
print(fn1(2))    #=> even
print(fn1(3))    #=> odd

## "#fcc" と "#ccf" を返す関数を生成する
fn2 = create_func("#fcc", "#ccf")
print(fn2(0))    #=> #fcc
print(fn2(1))    #=> #ccf
print(fn2(2))    #=> #fcc
print(fn2(3))    #=> #ccf

これを、前のセクションの関数と比べてみましょう。

  • 前の関数は、偶数や奇数のときに返す値 ("even"/"odd""#fcc"/"#ccf") が、内側の関数に埋め込まれていました。 そのため、返す値を変えたい場合は、外側の関数も含めて再定義する必要がありました。
  • 今回の関数は、偶数や奇数のときに返す値が埋め込まれておらず、かわりに外側の関数への引数として指定できます。 そのため、返す値を変えたい場合は、外側の関数への引数を変えるだけで済みます。

大事な点は、内側の関数から外側の関数のローカル変数 (この場合は引数) へアクセスしていることです。このような関数は「クロージャ」といいます。これについて、次のセクションで説明します。

<補足>
外側の関数から内側の関数のローカル変数にアクセスすることは、Python に限らずどの言語でも一般的にはできません。
</補足>

ここまでのまとめ

  • Python では関数もオブジェクト
    • → 関数の中で新しい関数を生成できる
  • 内側の関数から外側の関数のローカル変数 (引数も含む) にアクセスできる
    • → 挙動が少しずつ異なる関数を簡単に生成できる

クロージャ

クロージャとは?

クロージャとは何でしょうか? Wikipedia によると

クロージャ(クロージャー、英: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。この概念は少なくとも1960年代のSECDマシンまで遡ることができる。

(...省略...)

クロージャはプログラム内で環境を共有するための仕組みである。レキシカル変数はグローバルな名前空間を占有しないという点でグローバル変数とは異なっている。またオブジェクトのインスタンス変数とは、オブジェクトのインスタンスではなく関数の呼び出しに束縛されているという点で異なる。

うーん、何のことかわかりませんね。この説明を理解するには、IQ が 145 くらい必要そうです。

とりあえず、クロージャとは外側の関数のローカル変数にアクセスしている内側の関数だと思ってください (学術的には不正確かもしれませんが実用上はこの程度の理解で十分です)。

たとえば前のセクションの関数は、内側の関数が外側の関数のローカル変数 (注:引数もローカル変数のひとつです) にアクセスしています。そのため、内側の関数はクロージャです。

## これは外側の関数
def create_func(even_val, odd_val):  # 引数もローカル変数であることに注意
  ## これは内側の関数
  ##  (外側のローカル変数にアクセスしてるので、この関数はクロージャ)
  def newfunc(i):
    if i % 2 == 0:
      return even_val     # 外側のローカル変数にアクセスしている
    else:
      return odd_val      # 外側のローカル変数にアクセスしている
  return newfunc

これに対し、次のケースはクロージャではありません。なぜなら、内側の関数が外側のローカル関数にアクセスしてないからです。

def create_func(even_val, odd_val):
  ## これはクロージャではない
  ## (内側の関数が外側のローカル変数にアクセスしていないため)
  def newfunc(i):
    if i % 2 == 0:
      return "even"
    else:
      return "odd"
  return newfunc

また、次のケースもクロージャではありません。なぜなら、内側の関数がアクセスしているのはグローバル変数だからです。

## これらはグローバル変数
even_val = "#fcc"
odd_val  = "#ccf"

def create_func():
  ## これもクロージャではない
  ## (アクセスしているのがグローバル変数なので)
  def newfunc(i):
    if i % 2 == 0:
      return even_val
    else:
      return odd_val
  return newfunc

クロージャのしくみ

より深く知るために、クロージャのしくみについて説明します。

実装の面から説明すると、クロージャの実態は「変数テーブルがひっついた関数」です。クロージャと通常の関数の違いは、この変数テーブルの有無です。

次は Python3 での例です。クロージャに変数テーブルがひっついていることがわかります。

## クロージャを返す関数
def create_func(even_val, odd_val):
  def newfunc(i):
    if i % 2 == 0: return even_val
    else:          return odd_val
  return newfunc   # (ただの関数ではなく) クロージャを返す

## クロージャを作成すると…
fn = create_func("red", "blue")
## __closure__ という変数テーブルがひっついていることがわかる
print(fn.__closure__[0].cell_contents)   #=> red
print(fn.__closure__[1].cell_contents)   #=> blue

## なお勝手な変更はできない模様
fn.__closure__[0].cell_contents = "white"
   #=> AttributeError: attribute 'cell_contents' of 'cell' objects is not writable

また (クロージャでない) 通常の関数だと、この変数テーブルがありません。

## 通常の関数を返す関数
def create_func():
  def newfunc(i):
    if i % 2 == 0: return "even"
    else:          return "odd"
  return newfunc   # (クロージャではない) ただの関数を返す

## 通常の関数だと、変数テーブルがない
fn = create_func()
print(fn.__closure__)    #=> None

このように、クロージャには変数テーブルがひっついています。言い換えると、クロージャとは状態を持った関数であるといえます。よく「関数は状態を持たない」と言われますが、クロージャはそうではありませんので注意してください。

<補足>
外側のローカル変数へのアクセスは変数テーブル経由となるため、通常のローカル変数へのアクセスと比べて遅くなります。ちょうど、インスタンス変数へのアクセスがローカル変数へのアクセスより遅いのとよく似ています。
</補足>

<追記1>
「ローカル変数より遅いのは実装依存の話では?」という指摘をコメントでいただきました。たしかにその通りで、Python ではローカル変数より遅いですが、他の言語や処理系ではそうではない可能性は十分にあります。ご指摘ありがとうございました。
</追記1>

<追記2>
「クロージャが“状態”を持つという表現はよくないのでは?“属性”や“環境”というほうがよさそう」という指摘をいただきました (ありがとうございます)。個人的には以下のような理由から“状態”という表現でいいと思ってます。
・“状態”という言葉には更新可能かどうかは含まれていない
・“状態”を“属性”とするのは単に見方の問題(関数を、値を返すのは計算式、真偽値を返すのは判別式と見なせるけど、どうせ関数であることには変わらないのと同じ)
・“環境”はたしかに専門書で見かける用語だが、定義があやふやで初心者を混乱させるし、実体はしょせん変数テーブルでしかない
とはいえ筆者が“状態”と呼んでいるものは、もしかしたら初心者が Wikipedia を Wiki と呼ぶようなものかもしれません。興味のある方は、こちらの一連のツイートをご覧ください。
</追記2>

クロージャをオブジェクトで模倣する

ところで、「オブジェクトとは変数に関数がひっついたもの」という説明を聞いたことはありませんか?変数に関数がひっついたのがオブジェクトで、関数に変数がひっついたのがクロージャであれば、オブジェクトとクロージャはよく似てそうです。

実際、クロージャはオブジェクトで模倣できます。次がその例です。

## 先のセクションに出てきた、クロージャを返す関数
def create_func(even_val, odd_val):
  def func(i):
    if i % 2 == 0:
      return even_val
    else:
      return odd_val
  return func

## それを模倣するクラスを作る
class CreateFunc(object):

  def __init__(self, even_val, odd_val):  # コンストラクタ
    self.even_val = even_val
    self.odd_val  = odd_val

  def __call__(self, i):
    if i % 2 == 0:
      return self.even_val
    else:
      return self.odd_val

## 使い方
obj = CreteFunc("red", "blue")   # オブジェクトを生成 (注1)
print(obj.__call__(0))    #=> red
print(obj.__call__(1))    #=> blue
print(obj.__call__(2))    #=> red
print(obj.__call__(3))    #=> blue
## こうでもよい (注2)
print(obj(0))             #=> red
print(obj(1))             #=> blue
print(obj(2))             #=> red
print(obj(3))             #=> blue

## (注1) Pythonでは、オブジェクト生成時に new 演算子などはいらない。
## クラスをあたかも関数のように呼び出すだけでオブジェクトを生成できる。
## (注2) Pythonでは obj.__call__() を obj() のように呼び出せる。

このように、クロージャがやっていることはオブジェクトで模倣できます。このことは、クロージャをサポートしていない言語でもクロージャと同等のことができることを意味します。

ただし次の比較を見れば分かるように、(今回のような場合であれば) クロージャのほうが簡潔に書けます。

## クロージャ版                   ## オブジェクト版
def create_func(even, odd):       class CreateFunc(object):
                                    def __init__(self, even, odd):
                                      self.even = even
                                      self.odd  = odd

  def func(i):                      def __call__(self, i):
    if i % 2 == 0:                    if i % 2 == 0:
      return even                       return self.even
    else:                             else:
      return odd                        return self.odd

  return func

fn = create_func("red", "blue")   obj = CreateFunc("red", "blue")
fn(0)                             obj.__call__(0)   # or obj(0)
fn(1)                             obj.__call__(1)   # or obj(1)

以上のことからわかるように、クロージャとオブジェクトはよく似ています。もし「状態を保持するようなオブジェクト指向は間違い!関数型言語こそ正しい!」と主張する IQ145 な美少女がいたら、「でも先輩、関数型言語のクロージャって、オブジェクトとよく似てますよね?状態を持つ点も含めて。」と聞いてみてください。きっと IQ145 だからすばらしい回答をしてくれることでしょう。

ここまでのまとめ

  • クロージャとは、外側の関数のローカル変数にアクセスするような、内側の関数
    • 実体は、変数テーブルを持った関数オブジェクト
    • 内側の関数であっても、外側の関数のローカル変数にアクセスしてないなら、クロージャではないことに注意
  • クロージャとオブジェクトは似ている
    • クロージャ:関数にデータがひっついたもの
    • オブジェクト:データに関数がひっついたもの

ラッパー関数

「関数を生成するような高階関数」は、ラッパー関数を作るときにとても便利です。ここでは、「関数を生成するような高階関数」の便利さを示すために、「ラッパー関数」について説明します。

ラッパー関数とは?

ラッパー関数とは、既存の関数を呼び出すときに別の処理を追加するような関数です。たとえば:

  • (A) 引数を追加したり細工してから、もとの関数を呼び出す
  • (B) もとの関数を呼び出してから、戻り値を細工する
  • (C) もとの関数を呼び出すときに、前処理と後処理を追加する

もちろんこれら以外にも考えられますが、とりあえずは上の 3 種類を押さえておけばいいでしょう。

具体例を見てみます。

table = [
  {"name": "ハルヒ", "size": "C"},
  {"name": "みくる", "size": "E"},
  {"name": "有希",   "size": "A"},
]

## もとの関数
def search(name):             # データを検索する関数
  for d in table:
    if d["name"] == name:
      return d
  return None

## (A) 引数を追加したり細工してから、もとの関数を呼び出す
def search1(name):
  if name == "みちる":
    name = "みくる"           # 引数を細工して
  return search(name)         # もとの関数を呼び出す

## (B) もとの関数を呼び出してから、戻り値を細工する
def search2(name, targets=["みくる"]):
  ret = search(name)          # もとの関数を呼び出して
  if name in targets:
    ret["size"] = "禁則事項です"   # 戻り値を細工する
  return ret

## (C) もとの関数を呼び出すときに、前処理と後処理を追加する
def search3(name):
  print("** name=%s" % name)  # 前処理
  ret = search(name)          # もとの関数を呼び出す
  print("** ret=%s" % ret)    # 後処理
  return ret

このようにラッパー関数では、引数を細工したり、戻り値を細工したり、前後に処理を追加したりできます。言い換えると、もとの関数に機能を追加したのがラッパー関数といえます。

ラッパー関数を生成する高階関数

さて、これらのラッパー関数を生成するような高階関数を書いてみましょう。ポイントは次の通りです。

  • ポイント1:もとの関数を引数として受け取り、
  • ポイント2:機能を追加した新しい関数やクロージャを返す
def normalize(func):           # 関数を受けとり、
  def wrapper(name):
    if name == "みちる":
      name = "みくる"          # 引数を細工してから
    return func(name)          # もとの関数を呼び出す
  return wrapper               # ような関数 (クロージャ) を返す。

def censor(func, targets):     # 関数を受けとり、
  def wrapper(name):
    ret = func(name)           # もとの関数を呼び出してから
    if name in targets:
      ret["size"] = "禁則事項" # 戻り値を細工する
    return ret
  return wrapper               # ような関数 (クロージャ) を返す。

def trace(func):               # 関数を受けとり、
  def wrapper(name):
    print("** name=%s" % name) # 前処理を追加して
    ret = func(name)           # もとの関数を呼び出して
    print("** ret=%s" % ret)   # 後処理を追加する
    return ret
  return wrapper               # ような関数 (クロージャ) を返す。

これらの使い方は次の通りです。

table = [
  {"name": "ハルヒ", "size": "C"},
  {"name": "みくる", "size": "E"},
  {"name": "有希",   "size": "A"},
]

def search(name):            # データを検索する関数
  for d in table:
    if d["name"] == name:
      return d
  return None

## 引数を細工するラッパー関数
search1 = normalize(search)

## 戻り値を細工するラッパー関数
search2 = censor(search, ["みくる"])

## 前処理と後処理を追加したラッパー関数
search3 = trace(search)

高階関数を使うと、機能を追加したラッパー関数がいとも簡単に生成できてますね。

それだけではありません。これらの機能を自由に組み合わせることが簡単にできます。
たとえば:

## 引数と戻り値の両方を細工するラッパー関数
search12 = censor(normalize(search), ["みくる"])

## 引数と戻り値を細工し、かつ前処理と後処理を追加したラッパー関数
search123 = trace(censor(normalize(search), ["みくる"]))

あるいは:

## 引数と戻り値を細工し、かつ前処理と後処理を追加したラッパー関数
search = normalize(search)
search = censor(search, ["みくる"])
search = trace(search)

なぜ自由な組み合わせができるのでしょうか?それは、高階関数がもとの関数を引数として受けとるおかげです。

### たとえば最初の例だと、もとの関数が埋め込まれていた。
### そのため、関数が固定されていて他の関数に変更できない。
def search3(name):
  print("** name=%s" % name)
  ret = search(name)    # 関数が固定されている
  print("** ret=%s" % ret)
  return ret

### これに対し、高階関数だともとの関数が埋め込まれておらず、
### 引数として受けとるので、他の関数へ簡単に変更できる。
def trace(func):        # もとの関数は引数として受けとる
  def wrapper(name):
    print("** name=%s" % name)
    ret = func(name)    # 関数が固定されてない!
    print("** ret=%s" % ret)
    return ret
  return wrapper

部分適用

ところで、関数型プログラミングでは「部分適用」という言葉があります。これは「ラッパー関数を生成するような機能のひとつ」だと思っていただければ、それで結構です。

def add(x, y):        # 例:2つの数を足す関数があったとして、
  return x + y

def add1(x):          # 一方の引数を 1 に固定すれば、これは
  return add(x, 1)    # 「ある数に 1 を足す関数」になる。

def adder(y):         # 1 に固定せず、任意の数を指定したければ
  def wrapper(x):     # このような高階関数にすればよい。
    return add(x, y)
  return wrapper

add3 = adder(3)       # 使用例 (add(x, y) のうち y を 3 に固定する)
print(add3(7))        #=> 10

def apply(func, y):   # 関数自体も指定できるようにしてみる。
  def wrapper(x):
    return func(x, y)
  return wrapper

add3 = apply(add, 3)  # 使用例 (add(x, y) のうち y を 3 に固定する)
print(add3(7))        #=> 10

このように、ある関数から、引数の一部を与えた状態の関数を新しく作ることを、「部分適用」といいます。

しかし部分適用は、Python だとあんまり使う場面ないです。カリー化に至ってはさっぱりなので、説明は省略。

ここまでのまとめ

  • ラッパー関数とは、もとの関数に機能を追加した関数
    • (A) 引数を追加したり細工してから、もとの関数を呼び出す
    • (B) もとの関数を呼び出してから、戻り値を細工する
    • (C) もとの関数を呼び出すときに、前処理と後処理を追加する
  • 高階関数を使うと、ラッパー関数を簡単に生成できる
    • ポイント1: もとの関数を引数として受け取り、
    • ポイント2: 機能を追加した新しい関数やクロージャを返す
  • 「部分適用」とは、ラッパー関数を生成する方法のひとつ
    • 例:add(x, y) をもとに add(x, 3) と同等な関数 add3(x) を生成する

関数デコレータ

Python にはラッパー関数をよりよく使うための「関数デコレータ」という機能があります。Python では「ラッパー関数を生成する高階関数」を直接呼び出すことはあまりなく、たいていはこの「関数デコレータ」を通じて使います。Python を使ううえでこの「関数デコレータ」は欠かせない、重要な機能です。

関数デコレータとは?

さて、さきほどの「ラッパー関数を生成する高階関数」のサンプルコードは、こんな感じでした。

def search(name):
  ....

search = trace(search)

これは正しいコードですが、search という関数名が 3 回も登場しています。そのため、関数名を変更するときは少なくとも 3 ヶ所を変更しなければなりません。いわゆる「DRYでない」状態といえます。

このような場合、Python では次のように書けます。これを関数デコレータといいます。
(見た目は Java のアノテーションに似てますが、中身はまるで違います。)

@trace               # ← これが関数デコレータ
def search(name):
  ....

関数デコレータを使うと、関数名を書くのは 1 ヶ所だけになります (とても「DRY」な状態ですね)。最初は奇妙な書き方に見えるかもしれませんが、@trace def search(): ... というのは def search(): ...; search = trace(search) と同じですので、難しく考えないでください。

また必要であれば、複数の関数デコレータを指定できます。

## これは、
@deco1
@deco2
@deco3
def func():
  ....

## これと同じ
def func():
  ....
func = deco1(deco2(deco3(func)))

関数デコレータは、Python 以外の言語には見かけません。そのため見た目が奇妙に感じるかもしれませんが、ここまでの内容であれば難しくないはずです。

引数つき関数デコレータ

関数デコレータが難しくなるのは、引数をとる場合です。今から説明してみますが、(Python を本格的に勉強したい人はともかく、そうでないなら) もし理解できなくても気にせず読み飛ばしてください。

まず、「もとの関数を受けとり、新しい関数を返すような高階関数」が、もとの関数 以外 の引数をとる場合を考えます。

## この高階関数は、func 以外の引数 targets がある
def censor(func, targets):
  def wrapper(name):
    ret = func(name)
    if name in targets:
      ret["size"] = "禁則事項です"
    return ret
  return wrapper

これを関数デコレータとして使おうとすると、エラーになります。なぜエラーになるか、分かりますか?

## これはエラーになる
@censor
def search(name):
  ...

## なぜなら、censor() の第2引数 targets がないから
search = censor(search)
   #=> TypeError: censor() missing 1 required positional argument: 'targets'

上のコードは関数デコレータなので、Python は search = censor(search) を実行しようとします。しかし censor() の第2引数 (targets) が指定されていないため、エラーになるのです。

これを避けるには、「引数を受けとり、『関数デコレータ』を返す関数」を作ります。関数デコレータは「関数を受けとって新しい関数を返す関数」ですので、つまりは「引数を受け取り、『関数を受けとって新しい関数を返す関数』を返す関数」(!!) を作ることになります。

def censor(targets):    # 引数を受け取り、関数デコレータを返す関数
  def deco(func):         # 関数デコレータを作る
    def wrapper(name):      # ラッパー関数を作る
      ret = func(name)        # もとの関数を呼び出す
      if name in targets:     # いちばん外側の引数を参照している
        ret["size"] = "禁則事項です"
      return ret              # 戻り値を返す
    return wrapper          # ラッパー関数を返す
  return deco             # 関数デコレータを返す

うぉー、ややこしい…

これの使い方は次の通りです。

## これは
@censor(["みくる"])
def search(name):
  ....

## これと同じ
def search(name):
  ....
deco = censor(["みくる"])
search = deco(search)

## これとは違うことに注意!!
search = censor(search, ["みくる"])

引数つき関数デコレータについての説明は以上ですが、わかりましたか?

わからなかったという方:気にしなくていいです。いつの日か、わかる日が来るでしょう。こなければ、「小難しいことがわからなくても人生に大した影響はない」ことがわかるでしょう。

わかったという方:きっと、わかったつもりになっているだけでしょう。そういう「わかったつもり」が極まると、天才で美少女なポエムを書いちゃったりします。注意しましょう。

<ここから愚痴モード>

Python の関数デコレータは、使いこなせるとたしかに便利。しかし、作り方が引数をとる場合ととらない場合で違うし、引数をとる関数デコレータを定義するのがとても面倒くさいし教えるにも苦労する。はっきりいって、Python の関数デコレータは設計ミスですよ、こんなもの。

関数デコレータが、関数以外の引数を受けとれるようにしてくれるだけで、こういった問題は一挙に解決するんですよね。なんでこうしないんだろう…

## たとえばこういう書き方ができたとして、
@censor ["みくる"], "禁則事項です"
def search(name):
  ....

## これ↑がこれ↓と同じになってくれれば、
def search(arg):
  ....
search = censor(search, ["みくる"], "禁則事項です")

## 引数をとる関数デコレータがとらないデコレータと同じ書き方になるし、
## 引数をとらない関数デコレータをとるように変更しても使う側の互換性は
## 保たれるしで、みんなハッピーになれるのに。
def censor(func, targets, seal):
  def wrapper(name):
    ret = func(arg)
    if name in targets:
      ret["size"] = seal
    return ret
  return wrapper

</ここまで>

関数デコレータって、必要なん?

関数デコレータ機能は、Python 以外では見たことないです (あるかも知れませんが、ポピュラーとは言えないです)。そのため、他の言語のユーザからは「こんなの、高階関数の単なるシンタックスシュガーだよね?必要なくね?」と言われることがあります。

しかし関数デコレータ機能は、なくてもいいけどあるととってもうれしい機能です。なぜなら、関数デコレータ機能があると、ある種のコードがとても読みやすくなるからです。

たとえば、次のような JavaScript のコードを考えてみましょう。

var compile = taskdef("*.html", ["*.txt"], function() {
  var html = this.product;
  var txt  = this.materials[0];
  system.run("txt2html " + html + " > " + txt);
});

もし JavaScript に関数デコレータ機能があれば、これはこう書けるでしょう。こっちのほうがずっと読みやすいと思いませんか?

@taskdef("*.html", ["*.txt"])
function compile() {
  var html = this.product;
  var txt  = this.materials[0];
  system.run("txt2html " + html + " > " + txt);
}

また、次のような別のコードを考えてみましょう。function() {...} が多重の入れ子になっています。

function test_it_should_exit_with_0_when_same_contents() {
  with_dummyfile("A.txt", "aaa", function() {
    with_dummyfile("B.txt", "aaa", function() {
      status = system.run("compare A.txt B.txt");
      asssert_equal(status, 0);
    });
  });
};

もし JavaScript で関数デコレータが使えたら、きっとこう書けるでしょう。多重の入れ子がなくなるので、とてもスッキリします。

@with_dummyfile("B.txt", "aaa")
@with_dummyfile("A.txt", "aaa")
function test_it_should_exit_with_0_when_same_contents() {
  status = system.run("compare A.txt B.txt");
  asssert_equal(status, 0);
};

関数デコレータは、ただのシンタックスシュガーなので実装が簡単なわりに、得られる効果はかなり大きいです。言語にとってはコストパフォーマンスの高い機能なので、他の言語でもぜひ導入されてほしいですね。

デコレータパターンとは違うの?

クラス設計におけるデザインパターンのひとつに、「デコレータパターン」があります。Python の「関数デコレータ」という用語は、たぶんこのパターン名に由来してるんだろうと思います。またデコレータ パターン という目的のために、関数デコレータ 機能 を手段として使うことはよくあります。

しかし両者には違いがあります。

  • 「デコレータパターン」は言語機能ではなく、あくまでパターンのひとつです。また「デコレータパターン」では、新しい関数やクラスが、もとの関数やクラスと同じ使い方ができる (=同じインターフェースを持つ) ように設計します。これにより、もとの関数やクラスを新しいもので置き換えられます。

  • Python の「関数デコレータ」機能は (単なるシンタックスシュガーとはいえ) 立派な言語機能のひとつです。また「関数デコレータ」では、もとの関数と新しい関数とが同じようように使える必要はありません (=インターフェースが違ってもよい)。もとの関数に引数を足したり減らしたり、戻り値のデータ型を変えたりすることはよくあります。

こうしてみると Python の関数デコレータは、デコレータパターンとは違うことがわかります。本当なら機能をもっと正確に表す名前にすべきでした。

ここまでのまとめ

  • 「関数デコレータ機能」とは、ラッパー関数を生成するような高階関数を、使いやすくするためのシンタックスシュガー
    • @deco def fn(): ... は、def fn(): ...; fn = deco(fn) と同じ
    • @deco(arg) def fn(): ... は、def fn(): ...; fn = deco(arg)(fn) と同じ
    • @deco1 @deco2 def fn(): ... は、def fn(): ...; fn = deco1(deco2(fn)) と同じ
  • 「デコレータパターン」とは違う
    • デコレータパターン … デザインパターンのひとつ (言語機能ではない)。もとの関数やクラスと使い方が同じ (=インターフェースが同じ) ままで機能を追加するのが目的。
    • 関数デコレータ … 言語機能のひとつであり、実体はただのシンタックスシュガー。もとの関数やクラスと違うインターフェースにしてもよく、実際そうすることが多い。

その他、Python 固有の話

外側のローカル変数を変更する

Python で内側の関数から外側の関数のローカル変数にアクセスする場合、変数の参照 (read) は何の準備もなくできますが、変数の変更 (write) には制限があります。

## Python3 の場合
def outer():
  x = 10
  y = 20
  def inner():
    x = 15      # これは inner() のローカル変数に代入される。
    nonlocal y  # しかし nonlocal を使うことで、
    y = 25      # outer() のローカル変数に代入できる。
  inner()
  print(x)   #=> 10  (変更されてない)
  print(y)   #=> 25  (変更されている)

outer()

## Python2 の場合
def outer():
  x = 10
  y = [20]      # Python2にはnonlocalがないので、こんなトリックが必要
  def inner():
    x = 15      # これは inner() のローカル変数に代入される。
    y[0] = 25   # これは変数 y を変更していないことに注意!
  inner()
  print(x)      #=> 10  (変更されてない)
  print(y[0])   #=> 25  (yは変更されてないがy[0]は変更されている)

outer()

ほかの言語と比べると、ちょっと異質ですね。Python がこのような仕様になっているのは、いろんな事情のせいです。

  • JavaScript ではローカル変数の宣言に var が必要。そのおかげで「var x = 15 なら inner() のローカル変数、x = 15 なら outer() のローカル変数」という仕様にできる。
  • Ruby では関数/メソッド定義とクロージャ定義の文法が異なる。そのおかげで x = 15 が「関数/メソッド定義の中なら inner() のローカル変数とする」「クロージャ定義の中なら、outer() に変数 x があればそれを使い、なければ inner() のローカル変数として扱う」という使い分けができる。
  • Python には、var もないし、関数とクロージャはどちらも def で行うため文法が同じ。そのため、inner() から outer() のローカル変数を変更する場合は nonlocal を使って明示する必要がある。

<補足>
原理的には、関数定義とクロージャ定義の文法が同じでも両者の区別をつけることは可能なので、Ruby と同じように「外側に同名のローカル変数があるならそれへの代入とする」という仕様にできるはずです。原理的にはね。
</補足>

関数デコレータとクラスデコレータ

Python では、関数デコレータのほかに「クラスデコレータ」という機能もあります。

@publish              # ← これがクラスデコレータ
class Hello(object):
  ...

## これは次と同じ
class Hello(object):
  ...
Hello = publish(Hello)

Python で「デコレータ」というと、通常は関数デコレータのことを指します。しかしデコレータが 2 種類あるし、(前述したように) デザインパターンのひとつである「デコレータパターン」と混同する可能性もあるので、あいまいさを避けるためにできるだけ「関数デコレータ」と呼ぶほうがいいでしょう。

任意個の引数

今までの関数デコレータのサンプルコードでは、もとの関数の引数が 1 つだけだと仮定していました。

def trace(func):
  def newfunc(name):
    print("** name=%s" % name)
    ret = func(name)    # ← 引数が1個だけだと仮定している
    print("** ret=%s" % ret)
    return ret
  return newfunc

そのためこの関数デコレータは、引数が 1 つだけの関数にしか適用できません。

@trace
def search(name):   # ← 引数が1つだけなので適用できる
  for d in tables:
    if d["name"] == name: return d
  return None

@trace
def add(x, y):      # ← 引数が2つなので、実行時エラーになる
  return x+y

add(10, 20)
  #=> TypeError: newfunc() takes 1 positional argument but 2 were given

これを、引数が何個の場合でも適用できるようにするには、次のようにします。

def trace(func):
  def newfunc(*args):    # ← 任意個の引数を受けとる
    print("** args=%r" % (name,))
    ret = func(*args)    # ← 任意個の引数を渡す
    print("** ret=%r" % (ret,))
    return ret
  return newfunc

@trace
def add(x, y):      # ← 引数が2つでもエラーにならない
  return x+y

add(x, y)

また任意のキーワード引数にも対応するには、次のようにします。

def trace(func):
  def newfunc(*args, **kwargs):    # ← 任意個のキーワード引数も受けとる
    print("** args=%r, %kwargs=%r" % (name, kwargs))
    ret = func(*args, **kwargs)    # ← 任意個のキーワード引数も渡す
    print("** ret=%r" % (ret,))
    return ret
  return newfunc

@trace
def add(x, y):
  return x+y

add(y=20, x=10)     # ← キーワード引数を使った呼び出しでも OK

関数名と関数ドキュメント

Python で関数を定義すると、関数オブジェクトに関数名とドキュメントが設定されます。

def search(name):
  """Search table by name."""
  for d in table:
    if d["name"] == name:
      return d
  return None

## 関数名とドキュメントを表示
print(search.__name__)  #=> search
print(search.__doc__)   #=> Search table by name.

しかし高階関数や関数デコレータを使うと、これらが消えてしまったり、違う名前になったりします。

def trace(func):
  def newfunc(name):
    print("** name=%s" % name)
    ret = func(name)
    print("** ret=%s" % ret)
    return ret
  return newfunc

@trace
def search(name):
  ....

print(search.__name__)   #=> newfunc  (関数名が変わっちゃった!)
print(search.__doc__)    #=> None (ドキュメントが消えちゃった!)

これはあまりよろしくないです。そのため、関数名とドキュメントを、もとの関数から新しい関数へとコピーするとよいでしょう。

def trace(func):
  def newfunc(name):
    ....
  # 関数名とドキュメントを、もとの関数からコピーする
  newfunc.__name__ = func.__name__
  newfunc.__doc__  = func.__doc__
  return newfunc

@trace
def search(name):
  ....

print(search.__name__)   #=> search
print(search.__doc__)    #=> Search table by name.

このコピーをしてくれるユーティリティが、Python には標準で用意されていますので、使ってみるといいでしょう。

import functools

def trace(func):
  @functools.wraps(func)   # ← もとの関数からコピーしてくれる
  def newfunc(name):
    ....
  return newfunc

@trace
def search(name):
  ....

print(search.__name__)   #=> search
print(search.__doc__)    #=> Search table by name.

まとめ

本ポエムでは、高階関数のうち「関数を生成するような関数」を説明しました。これを使うと、挙動が少しずつ異なるような関数を生成できます。このとき重要になるのは「クロージャ」であり、これは関数に変数がくっついたものです。

また既存の関数に機能を追加した「ラッパー関数」が便利であることも説明しました。ラッパー関数を生成するときにも高階関数が役に立ち、さらにそれを読みやすくするための「関数デコレータ」という機能も紹介しました。

IQ145 で美少女な先輩よりはわかりやく説明できていると思いますが、気付いた点があったらコメントをお願いします。

練習問題

解答例はコメント欄に書いておきます。

【問題1】
呼び出すたびに、2 つの値を交互に返すような関数を考えます。

## 'red' と 'blue' を交互に返す関数
print(toggle_color())  #=> 'red'
print(toggle_color())  #=> 'blue'
print(toggle_color())  #=> 'red'
print(toggle_color())  #=> 'blue'

## 1 と 0 を交互に返す関数
print(toggle_on_off())  #=> 1
print(toggle_on_off())  #=> 0
print(toggle_on_off())  #=> 1
print(toggle_on_off())  #=> 0

## 'show' と 'hide' を交互に返す関数
print(toggle_visible())  #=> 'show'
print(toggle_visible())  #=> 'hide'
print(toggle_visible())  #=> 'show'
print(toggle_visible())  #=> 'hide'

このような関数を生成する高階関数 new_toggle() を定義してください。使い方はこんな感じ:

toggle_color   = new_toggle("red", "blue")
toggle_on_off  = new_toggle(1, 0)
toggle_visible = new_toggle("show", "hide")

【問題2】
文字列の配列を受け取り、すべて大文字や小文字に変換するような関数を、高階関数 map() を使って定義してみます (map() については前回を参照のこと):

def uppers(strings):
  return map(lambda s: s.upper(), strings)

def lowers(strings):
  return map(lambda s: s.lower(), strings)

print(uppers(['a', 'b', 'c']))   #=> ['A', 'B', 'C']
print(lowers(['A', 'B', 'C']))   #=> ['a', 'b', 'c']

またすべての文字列の長さを求める関数を、同じように map() を使って定義してみます:

def lengths(strings):
  return map(lambda s: len(s), strings)
  ## または return map(len(s), strings)

print(lengths(["Haruhi", "Mikuru", "Yuki"]))  #=> [6, 6, 4]

これらはどれも lambda 式が異なるだけで、それ以外は同じコードになっています。

そこで、これらの関数を簡単に生成するような高階関数 mapper() を定義してみてください。使い方はこんな感じ:

uppers = mapper(lambda s: s.upper())
lowers = mapper(lambda s: s.lower())
lengths = mapper(lambda s: len(s))   # または mapper(len)

print(uppers(['a', 'b', 'c']))   #=> ['A', 'B', 'C']
print(lowers(['A', 'B', 'C']))   #=> ['a', 'b', 'c']
print(lengths(["Haruhi", "Mikuru", "Yuki"]))  #=> [6, 6, 4]

【問題3】
次のような、HTTP リクエストを送信する関数があるとします。

## HTTP リクエストを送信する関数
def http(method, url, params={}, headers={}):
  ....

## HTTPS リクエストを送信する関数
def https(method, url, params={}, headers={}):
  ....

## 使い方
response = http("GET", "http://www.google.com/", {"q": "python"})

これのラッパー関数を、GET/POST/PUT/DELETE/HEAD の各メソッドごとに作りました。

def GET(url, params={}, headers={}):
  return http("GET", url, params, headers)

def POST(url, params={}, headers={}):
  return http("POST", url, params, headers)

def PUT(url, params={}, headers={}):
  return http("PUT", url, params, headers)

def DELETE(url, params={}, headers={}):
  return http("DELETE", url, params, headers)

def HEAD(url, params={}, headers={}):
  return http("HEAD", url, params, headers)

しかしこれだと、似たようなコードの繰り返しです。もっと簡潔に書く方法はないでしょうか。

【問題4】
次のコードは、Python で書かれたテストコードです。テストの中でダミーファイルを生成・削除していることがわかります。

import os, unittest

class FileTest(unittest.TestCase):

  def test_read(self):
    ## ダミーファイルを作成
    filename = "file1.txt"
    File.open(filename, "w") as f:
      f.write("FOO\n")
    ## テストを実行
    try:
      with open(filename) as f:
        content = f.read()
      self.assertEqual(content, "FOO\n")
    ## ダミーファイルを削除
    finally:
      os.unlink(filename)

これをもっと簡潔に書くための、@with_dummyfile() というデコレータ関数を定義してください:

import os, unittest

class FileTest(unittest.TestCase):

  @with_dummyfile("file1.txt", "FOO\n")   # ← これを定義する
  def test_read(self):
    with open("file1.txt") as f:
      content = f.read()
    self.assertEqual(content, "FOO\n")

【問題5】
とある Web アプリケーション用フレームワークが、次のような使い方でした。@view_config() という関数デコレータを使うのですが、読むのも書くのも長ったらしい。

## 長すぎて、読むもの書くのもつらい
@view_config(request_method="GET",
             route_urlpath="/api/books/{book_id}/comments/{comment_id}")
def show_book(self):
  book_id    = self.request.matched['book_id']
  comment_id = self.request.matched['comment_id']
  ....

そこで、より簡潔に書ける @on() という関数デコレータを定義してください。使い方はこんな感じ:

@on("GET", "/api/books/{book_id}/comments/{comment_id}")
def show_book(self):
  book_id    = self.request.matched['book_id']
  comment_id = self.request.matched['comment_id']
  ....

また、URL パスパターン中のパラメータを関数の引数として受けとるようにするデコレータ @api を定義してください。使い方はこんな感じ:

@on("GET", "/api/books/{book_id}/comments/{comment_id}")
@api
def show_book(self, book_id, comment_id):
  # 引数 book_id には self.request.matched['book_id'] が渡される。
  # 引数 commend_id には self.request.matched['comment_id'] が渡される。
  ....

できる人は、@on()@api の機能を取り込んでください。使い方はこんな感じ:

@on("GET", "/api/books/{book_id}/comments/{comment_id}")
def show_book(self, book_id, comment_id):
  # 引数 book_id には self.request.matched['book_id'] が渡される。
  # 引数 commend_id には self.request.matched['comment_id'] が渡される。
  ....

ヒント:

## これは
keyword_args = {"book_id": "123", "comment_id": "98765"}
show_book(self, **keyword_args)
## これと同じ
show_book(self, book_id="123", comment_id="98765")
192
185
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
192
185