Pythonのファーストクラスオブジェクト
Python (JavaScript, Ruby, Scala などの他の言語でも同様です) で見られる最も重要な概念の一つが first-class citizenship です。
First-Class citizen あるいは First-Class citizenship は、ファーストクラスオブジェクトとも言われ、プログラミング言語の設計において、ファーストクラスオブジェクトは、他の実体が一般的に利用できるすべての操作をサポートする実体です。これらの操作には、引数として渡されること、関数から返されること、変更されること、変数に代入されることなどが含まれます。
この記事では、ファーストクラスの意味と、それがコードに与える実用的な貢献について、派生概念であるファクトリと合わせて説明します。
変数に値を代入したり、関数に値を渡したりすることはできます。しかし、関数は有効な「値」でもあるのでしょうか?整数やリストを使うように、関数を使うことができるでしょうか?
答えはイエスで、この機能は関数のいわゆるファーストクラスオブジェクト 、あるいは高階関数によって与えられています。
先に述べたように、このテーマは非常に重要であり、また非常に幅広いものです。詳しく説明するとこの記事が長くなってしまいますし、私にはそのような課題に直面するだけの知識がないように思います。そこで、簡単な例や練習問題を通して、主要なコンセプトを説明していきます。
復習:Pythonで関数を定義する方法
まず、Pythonで関数を定義するために使う基本的な構文を簡単に復習してみましょう。関数には、キーワードdef、関数名、引数、本体の4つの主要な要素があります。
code: python
def name_of_the_function(arguments...):
body
簡単な例として、数値の二乗を計算する関数があります。
code: python
def square(x):
return x * x
この関数は square() と呼ばれ、名前のない 1 つの引数 x を受け取り、x * x を計算して返します。 関数の引数は、(数学と同じように)関数を呼び出すときに渡される具体的な値のプレースホルダーであり、その値を使って関数のコードを実行します。関数は1つの値を返すことができ、明示的にreturn文を挿入しない場合、Pythonは関数にデフォルトのreturn Noneを追加します。
以上が、私たちが始めるのに必要なことです。
引数としての関数:ジェネリック関数
プログラミング言語の世界でファーストクラスオブジェクトを得たコンポーネントに与えられた最初の機能は、関数の引数として渡すことができることです。すべての組み込み型の値や、オブジェクトのインスタンスを渡すことができることはすでにご存知でしょうが、実は関数も渡すことができます。簡単な例を見てみましょう。
code: python
def process(func, value):
return func(value)
このように、第一引数のfuncは、内部的には関数として使われています。先ほど定義した関数 square と一緒に使ってみましょう。
code: python
>> process(square, 5)
25
関数処理では、関数squareを受け取っているので、内部ではsquare(value)を実行し、valueが5なので、結果は5の2乗になります。数値をインクリメントするinc()という関数を定義してみます。
code: python
def inc(x):
return x + 1
これをprocess()に渡すと、6が返ってきます。
code: python
>> process(inc, 5)
6
演習問題1
関数と反復可能なもの(例えばリスト)を受け取り、反復可能なものの各要素に関数を適用する関数applyを書いてください。
code: python
def apply(func, iterable):
...
定義すると、次のように実行できるようになります。
code: python
ソリューション
この問題の基本的なソリューションの例は以下の通りです。
code: python
def apply(func, iterable):
results = []
for i in iterable:
results.append(func(i))
return results
アドバイスとしては、常にシンプルなソリューションから始めて、それを改善していくことです。このような場合には、通常、よりPythonicなソリューション(または、よりイディオム的な解決策)と呼ばれるものを書くことができるかもしれません(そして、そうすべきです)。
code: python
def apply(func, iterable):
演習問題2
引数を1だけ減少させる関数dec()を定義してください(inc()の反対の処理)。次に、関数のリストと1つの値を受け取り、それぞれの関数を値に適用するcompute()という関数を作成します。
code: python
def compute(functions, value):
...
定義されると、次のように実行できるようになります。
code: python
ソリューション
この問題は、前の問題と何らかの形で直交しています。簡単なソリューションとして
code: python
def dec(value):
return value - 1
def compute(functions, value):
results = []
for f in functions:
results.append(f(value))
return results
前述のコードのように、これもよりPythonicなコードにリファクタリングしてみましょう。
code: python
def compute(functions, value):
Python の組み込み関数
最初の演習1で定義した関数apply()はとても重要なので、Pythonには全く同じ仕事をする関数map()が組み込まれています。自由に試してみたり、Python 組み込み関数 map() を読んでみてください。map()は(イテレータである)mapオブジェクトを返すので、結果を印刷したい場合はリストにキャストする必要があることを覚えておいてください。 code: python
<map object at 0x7fc011402ee0>
関数をラップする
関数を「値」として扱うことができるので、リストや辞書のような構造体に格納することもできます。この機能があるだけで、言語の表現力が高まり、特にジェネリック関数が書きやすくなることはご理解いただけたと思います。
プログラミング言語において、ジェネリックという形容詞は、通常、単純な手続き型プログラミングよりもアルゴリズムの抽象度が高いことに関連しています。この資料で行ったことは、ジェネリックコードと定義することができます。なぜなら、リストの要素に対して特定のアクションを実行する関数を指定したのではなく、まだ定義されていない関数を適用する関数を指定したからです。
関数の入れ子:ラッパー
関数が他の関数を受け入れることができることを学んだので、次は関数が他の関数をネストし、関数の内部に定義することができることを学びましょう。これにより、通常ヘルパー関数と呼ばれるものを作成することができます。ヘルパー関数はアルゴリズムを簡略化するためのもので、グローバルに作成する必要はありません。
例えば、リストの各ファイルから拡張子を削除する関数を書いてみましょう。
code: python
def get_extensions(file_list):
results = []
for i in file_list:
if "." in i:
else:
ext = ""
results.append(ext)
return results
これをテストするために、リスト ["foo.txt", "bar.mp4", "python3"]を与えます。
code: python
この結果からわかるように、このアルゴリズムはひどく複雑ではありませんが、Pythonコードの可読性を向上させる標準的なイディオマティックなリファクタリングの1つであるリスト内包表記の形で書くことはできないほど複雑です。
しかし、ヘルパー関数を定義すれば、状況は改善されます。
code: python
def get_extensions(file_list):
def _get_extension(file_name):
if "." in file_name:
ext = file_name.split(".")-1 else:
ext = ""
return ext
results = []
for i in file_list:
results.append(_get_extension(i))
return results
そして、リスト内包表記の形で書き換えると、次のようになります。
code: python
def get_extensions(file_list):
def _remove_ext(file_name):
if "." not in file_name:
return ""
return file_name.split(".")-1 念のために伝えておくと、標準ライブラリには splitext という非常に優れた関数が用意されていて、今回の例と全く同じ働きをするので、その機能が必要な場合は再実装しないようにしましょう。 練習問題3
学んだことを実践するために、非常に簡単な演習を行います。値を受け取るwrapped_inc()という関数を作ります。本体の中に、値を受け取ってそれをインクリメントする関数 _inc() を作ります。そして、外側の関数のボディの中で、値を渡して_inc()を呼び出し、結果を返します。できあがったらそれを見て、wrapped_inc(41)を実行するとどうなるか、結果はどうなるかを説明してください。
ソリューション
この関数はget_extensions()で行ったことをよりシンプルにしたものに過ぎません。
code: python
def wrapped_inc(value):
def _inc(value):
return value + 1
return _inc(value)
wrapped_inc(41)を実行すると、この関数はラッパーを定義してから呼び出しているので、_inc()は41を受け取ってインクリメントし、それを返します。2つ目のreturn文では、同じ値を元の呼び出し元に返します。返された値は42で、これがすべての疑問に対する答えです。
ラッパーのまとめ
内側の関数を定義して呼び出す外側の関数がある場合、外側の関数が内側の関数をラップ(wrap)していると言います。ラッパーの概念は、プログラミング言語やアーキテクチャにおいて非常に重要です。なぜなら、どんなシステムも小さなシステムのラッパーとして見ることができ、その結果、より高い抽象度で記述することができるからです。
戻り値としての関数:ファクトリー
ファーストクラスオブジェクトが提供する3つ目の特徴は、関数が他の関数を返せることです。最も単純な(そしてあまり役に立たない)例は、関数がヘルパー関数を定義して返すというものです。
code: python
def create_inc():
def _inc(value):
return value + 1
return _inc
create_inc()は引数としてvalueを受け入れないことに注意してください。なぜなら、その仕事は関数を返すことであって、何かを計算することではないからです。もっと面白い例に移る前に、これをどのように使うか見てみましょう。
code: python
>> f = create_inc()
>> f(5)
6
create_inc()を呼び出すと、変数fに代入された関数_incが得られます。関数を含んでいるので、値を渡して呼び出すことができます。先に述べたように、これはあまり有用な例ではありませんが、他の関数を生成する関数を作ることができるという重要な概念を示しています。この資料では、外側の関数をファクトリー(Factory) と呼んでいます。
今のところ、ファクトリーはそれほど便利ではありませんが、返される関数にパラメータを設定することができれば、もっと強力になります。次に、実はそれが可能であるので試してみましょう。
code: python
def create_inc(steps):
def _inc(value):
return value + steps
return _inc
ここでは、パラメータ steps をファクトリーに渡し、これを関数 _inc() で使用しています。内部の関数は steps にアクセスすることができます。それは自分が住んでいるのと同じスコープの一部だからです。しかし、関数が作成されてファクトリーの「外」にあるときにも steps にアクセスできるのが不思議です。
code: python
>> inc5 = create_inc(5)
>> inc10 = create_inc(10)
>> inc5(2)
7
>> inc10(2)
12
ご覧のように、inc5はstepの値として5を使用し、inc10は10を使用しています。内側の関数が定義されたスコープを「記憶」することを、この機能をサポートするプログラミング言語ではクロージャ(Closure) と呼びます。
練習問題4
異なるリストで apply(square, some_iterable) と apply(inc, some_iterable) を頻繁に使用していることに気づいたので、イテレート可能なものだけを受け取り、その名前が示す動作を行う2つのショートカット関数lsquare()とlinc()を作りたいとします。
partial_square()という関数を書きましょう。この関数は引数を取らず、反復可能なものに対してsquare()を実行する関数を返します。
code: python
def partial_square():
...
これを定義すると、次のコードを実行できるようになります。
code: python
>> lsquare = partial_square()
ソリューション
最初のバージョンのcreate_inc()から始めることができますが、内側の関数はapply()を呼び出す必要があります。
code: python
def partial_square():
def _apply(iterable):
return apply(square, iterable)
return _apply
この例の、partial_square()は、関数を実行する関数を定義する関数です。少し混乱したかもしれませんが、少し休憩してから、コードをじっと見つめれば丈夫です。
演習問題5
partial_square()を改良して、関数func()を受け取り、イテレート上でfunc()を実行する関数を返すpartial_apply()という関数を書いてみましょう。
code: python
def partial_apply(func):
...
これを定義すると、次のコードを実行できるようになります。
code: python
>> lsquare = partial_apply(square)
ソリューション
2つのバージョンのcreate_inc()で学んだことを活用して、partial_square()をより一般的なものに簡単に変更することができます。
code: python
def partial_apply(func):
def _apply(iterable):
return apply(func, iterable)
return _apply
Python 標準ライブラリ
部分関数(Partial Function)は非常に重要で便利な概念なので、Pythonは先ほど書いたものをさらに汎用的にした functools.partial というソリューションを提供しています。 ファクトリーのまとめ
ファクトリはオブジェクト指向プログラミングにおいて非常に重要な概念であり、一度導入すれば特定の問題に対するアプローチを根本的に変えることができます。いつものように、単純な解決策がある場合は複雑な解決策を使うべきではないことを覚えておいてください。
関数はオブジェクトなのか?、オブジェクトは関数なのか?
Pythonではすべてがオブジェクトです。整数や浮動小数点数のような基本的な値、リストや辞書のような構造体、クラスのインスタンス、クラス自体、そして関数です。実際に関数の内部を見て、その属性の一部を読み取ることができます。
code: python
>> dir(inc)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__',
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']
>> inc.__name__
'_inc'
これは、Pythonが、関数を引数として渡したり、他の関数から関数を返したりしても、問題ない理由を説明しています。Pythonにとっては、整数も辞書も関数もすべて同じものなのです。
しかし、クラスをよく見てみると、すぐにクラスと関数を結びつけるものがあることに気がつきます。クラスをインスタンス化するときには、あたかもそれが関数であるかのように呼び出します。そして、Pythonが内部的に__init__()というメソッドを呼び出します。
code: python
class Item:
def __init__(self, label):
self.label = label
i = Item("TNT") # Point1
この例の Point1 のコードは、関数が実際にはクラスであるにもかかわらず、明らかに関数呼び出しとなっています。どうなっているのでしょうか?
Pythonには呼び出し可能(callable)という概念があります。これは、丸括弧構文()を使って呼び出すことができ、呼び出しに引数を渡すことができるオブジェクトのことです。オブジェクトは、引数を受け取るメソッド __call__() を定義していれば callable です。
code: python
>> i = Item("TNT")
>> i
<__main__.Item object at 0x7fc07203ee80>
>> i.label
'TNT'
>> j = Item.__call__("TNT")
>> j
<__main__.Item object at 0x7fc071fd64c0>
>> j.label
'TNT'
dir(inc) の実行結果からわかるように、関数にはメソッドコールがありますが、整数のようなものにはありません(実際にdir(5)を実行してみてください)。
ですから、卵が先か鶏が先かという因果関係の分からない問題を解決するためには、もう少し具体的に、Pythonでは関数はオブジェクトであり、関数もクラスもcallableであると言うべきでしょう。実用面に目を向けてみましょう。もしクラスが関数のように振る舞えば、ジェネリックプログラミング、ラッパー、ファクトリーを適用することができます。
以下の段落では、クラス、インスタンス、オブジェクトという3つの言葉を多用します。オブジェクトは広義の意味で使用していますので、厳密にはクラスとインスタンスの両方を意味します。これは学術論文ではありませんので、ここでの不正確な用語の使用については気にせずに、大局的に物事を見るようにしてください。
また、クラスをインスタンス化するときに暗黙のうちに呼ばれる__init__()というメソッドについて具体的に説明します。他のメソッドは標準的な関数のように呼ばれ、動作します。
ジェネリックオブジェクト
ジェネリック関数について述べたことを翻訳すると、次のようになります。
オブジェクトは引数として渡すことができる
オブジェクトは他のオブジェクトを引数として受け取ることができる
例えば、関数 process() をクラスとして再実装します。
code: python
class Process:
def __init__(self, func):
self.func = func
def run(self, value):
return self.func(value)
あるいは、機能的なバージョンのprocessを維持し、ユーザーが特定のインターフェイスを持つオブジェクトを渡すと仮定すると、次のようにコードできます。
code: python
class Square:
def run(self, x):
return x * x
def process(filt, value):
f = filt()
return f.run(value)
process(Square, 5)
注意していただきたいのは、構文的に何ができるのかを示しているだけで、この方法が常にベストだと言っているわけではないということです(この例の場合においては良い方法になります)
後者の例では、ユーザーから渡されたオブジェクトは、初期化が必要なクラスであると仮定していますが、APIを変更して、代わりにインスタンスを要求することもできます。クラスを渡すと、関数はポリモーフィズムに起因するすべての問題にさらされますが、関数にはクラスをインスタンス化する権限が与えられ、そのセットアップに高度な設定を与えることができます。インスタンスを渡すことは、APIの面では確かに複雑ではありませんが、制限も多くなります。もしPythonのポリモーフィズムについてもっと知りたいのであれば、Multiple inheritance and mixin classes in Python という記事で探しているものが見つかるかもしれません。 ラッパー
関数を他の関数の中に定義することができるので、他のクラスの中にクラスを定義することを妨げるものは何もありません。構文はヘルパー・メソッドと非常に似ており、選択の理由も同じです。
code: python
class Outer:
class Inner:
pass
def __init__(self):
pass
関数の中にクラスを定義することもできますが、これが役に立つことはほとんどないと思います。クラスの中に関数を定義するケースは、標準的なメソッドの構文で明らかにカバーされています。
ファクトリー
ファクトリー(Factory)の概念は、オブジェクト指向のパラダイムに根ざしており、ジェネリックオブジェクトの場合と同様に、2つの異なる使用方法があります。
オブジェクトファクトリは、オブジェクトを生成する関数です。
ファクトリーオブジェクトとは、ファクトリーのように動作するオブジェクトのことです。
オブジェクトファクトリ
オブジェクトに関しては、常にクラスとインスタンスの2つのエンティティが関係しています。(Pythonではメタクラスを追加することもできますが、ほとんど使用されないので、今は方程式の外に置いておきたいと思います)。オブジェクトファクトリには、クラスファクトリとインスタンスファクトリがあります。
クラスファクトリは、単純に、あるアルゴリズムに従ってクラスを返す関数です。
code: python
def create_type(precision='low'):
if precision == 'low':
return int
elif precision == 'medium':
return float
elif precision == 'high':
from decimal import Decimal
return Decimal
else:
raise ValueError
この非常に簡単なファクトリーをアプリケーションで使用すると、GUIで提供される計算値をホストするための型を動的に生成することができます。
例えば:
code: python
>> gui_input = 3.141592653589793238
>> low_resolution_input = create_type()(gui_input)
>> low_resolution_input
3
>> medium_resolution_input = create_type(precision="medium")(gui_input)
>> medium_resolution_input
3.141592653589793
>> high_resolution_input = create_type(precision="high")(gui_input)
>> high_resolution_input
Decimal('3.141592653589793115997963468544185161590576171875')
注意:このコードには科学的な妥当性はなく、単に概念を説明するためのものです。
一方、インスタンスファクトリは、クラスのインスタンスを返します。クラス自体は内部で作成することができます。
code: python
def init_myint(value):
class MyInt(int):
pass
return MyInt(value)
あるいは、外側の関数から渡すことができます。
code: python
def init_myobj(cls, value):
return cls(value)
ファクトリーオブジェクト
これは、他のオブジェクトを生成・設定し、それを返すことを目的としたクラスやインスタンスを持つことを意味します。ここでは、クラスやインスタンスを生成するクラスや、クラスやインスタンスを生成するインスタンスを持つことができるので、組み合わせの数は劇的に増加します。
わかりやすくするために、他のインスタンスを生成するインスタンスであるファクトリーオブジェクトの例を1つだけ挙げるだけにとどめます。
code: python
class Converter:
def __init__(self, precision='low'):
if precision == 'low':
self._type = int
elif precision == 'medium':
self._type = float
elif precision == 'high':
from decimal import Decimal
self._type = Decimal
else:
raise ValueError
def run(self, value):
return self._type(value)
ご覧のように、関数とオブジェクトの通常の違いを除けば、この例には目新しいものはありません。
デコレーター
デコレータ(Decorator)は、Pythonプログラマが知っておくべきもう一つの重要な概念です。デコレーターの詳細をすべて知る必要はないかもしれませんが、基本的な原理を理解し、それを書けるようになっておくべきでしょう。
Python デコレーター:メタプログラミングの表現方法 という記事ですでに詳しく説明しているので、ここでは表面をなぞるだけにして、ファーストクラスオブジェクトとの関連を示します。デコレーターを作成する最も簡単な方法は、以前ラッパーと呼ばれていたものを使った関数的なアプローチです。 code: python
def decorator(func):
def _func():
func()
return _func
この例では、decorator()は、別の関数 func() を受け取る関数です。decorator() が呼ばれると、内部ヘルパー関数 _func() が作成され、 func() を呼び出してこのラッパーを返します。今のところ、このコードは何も追加していません。しかし、クロージャについて学んだことを考えてみると、_func() を使って多くのことができることにすぐに気づくでしょう。
デコレーターの構文については、次のように書くことを覚えておいてください。
code: python
@decorator
def some_function():
pass
これは次のようなコードの単なるショートカットとなる、シンタックスシュガー(Syntax Sugar)です。
code: python
def some_function():
pass
some_function = decorator(some_function)
アクションでは、関数がファーストクラスオブジェクトとして扱われています。
簡単な具体例として、関数のデフォルトの戻り値を変更するデコレーターを書いてみましょう。最初のまとめで述べたように、Pythonの関数はデフォルトでNoneを返しますが、この挙動を変更したい場合があります。例えば、Trueを返すようにしたいとしましょう。次のようなデコレーターを書くことができます。
code: python
def return_true(func):
def _func(*args, **kwargs):
value = func(*args, **kwargs)
if value is None:
return True
return _func
次のような単純な関数にデコレートして確かめてみましょう。
code: python
In 1: from mydecorator import return_true ...: def test():
...: pass
...:
code: python
...: pass
...:
In 5: test2 = return_true(test2) これまでのセクションで説明してきたことを踏まえて、デコレーターがそれほど曖昧なものではなくなったことを願っています。純粋な関数の代わりにクラスを使用する場合は少し複雑になりますが、基本的な概念はまったく同じです。
functools
Pythonコアの開発者は、ユーザーの機能的なニーズを非常に真剣に受け止めており、高次関数に関する問題に完全に特化したモジュールを提供しています。
この functools モジュールについては、前のセクションで関数 partial を説明したときにすでに触れましたが、少なくともモジュール全体を調査して、それが何を提供できるのかを認識するに値すると思います。いつキャッシュ が必要になるか、map/reduce を活用するかはわかりません。 もし少ししか時間がないのであれば、wraps のドキュメントを読むことを強くお勧めします。なぜなら、関数ベースのデコレーターにはすべてwrapsを使うべきだからです。 使用例:Flaskのアプリケーションファクトリ
最後に、実用的な例を紹介します。よく知られたウェブフレームワークであるFlaskには、プロジェクトをセットアップする際に強く推奨される、アプリケーションファクトリ のコンセプトがあります。 ドキュメントを見ればわかるように、アプリケーションファクトリーとは、Flaskクラスを初期化し、設定し、最終的にそれを返す関数に他なりません。ファクトリーはその後、例えば wsgi.py のようにエントリーポイントとして使用することができます。インポート/実行時にアプリケーションを作成するプレーンなファイルと比較して、ファクトリーの利点は何ですか?ドキュメントにははっきりと次のように書かれています。
テスト:あらゆるケースをテストするために、異なる設定のアプリケーションのインスタンスを持つことができます。
複数のインスタンス:同じアプリケーションの異なるバージョンを実行することを想像してみてください。もちろん、ウェブサーバーで異なる設定をした複数のインスタンスを用意することもできますが、ファクトリーを使用すれば、同じアプリケーションの複数のインスタンスを同じアプリケーションプロセスで実行することができ、これは便利です。
ファーストクラスオブジェクトがコードに与える表現力の大きさを示すものだと思います。
参考
Python 標準ライブラリ
Wikipedia
The Digital Cat
Python.Osaka