Pythonで読み解く関数型プログラミング:宣言的な記述でコードをより美しく
![]()
【Hiroki】
Yukiさん、こんにちは!今日は以前から気になっていた「関数型プログラミング」について教えてほしいんです。Pythonでもできるって聞いたんですけど、なんだか難しそうで……。
![]()
【Yuki】
Hirokiくん、こんにちは。関数型プログラミング……確かに、最初は少しとっつきにくい印象があるかもしれませんね。オブジェクト指向とはまた違った考え方が必要になるので、戸惑うのも無理はないと思います……。
でも、仕組みを理解してしまえば、コードがとてもシンプルで、バグの入り込みにくい綺麗なものになるんですよ。今日はPythonを使って、そのエッセンスをゆっくり紐解いていきましょうか。
![]()
【Hiroki】
ありがとうございます!よろしくお願いします。そもそも、関数型プログラミングって、普通のプログラミングと何が違うんですか?
関数型プログラミングとは?
![]()
【Yuki】
一言で言うと、「計算を関数の組み合わせとして捉える」スタイル、と言えるでしょうか……。
私たちが普段よく書くコードは「命令型」と呼ばれていて、「まずこれをやって、次にこの変数を書き換えて……」という手順をコンピュータに指示します。
それに対して、関数型は「何であるか」を定義し、「状態を変化させない」ことを大切にするんです。
![]()
【Hiroki】
状態を変化させない……?変数の値を変えちゃいけないってことですか?
![]()
【Yuki】
ええ、基本的にはそうなんです。数学の関数 $f(x) = y$ を思い出してみてください。同じ $x$ を入れれば必ず同じ $y$ が返ってきますよね?途中で勝手に値が変わったりはしません。
このように、「副作用」を避けて、関数の外側に影響を与えないように作るのが関数型プログラミングの大きな特徴なんです。
![]()
【Hiroki】
なるほど。でも、それだとどうやってプログラムを動かすのか、まだイメージが湧かないです。
![]()
【Yuki】
そうですよね。まずは、Pythonにおける関数型プログラミングの第一歩、「第一級オブジェクト」としての関数から見ていきましょう。
第一級オブジェクトとしての関数
![]()
【Yuki】
Pythonでは、関数も数値や文字列と同じように「オブジェクト」として扱えます。変数に代入したり、他の関数の引数として渡したり、戻り値として返したりできるんです。これを「第一級オブジェクト」(ファーストクラスオブジェクト)と呼びます。
![]()
【Hiroki】
関数を変数に入れる……?ちょっとやってみていいですか?
![]()
【Hiroki】
def say_hello(name):
return f"こんにちは、{name}くん"
# 関数を変数に代入
greet = say_hello
print(greet("Hiroki"))
![]()
【Yuki】
はい、完璧です。関数名に () をつけずに参照すると、その関数自体を指すことになります。
こうして関数を自由に扱えるからこそ、複雑な処理を小さな部品(関数)の組み合わせで表現できるんです。
![]()
【Hiroki】
これなら僕にもわかります!
![]()
【Yuki】
良かったです……。次に、関数型プログラミングの根幹を支える「純粋関数」についてお話ししますね。
純粋関数と副作用の抑制
![]()
【Yuki】
関数型プログラミングでは、「純粋関数」(Pure Function)を目指します。
純粋関数には2つの条件があるんです。
1. 同じ引数に対しては、常に同じ戻り値を返すこと。
2. 関数の外部の状態を変更しない(副作用がない)こと。
![]()
【Hiroki】
副作用……さっきも出てきましたね。具体的にはどんなことですか?
![]()
【Yuki】
例えば、グローバル変数を書き換えたり、引数で受け取ったリストを直接 append で変更したりすることです。
例を見てみましょうか。
![]()
【Yuki】
# 非純粋な関数の例
total = 0
def add_to_total(n):
global total
total += n # 外部の状態を変更している
return total
# 純粋な関数の例
def add(a, b):
return a + b # 外部には一切影響を与えず、計算結果だけを返す
![]()
【Hiroki】
あ、確かに add_to_total は呼び出すたびに結果が変わっちゃいますね。
add の方は、いつどこで呼んでも 1 + 2 は 3 になります。
![]()
【Yuki】
その通りです。純粋関数は予測可能性が高いので、テストも簡単になりますし、バグも見つけやすくなるんです。
私は、誰かのためにひっそりと、でも確実に役割を果たす小さなツールのようなものが好きなんです……。純粋関数も、与えられた仕事だけを完璧にこなして他に迷惑をかけない、そんな健気なところがあると思いませんか……?
![]()
【Hiroki】
(Yukiさん、なんだか楽しそうだな……)
確かに、影響範囲がその関数の中だけで完結していると安心感がありますね。
状態を持たない:不変性(イミュータビリティ)の重要性
![]()
【Yuki】
次に大切なのが、「不変性(イミュータビリティ)」です。
一度作成したデータは変更せず、変更が必要な場合は新しいデータを作成するという考え方です。
![]()
【Hiroki】
でも、リストの中身を入れ替えたい時はどうすればいいんですか?
![]()
【Yuki】
その場合は、元のリストを書き換えるのではなく、「新しいリスト」を作って返します。
Pythonでは tuple(タプル)などは不変ですが、list は可変ですよね。関数型らしく書くなら、なるべく不変な扱いを心がけます。
![]()
【Hiroki】
# 命令的な書き方(元のリストを変更)
my_list = [1, 2, 3]
my_list.append(4)
# 関数型に近い書き方(新しいリストを作成)
my_list = [1, 2, 3]
new_list = my_list + [4]
![]()
【Hiroki】
こういうことでしょうか?
![]()
【Yuki】
はい、そうです。これなら my_list を使っている他の場所に影響を与える心配がありません。
メモリの効率を考えると少し贅沢な気もしますが、現代のコンピュータなら多くの場合、安全性のメリットの方が大きくなることが多いんですよ……。
ラムダ式:名もなき関数たち
![]()
【Yuki】
関数型プログラミングを便利にする道具として、「ラムダ式(無名関数)」があります。
名前をつけるほどでもない小さな処理を、その場でサッと書くための仕組みです。
![]()
【Hiroki】
ラムダ……聞いたことあります!
![]()
【Yuki】
書き方は lambda 引数: 式 という形になります。
![]()
【Yuki】
# 通常の関数定義
def square(x):
return x * x
# ラムダ式での定義
square_lambda = lambda x: x * x
print(square_lambda(5)) # 25
![]()
【Hiroki】
すごく短く書けますね!でも、これっていつ使うんですか?
![]()
【Yuki】
主に、これから説明する「高階関数」の引数として使うことが多いです。
わざわざ def で定義しなくても、その場限りの使い捨て関数として渡せるので、コードがスッキリします。
高階関数:map、filter、そしてreduce
![]()
【Yuki】
ここからが関数型プログラミングの本領発揮です。
関数を引数に取ったり、戻り値として関数を返したりする関数を「高階関数」と呼びます。
Pythonでよく使われるのは、map、filter、reduce の3つですね。
![]()
【Hiroki】
一つずつ教えてください!
![]()
【Yuki】
まずは map です。これは、リストなどのすべての要素に、特定の関数を適用するものです。
![]()
【Yuki】
numbers = [1, 2, 3, 4, 5]
# すべての要素を2乗する
squared = map(lambda x: x**2, numbers)
print(list(squared)) # [1, 4, 9, 16, 25]
![]()
【Hiroki】
for文を使わなくても、一気に処理できるんですね。
![]()
【Yuki】
ええ。次は filter です。これは、条件に合う要素だけを抽出します。
![]()
【Yuki】
numbers = [1, 2, 3, 4, 5, 6]
# 偶数だけを取り出す
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens)) # [2, 4, 6]
![]()
【Hiroki】
これも直感的でわかりやすいです!
![]()
【Yuki】
最後に reduce です。これは少し特殊で、要素を順番に処理して1つの値にまとめ上げます。
Python 3では標準組み込み関数ではなく、functools モジュールの中にあります。
![]()
【Yuki】
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# すべての要素を掛け合わせる (1*2*3*4*5)
total_product = reduce(lambda x, y: x * y, numbers)
print(total_product) # 120
![]()
【Hiroki】
x と y の2つの引数を取るんですね。
![]()
【Yuki】
そうです。最初の2つの要素で計算した結果を次の要素と計算して……というのを繰り返します。
これらを組み合わせることで、複雑なリスト操作を「宣言的」に記述できるようになるんです。
Pythonにおける「関数型」の立ち位置とリスト内包表記
![]()
【Hiroki】
map や filter、すごく便利そうですけど、Pythonでは 「リスト内包表記」 の方がよく使われるって聞いたことがあるような……。
![]()
【Yuki】
……!Hirokiくん、よく知っていますね。
実はその通りなんです。Pythonの生みの親であるGuidoさんは、リスト内包表記の方を好んでいて、map や filter よりも読みやすいと考えているようです。
![]()
【Hiroki】
リスト内包表記だとどうなるんですか?
![]()
【Yuki】
先ほどの例を書き換えてみましょう。
![]()
【Yuki】
numbers = [1, 2, 3, 4, 5, 6]
# mapの代わり
squared = [x**2 for x in numbers]
# filterの代わり
evens = [x for x in numbers if x % 2 == 0]
# 組み合わせ
even_squares = [x**2 for x in numbers if x % 2 == 0]
![]()
【Hiroki】
確かに、こっちの方が「何をしているか」がパッと見てわかりやすい気がします。
![]()
【Yuki】
そうですね……。Pythonにおいては、リスト内包表記を使うのが「Pythonic」(Pythonらしい)とされることが多いです。
ただ、関数型プログラミングの考え方自体は、どちらを使っても変わりません。
「データがどう加工されていくか」という流れを意識することが大切なんです。
![]()
【Hiroki】
なるほど。道具が何であれ、考え方の核となる部分は同じなんですね。
実践的なライブラリ:itertoolsとfunctools
![]()
【Yuki】
さらにステップアップしたい時は、Pythonの標準ライブラリである itertools と functools を覗いてみるといいかもしれません。
![]()
【Hiroki】
どんなことができるんですか?
![]()
【Yuki】
itertools は、効率的なループ処理のための道具箱です。無限に続く数列を作ったり、組み合わせ(コンビネーション)を生成したりできます。
functools には、先ほどの reduce の他に、partial(部分適用)という面白い機能があります。
![]()
【Yuki】
from functools import partial
def multiply(x, y):
return x * y
# 引数の1つを「2」に固定した新しい関数を作る
double = partial(multiply, 2)
print(double(10)) # 20
![]()
【Hiroki】
あ、これって関数をカスタマイズして再利用するのに便利そうですね。
![]()
【Yuki】
はい。既存の関数を少しだけ変えて使い回したい時に重宝します。
こうしたツールを使いこなせると、コードの表現力がぐんと広がりますよ……。
まとめ:関数型のエッセンスを日常のコードに
![]()
【Hiroki】
Yukiさん、ありがとうございます!関数型プログラミング、なんとなくイメージが掴めてきました。
全部を関数型にするのは大変そうですけど、少しずつ取り入れるだけでもコードが綺麗になりそうです。
![]()
【Yuki】
そう言ってもらえると嬉しいです……。
無理にすべてを関数型にする必要はありません。Pythonはマルチパラダイムな言語ですから、オブジェクト指向の良いところと、関数型の良いところを「いいとこ取り」すればいいんです。
![]()
【Hiroki】
「いいとこ取り」、いい響きですね。
![]()
【Yuki】
最後に、大切なポイントをまとめておきますね。
- 純粋関数を意識して、副作用を最小限に抑えること。
- データはなるべく変更せず、不変(イミュータブル)に扱うこと。
- 高階関数やリスト内包表記を活用して、処理の流れを簡潔に書くこと。
これらを意識するだけで、数ヶ月後の自分が見ても「読みやすい」と思えるコードに近づけるはずです……。
![]()
【Hiroki】
数ヶ月後の自分に感謝されるコード……頑張って書いてみます!
![]()
【Yuki】
ふふ、応援していますね。
もし途中でわからなくなったら、いつでも聞いてください。
私も、Hirokiくんと一緒に勉強するのは……その、嫌いじゃないですから……。
![]()
【Hiroki】
はい!またよろしくお願いします、Yukiさん!
参考文献・URL - Python 公式ドキュメント: 関数型プログラミング HOWTO - Python 公式ドキュメント: functools --- 高階関数と呼び出し可能オブジェクトに対する操作 - Python 公式ドキュメント: itertools --- 効率的なループのためのイテレータ生成関数
この記事では基礎を解説しましたが、実務においては「もっと複雑なデータを扱いたい」「独自のシステムに組み込みたい」といった、個別の課題に直面することも多いはずです。
「自分で書く時間は最小限に抑え、プロの品質でツールを完成させたい」という方は、ぜひ一度ご相談ください。
- 専門家の知見に基づいた、保守性の高いコード設計
- AIの専門家による、Gemini API等の最新AIを組み合わせた高度な自動化
- ChatGPT等が生成したコードのデバッグ・最適化
「教わる」だけでなく「形にする」パートナーとして、フリーランスエンジニアのmei_13が最短ルートでの解決をサポートします。


