関数型プログラミングは、コンピュータサイエンスの数学的基礎と密接に結びついた人気の プログラミングパラダイムである。何が関数型言語であるかの厳密な定義はありませんが、私たちは関数を使ってデータを変換する言語であると考えています。
Pythonは関数型プログラミング言語ではありませんが、他のプログラミングパラダイムと一緒にその概念のいくつかを取り入れています。Pythonでは、関数型スタイルでコードを書くことは簡単で、手元のタスクに最適な解決策を提供できるかもしれません。
関数型プログラミングの概念
関数型言語は宣言型言語であり、コンピュータにどのような結果が欲しいかを伝える。これは通常、問題を解決するために取るべき手順をコンピュータに指示する命令型言語と対照的です。Pythonは通常命令型にコーディングされますが、必要であれば宣言型も使用することができます。
Pythonの機能のいくつかは、純粋な関数型プログラミング言語であるHaskellから影響を受けています。関数型言語とは何かを理解するために、Haskellにある、望ましい、関数的な特徴として見られる機能を見てみましょう。
- 純粋関数 – 副作用を持たない、つまり、プログラムの状態を変化させない。同じ入力があれば、純粋な関数は常に同じ出力を生成する。
- 不変性 – データを作成した後に変更することはできません。例えば、3つの項目を持つリスト
List
を作成し、変数my_list
に格納するとします。もしmy_list
が不変であれば、個々の項目を変更することはできません。もし、my_list
が不変であれば、個々の項目を変更することはできません。もし、異なる値を使用したい場合は、my_list
に新しいList
を設定する必要があります。 - 高階関数 – 関数はパラメータとして他の関数を受け取ることができ、関数は出力として新しい関数を返すことができます。これにより、アクションを抽象化することができ、コードの動作に柔軟性を持たせることができます。
HaskellはレイジーローディングによってPythonのイテレータやジェネレータにも影響を与えましたが、その機能は関数型言語には必要ありません。
Pythonによる関数型プログラミング
Pythonの特別な機能やライブラリがなくても、より機能的な方法でコーディングを開始することができます。
純関数
関数を純粋なものにしたいのであれば、入力の値や関数のスコープ外に存在するデータを変更しないようにします。
こうすることで、書いた関数をより簡単にテストできるようになります。変数の状態を変更しないので、同じ入力で関数を実行するたびに同じ出力が得られることが保証されています。
では、数字を2倍する純粋な関数を作ってみましょう。
def multiply_2_pure(numbers):
new_numbers = []
for n in numbers:
new_numbers.append(n * 2)
return new_numbers
original_numbers = [1, 3, 5, 10]
changed_numbers = multiply_2_pure(original_numbers)
print(original_numbers) # [1, 3, 5, 10]
print(changed_numbers) # [2, 6, 10, 20]
元の numbers
のリストは変更されませんし、関数の外では他の変数を参照しないので、純粋な関数と言えます。
不朽の名作
25に設定した変数がどうしてNone
になったのか不思議に思うバグに遭遇したことはないだろうか?もしその変数が不変であれば、変更された値がすでにソフトウェアに影響を与えている場所ではなく、変数が変更されている場所でエラーが投げられたはずです。バグの根本的な原因は、もっと前に見つけることができます。
Pythonはいくつかのイミュータブルなデータ型を提供しており、その代表的なものが Tuple
です。タプルをリストと比較してみましょう。リストはミュータブルです。
mutable_collection = ['Tim', 10, [4, 5]]
immutable_collection = ('Tim', 10, [4, 5])
# Reading from data types are essentially the same:
print(mutable_collection[2]) # [4, 5]
print(immutable_collection[2]) # [4, 5]
# Let's change the 2nd value from 10 to 15
mutable_collection[1] = 15
# This fails with the tuple
immutable_collection[1] = 15
このとき表示されるエラーは、 TypeError: 'tuple' object does not support item assignment
です。
さて、タプル
がミュータブルオブジェクトに見えるという面白いシナリオがあります。例えば、 immutable_collection
のリストを [4, 5]
から [4, 5, 6]
に変更したいとしたら、以下のようになります。
immutable_collection[2].append(6)
print(immutable_collection[2]) # [4, 5, 6]
これは、 List
がミュータブルオブジェクトであるために動作します。では、リストを [4, 5]
に戻してみましょう。
immutable_collection[2] = [4, 5]
# This throws a familiar error:
# TypeError: 'tuple' object does not support item assignment
予想通り失敗しました。Tuple` の中のミュータブルオブジェクトの内容を変更することはできますが、メモリに格納されているミュータブルオブジェクトへの参照を変更することはできないのです。
高次関数
高階関数は、引数として関数を受け取るか、さらなる処理のために関数を返すか のいずれかであることを思い出してください。Python で簡単な関数を作成する方法を説明します。
ある行を複数回表示する関数を考えてみましょう。
def write_repeat(message, n):
for i in range(n):
print(message)
write_repeat('Hello', 5)
もし、ファイルに5回書き込んだり、メッセージを5回記録したりしたいとしたらどうでしょう?3つの異なる関数を書いてループさせる代わりに、これらの関数を引数として受け取る1つの高階関数を書けばよいのです。
def hof_write_repeat(message, n, action):
for i in range(n):
action(message)
hof_write_repeat('Hello', 5, print)
# Import the logging library
import logging
# Log the output as an error instead
hof_write_repeat('Hello', 5, logging.error)
さて、リスト内の数字を2、5、10と増やす関数を作るという課題を考えてみましょう。まず、最初のケースから見ていこう。
def add2(numbers):
new_numbers = []
for n in numbers:
new_numbers.append(n + 2)
return new_numbers
print(add2([23, 88])) # [25, 90]
add5と
add10` の関数を書くのは簡単ですが、これらの関数が同じように動作することは明らかです: リストをループしてインクリメンターを追加します。そこで、多くの異なるインクリメント関数を作成する代わりに、1つの高次関数を作成する。
def hof_add(increment):
# Create a function that loops and adds the increment
def add_increment(numbers):
new_numbers = []
for n in numbers:
new_numbers.append(n + increment)
return new_numbers
# We return the function as we do any other value
return add_increment
add5 = hof_add(5)
print(add5([23, 88])) # [28, 93]
add10 = hof_add(10)
print(add10([23, 88])) # [33, 98]
高階層関数は、コードに柔軟性を与える。どのような関数が適用され、どのような関数が返されるかを抽象化することで、プログラムの動作をより制御できるようになります。
Python は便利な組み込みの高階関数をいくつか提供しており、シーケンスの処理をより簡単にすることができます。ここでは、これらの組み込み関数をよりよく利用するために、まずラムダ式について見ていきます。
ラムダ式
ラムダ式は無名関数です。Pythonで関数を作るときは、 def
キーワードを使って名前を付けます。ラムダ式を使うと、もっと素早く関数を定義することができます。
ここでは、数値と定義された値を掛け合わせる関数を返す高次関数 hof_product
を作ってみましょう。
def hof_product(multiplier):
return lambda x: x * multiplier
mult6 = hof_product(6)
print(mult6(6)) # 36
ラムダ式はキーワード lambda
で始まり、その後に関数の引数が続く。コロンの後は、ラムダ式が返すコードである。このように、「すぐに」関数を作成できる機能は、高階関数で作業する際に多用される。
ラムダ式にはさらに多くの機能があり、Pythonのラムダ関数という記事で紹介しているので、もっと詳しく知りたい方はそちらを参照してください。
内蔵された高次関数
Python は関数型プログラミング言語でよく使われる高階関数をいくつか実装しており、リストやイテレータのような反復可能なオブジェクトをより簡単に処理できるようになっています。スペースやメモリを効率的に使うために、これらの関数はリストの代わりに iterator
を返します。
地図
map` 関数を使うと、イテレート可能なオブジェクトの各要素に関数を適用することができます。例えば、名前のリストがあり、Stringsに挨拶を追加したい場合、以下のようにすることができる。
names = ['Shivani', 'Jason', 'Yusef', 'Sakura']
greeted_names = map(lambda x: 'Hi ' + x, names)
# This prints something similar to: <map 0x10ed93cc0="" at="" object=""
print(greeted_names)
# Recall, that map returns an iterator
# We can print all names in a for loop
for name in greeted_names:
print(name)
フィルター
filter関数は、イテレート可能なオブジェクトのすべての要素を
Trueまたは
Falseを返す関数でテストし、
True` と評価されたものだけを保持します。もし、数字のリストがあって、5で割り切れるものだけを残したいとしたら、次のようにすることができます。
numbers = [13, 4, 18, 35]
div_by_5 = filter(lambda num: num % 5 == 0, numbers)
# We can convert the iterator into a list
print(list(div_by_5)) # [35]
map と filter の組み合わせ
それぞれの関数はイテレータを返し、どちらもイテレート可能なオブジェクトを受け取るので、これらを一緒に使うことで実に表現力豊かなデータ操作を行うことができます。
# Let's arbitrarily get the all numbers divisible by 3 between 1 and 20 and cube them
arbitrary_numbers = map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))
print(list(arbitrary_numbers)) # [27, 216, 729, 1728, 3375, 5832]
arbitrary_numbers` の式は3つの部分に分解することができます。
-
range(1, 21)
は、1, 2, 3, 4… 19, 20 の数字を表す反復可能なオブジェクトである。 -
filter(lambda num: num % 3 == 0, range(1, 21))
は、3, 6, 9, 12, 15, 18 という数列を表すイテレータです。 - これらを
map
式で3乗すると、27, 216, 729, 1728, 3375, 5832 という数列のイテレータが得られます。
リスト内包
Pythonの機能で、関数型プログラミング言語でよく登場するものにリスト内包があります。mapや
filter` 関数のように、リスト内包は簡潔で表現力豊かな方法でデータを変更することができます。
それでは、先ほどの map
と filter
の例をリスト内包で試してみましょう。
# Recall
names = ['Shivani', 'Jan', 'Yusef', 'Sakura']
# Instead of: map(lambda x: 'Hi ' + x, names), we can do
greeted_names = ['Hi ' + name for name in names]
print(greeted_names) # ['Hi Shivani', 'Hi Jason', 'Hi Yusef', 'Hi Sakura']
基本的なリスト内包はこのような形式です。基本的なリスト内包は次のような形式です: [result for
singular-element in
list-name].
もし、オブジェクトをフィルタリングしたい場合は、 if
キーワードを使用する必要があります。
# Recall
numbers = [13, 4, 18, 35]
# Instead of: filter(lambda num: num % 5 == 0, numbers), we can do
div_by_5 = [num for num in numbers if num % 5 == 0]
print(div_by_5) # [35]
# We can manage the combined case as well:
# Instead of:
# map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))
arbitrary_numbers = [num ** 3 for num in range(1, 21) if num % 3 == 0]
print(arbitrary_numbers) # [27, 216, 729, 1728, 3375, 5832]
すべての map
と filter
式は、リスト内包として表現することができます。
いくつかの注意点
Pythonの生みの親であるGuido van Rossumは、Pythonに関数型機能を持たせることを意図していなかったことはよく知られていますが、その導入が言語にもたらしたいくつかの利点を評価しています。彼は、あるブログ記事で関数型プログラミング言語の機能の歴史について述べています。その結果、言語の実装はFunctional Programmingの機能に対して最適化されてきませんでした。
さらに、Pythonの開発者コミュニティは、膨大な数のFunctional Programmingの機能を使うことを推奨していません。もしあなたが、世界中のPythonコミュニティでレビューされるようなコードを書いているとしたら、 map
や filter
を使う代わりにリスト内包を書くことでしょう。ラムダは最小限の使用にとどめ、関数に名前を付けるでしょう。
Pythonインタプリタでimport this
と入力すると、”The Zen of Python “が表示されます。Pythonは一般的に、コードを可能な限りわかりやすい方法で書くことを推奨しています。理想を言えば、すべてのコードが1つの方法で書かれるべきです。コミュニティは、それがFunctionalスタイルであるべきだとは考えていません。
結論
Functional Programming is a programming paradigm with software primarily composed of functions processing data throughout its execution. Although there’s not one singular definition of what is Functional Programming, we were able to examine some prominent features in Functional Languages: Pure Functions, Immutability, and Higher Order Functions.
Python allows us to code in a functional, declarative style. It even has support for many common functional features like Lambda Expressions and the map
and filter
functions.
However, the Python community does not consider the use of Functional Programming techniques best practice at all times. Even so, we’ve learned new ways to solve problems and if needed we can solve problems leveraging the expressivity of Functional Programming.