3.1 Pythonの関数はファーストクラスオブジェクト

投稿者: | 2022-11-26

『Pythonトリック』(著:DanBder)より

目次

関数はオブジェクト

  • Pythonの関数はファーストクラスオブジェクトです
  • つまり、変数に代入したり、データ構造に格納したり、引数として他の関数に渡したり、さらには他の関数から値として返すこともできる
  • これらの概念を理解できれば、ラムダやデコレーターの高度な機能もすんなり理解できるようになる
def yell(text):
    return text.upper() + "!"
yell('hello')
'HELLO!'
  • yell関数はPythonのオブジェクトであるため、他のオブジェクトと同じように別の変数に代入が出来る
bark = yell
  • yell関数を呼び出すことなく、**yellが参照している「関数オブジェクト」を受け取り**、その関数オブジェクトを指すbarkという2つの名前を作成する
  • すると、barkを呼び出すことで、同じ関数オブジェクトを実行できるようになる
bark('woof')
'WOOF!'
  • 関数オブジェクトとそれらの名前は別のものです
  • つまり、yellは関数オブジェクトの名前だけであって、オブジェクトの本体そのものをbarkに代入するイメージ
  • このことを照明するために、関数の元の名前(yell)を削除してみる
  • 別の名前(bark)がまだ元の関数オブジェクトを指しているため、その名前を使って元の関数を呼び出すことができる
del yell

yell('hello')
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-15-cf1ce97e6b2a> in <module>
      1 del yell
      2 
----> 3 yell('hello')


NameError: name 'yell' is not defined
bark('hello')
'HELLO!'
  • ちなみに、Pythonは関数を作成するたびに、デバッグを目的として、その関数に文字列の識別子を割り当てる
  • この内部識別子にアクセスするには、name 属性を使用する
bark.__name__
'yell'
  • この時点では、この関数のname属性の値は依然としてyellだが、名前識別子はデバッグ支援ツール以外の何ものでもない
  • 「関数を指している変数」と「関数自体」は実際にはまったくの別ものになる

関数はデータ構造に格納できる

  • 関数はファーストクラスオブジェクトであるため、他のオブジェクトと同じようにデータ構造に格納できる
  • 関数をリストに追加もできる
funcs = [bark, str.lower, str.capitalize]
funcs
[<function __main__.yell(text)>,
 <method 'lower' of 'str' objects>,
 <method 'capitalize' of 'str' objects>]
  • このリストに格納されている関数オブジェクトにアクセスする方法は、他の種類のオブジェクトの場合と同じ
for f in funcs:
    print(f, f('hey there'))
<function yell at 0x7ff157de3a60> HEY THERE!
<method 'lower' of 'str' objects> hey there
<method 'capitalize' of 'str' objects> Hey there
funcs[0]('heyho')
'HEYHO!'

関数は他の関数に渡すことができる

  • 関数はオブジェクトであるため、他の関数に引数として渡すことができる
def greet(func):
    greeting = func('Hi, I am a Python program')
    print(greeting)
  • 上記の関数に、引数として別の関数を渡す(greet関数にbark関数を渡す)
greet(bark)
HI, I AM A PYTHON PROGRAM!
def whisper(text):
    return text.lower() + '...'
greet(whisper)
hi, i am a python program...
  • 関数オブジェクトを引数として他の関数に渡す能力の効果は絶大です!
  • 振る舞いを抜き出してプログラム内でやりとりできるのです
  • この例では、greet関数は同じままだが、引数として別の振る舞いを渡すことで、その出力に影響を与えることができる
  • 他の関数を引数として受け取ることができる関数は「高階関数」(higher-order-function)とよぶ
  • 高階関数は関数型プログラミングに不可欠
  • Pythonのmapは、典型的な高階関数です。この関数は、関数オブジェクトとイテラブルを受け取り、イテラブル内の要素ごとにその関数を呼び出しながら結果を返す
  • bark関数を一連の挨拶分にマッピングすると、まとめてフォーマットできる
list(map(bark, ['hello', 'hey', 'hi']))
['HELLO!', 'HEY!', 'HI!']

関数は入れ子にできる

  • Pythonでは、関数を他の関数に中で定義ができる
  • 入れ子、ネスト、内側の関数と呼ばれる
def speak(text):
    def murmur(t):
        return t.lower() + '...'
    return murmur(text)

speak('Hello, World')
'hello, world...'
  • 何が起きたかというと、speakを呼び出すたびに、内側のmurmur関数が新たに定義され、定義されたそばから呼び出される
  • ですが、思わぬ落とし穴がある。speakの外では、murmurは存在しないのである
murmur('Yo')
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-31-e2bafe03df6c> in <module>
----> 1 murmur('Yo')


NameError: name 'murmur' is not defined
  • では、入れ子のmurmur関数にspeakの外からアクセスしたい場合はどうすればよいのか?
  • 関数はオブジェクトです
  • 内側の関数を外側の関数の呼び出し元に返すことができる
def get_speak_func(volume):
    def murmur(text):
        return text.lower() + '...'
    def yell(text):
        return text.upper() + '!'
    if volume > 0.5:
        return yell
    else:
        return murmur
  • get_speak_funcが実際には内側の関数を呼び出さず、volume引数にもとづいて適切な内側の関数を選択し、関数オブジェクトを返すだけであることに注目してください
get_speak_func(0.3)
<function __main__.get_speak_func.<locals>.murmur(text)>
get_speak_func(0.7)
<function __main__.get_speak_func.<locals>.yell(text)>
  • その後はもちろん、返された関数オブジェクトを直接呼び出すか、変数に代入してから呼び出すことができる
speak_func = get_speak_func(0.7)
speak_func('Hello')
'HELLO!'
  • このことをしばし考えてみてください
  • 関数に振る舞いを引数として渡せるだけでなく、振る舞いを戻り値として返せるなんて、すごいと思いませんか?
  • さて、少しこんがらかってくるのはここからです。

関数はローカルの状態を取得できる

  • 前項では、関数に内側の関数を定義できることと、さらに(本来なら隠れている)内側の関数を外側の関数から返せることがわかりました
  • ここから少し大変。関数側プログラミングの世界に足を踏み入れる
  • 内側の関数を定義すると、外側の関数から返せるだけでなく、外側の関数の状態の一部を取得して保持することも可能になる
  • このことを示すため、get_speak_func関数を少し書き換える
  • volume引数とtext引数を直接受け取って、返された関数をすぐに呼び出せるようにする
def get_speak_func(text, volume):
    def murmur():
        return text.lower() + '...'
    def yell():
        return text.upper() + '!'
    if volume > 0.5:
        return yell
    else:
        return murmur
get_speak_func('Hello, World', 0.7)()
'HELLO, WORLD!'
get_speak_func('Hello, World', 0.2)()
'hello, world...'
  • 内側のmurmurとyellをよく見てください
  • これらの関数からtextパラメーターがなくなっていることに気付く
  • ですがどういうわけか、これらの関数は外側の関数で定義されているtextパラメーターに依然としてアクセスできる
  • もっとはっきりいうと、このパラメータの値を取得して「記憶」しているように見える
  • このようなことを行う関数をレキシカルクロージャ(lexical closure)と呼ぶ
  • ここでは単にクロージャと呼ぶ
  • クロージャは、プログラマのフローがその外側のレキシカルクスコープから離れても、そのスコープの値を覚えている
  • 実際にはどういうことかというと、関数は振る舞いを返せるだけでなく、そうした振る舞いを事前に設定できるということです
  • 例をあげる
def make_adder(n):
    def add(x):
        return x + n
    return add
make_adder(3)
<function __main__.make_adder.<locals>.add(x)>
make_adder(5)
<function __main__.make_adder.<locals>.add(x)>
plus_3 = make_adder(3)
plus_5 = make_adder(5)
plus_3(4)
7
plus_5(4)
9
  • この例では、make_adder関数が「加算器」関数を作成して設定するためのファクトリとして機能します
  • この「加算器」関数がmake_adder関数(外側のスコープ)のn引数に依然としてアクセスできることに注目してください

関数のように振る舞うオブジェクト

  • Pythonでは、すべての関数がオブジェクトだが、その逆は当てはまらない
  • つまり、オブジェクトは関数ではない
  • ただし、オブジェクトを呼び出し可能(carable)にすると、多くの場合は関数のように扱うことができる
  • オブジェクトが呼び出し可能である場合は、丸括弧()の関数呼び出し構文を使用することが可能であり、さらには関数呼び出しの引数を渡すこともできる
  • そのすべてを支えているのはダンダーメソッド 「__call__」です。
  • 例として、呼び出し可能オブジェクトを定義するクラスをみてみよう
class Adder:
    def __init__(self, n):
        self.n = n

    def __call__(self, x):
        return self.n + x
Adder(3)
<__main__.Adder at 0x7ff1576bb9d0>
plus_3 = Adder(3)
plus_3(4)
7
  • 内部では、オブジェクトのインスタンスを関数として「呼び出す」と、このオブジェクトのcallメソッドの実行が試みられます
  • すべてのオブジェクトが呼び出す可能になるわけではありません
  • このため、オブジェクトが呼び出し可能と見なされるかどうかをチェックする組み込み関数callableが用意されています
callable(plus_3)
True
callable(bark)
True
callable('hello')
False

ここがポイント

  • 関数を含め、Pythonではすべてのものがオブジェクトである。オブジェクトは変数に代入したり、データ構造に格納したりできる
  • また、他の関数に引数として渡したり、他の関数から値として返したりすることもできる(ファーストクラス関数)
  • ファーストクラス関数を使用すれば、振る舞いを抜き出してプログラム内でやり取りできる
  • 関数は入れ子にできる。入れ子(内側)の関数では、外側の関数の状態を取得して保持することができる。このような関数をクロージャと呼ぶ
  • オブジェクトは呼び出し可能にできる。多くの場合、呼び出し可能オブジェクトは関数のように扱うことができる