mei_13のPython講座 ロゴ

【解説】Pythonで読み解く関数型プログラミング:宣言的な記述でコードをより美しく




Pythonで読み解く関数型プログラミング:宣言的な記述でコードをより美しく


Hirokiのアイコン
【Hiroki】 Yukiさん、こんにちは!今日は以前から気になっていた「関数型プログラミング」について教えてほしいんです。Pythonでもできるって聞いたんですけど、なんだか難しそうで……。


Yukiのアイコン
【Yuki】 Hirokiくん、こんにちは。関数型プログラミング……確かに、最初は少しとっつきにくい印象があるかもしれませんね。オブジェクト指向とはまた違った考え方が必要になるので、戸惑うのも無理はないと思います……。 でも、仕組みを理解してしまえば、コードがとてもシンプルで、バグの入り込みにくい綺麗なものになるんですよ。今日はPythonを使って、そのエッセンスをゆっくり紐解いていきましょうか。


Hirokiのアイコン
【Hiroki】 ありがとうございます!よろしくお願いします。そもそも、関数型プログラミングって、普通のプログラミングと何が違うんですか?

関数型プログラミングとは?


Yukiのアイコン
【Yuki】 一言で言うと、「計算を関数の組み合わせとして捉える」スタイル、と言えるでしょうか……。 私たちが普段よく書くコードは「命令型」と呼ばれていて、「まずこれをやって、次にこの変数を書き換えて……」という手順をコンピュータに指示します。 それに対して、関数型は「何であるか」を定義し、「状態を変化させない」ことを大切にするんです。


Hirokiのアイコン
【Hiroki】 状態を変化させない……?変数の値を変えちゃいけないってことですか?


Yukiのアイコン
【Yuki】 ええ、基本的にはそうなんです。数学の関数 $f(x) = y$ を思い出してみてください。同じ $x$ を入れれば必ず同じ $y$ が返ってきますよね?途中で勝手に値が変わったりはしません。 このように、「副作用」を避けて、関数の外側に影響を与えないように作るのが関数型プログラミングの大きな特徴なんです。


Hirokiのアイコン
【Hiroki】 なるほど。でも、それだとどうやってプログラムを動かすのか、まだイメージが湧かないです。


Yukiのアイコン
【Yuki】 そうですよね。まずは、Pythonにおける関数型プログラミングの第一歩、「第一級オブジェクト」としての関数から見ていきましょう。

第一級オブジェクトとしての関数


Yukiのアイコン
【Yuki】 Pythonでは、関数も数値や文字列と同じように「オブジェクト」として扱えます。変数に代入したり、他の関数の引数として渡したり、戻り値として返したりできるんです。これを「第一級オブジェクト」(ファーストクラスオブジェクト)と呼びます。


Hirokiのアイコン
【Hiroki】 関数を変数に入れる……?ちょっとやってみていいですか?


Hirokiのアイコン
【Hiroki】

def say_hello(name):
    return f"こんにちは、{name}くん"

# 関数を変数に代入
greet = say_hello

print(greet("Hiroki"))


Yukiのアイコン
【Yuki】 はい、完璧です。関数名に () をつけずに参照すると、その関数自体を指すことになります。 こうして関数を自由に扱えるからこそ、複雑な処理を小さな部品(関数)の組み合わせで表現できるんです。


Hirokiのアイコン
【Hiroki】 これなら僕にもわかります!


Yukiのアイコン
【Yuki】 良かったです……。次に、関数型プログラミングの根幹を支える「純粋関数」についてお話ししますね。

純粋関数と副作用の抑制


Yukiのアイコン
【Yuki】 関数型プログラミングでは、「純粋関数」(Pure Function)を目指します。 純粋関数には2つの条件があるんです。 1. 同じ引数に対しては、常に同じ戻り値を返すこと。 2. 関数の外部の状態を変更しない(副作用がない)こと。


Hirokiのアイコン
【Hiroki】 副作用……さっきも出てきましたね。具体的にはどんなことですか?


Yukiのアイコン
【Yuki】 例えば、グローバル変数を書き換えたり、引数で受け取ったリストを直接 append で変更したりすることです。 例を見てみましょうか。


Yukiのアイコン
【Yuki】

# 非純粋な関数の例
total = 0
def add_to_total(n):
    global total
    total += n  # 外部の状態を変更している
    return total

# 純粋な関数の例
def add(a, b):
    return a + b  # 外部には一切影響を与えず、計算結果だけを返す


Hirokiのアイコン
【Hiroki】 あ、確かに add_to_total は呼び出すたびに結果が変わっちゃいますね。 add の方は、いつどこで呼んでも 1 + 23 になります。


Yukiのアイコン
【Yuki】 その通りです。純粋関数は予測可能性が高いので、テストも簡単になりますし、バグも見つけやすくなるんです。 私は、誰かのためにひっそりと、でも確実に役割を果たす小さなツールのようなものが好きなんです……。純粋関数も、与えられた仕事だけを完璧にこなして他に迷惑をかけない、そんな健気なところがあると思いませんか……?


Hirokiのアイコン
【Hiroki】 (Yukiさん、なんだか楽しそうだな……) 確かに、影響範囲がその関数の中だけで完結していると安心感がありますね。

状態を持たない:不変性(イミュータビリティ)の重要性


Yukiのアイコン
【Yuki】 次に大切なのが、「不変性(イミュータビリティ)」です。 一度作成したデータは変更せず、変更が必要な場合は新しいデータを作成するという考え方です。


Hirokiのアイコン
【Hiroki】 でも、リストの中身を入れ替えたい時はどうすればいいんですか?


Yukiのアイコン
【Yuki】 その場合は、元のリストを書き換えるのではなく、「新しいリスト」を作って返します。 Pythonでは tuple(タプル)などは不変ですが、list は可変ですよね。関数型らしく書くなら、なるべく不変な扱いを心がけます。


Hirokiのアイコン
【Hiroki】

# 命令的な書き方(元のリストを変更)
my_list = [1, 2, 3]
my_list.append(4)

# 関数型に近い書き方(新しいリストを作成)
my_list = [1, 2, 3]
new_list = my_list + [4]


Hirokiのアイコン
【Hiroki】 こういうことでしょうか?


Yukiのアイコン
【Yuki】 はい、そうです。これなら my_list を使っている他の場所に影響を与える心配がありません。 メモリの効率を考えると少し贅沢な気もしますが、現代のコンピュータなら多くの場合、安全性のメリットの方が大きくなることが多いんですよ……。

ラムダ式:名もなき関数たち


Yukiのアイコン
【Yuki】 関数型プログラミングを便利にする道具として、「ラムダ式(無名関数)」があります。 名前をつけるほどでもない小さな処理を、その場でサッと書くための仕組みです。


Hirokiのアイコン
【Hiroki】 ラムダ……聞いたことあります!


Yukiのアイコン
【Yuki】 書き方は lambda 引数: 式 という形になります。


Yukiのアイコン
【Yuki】

# 通常の関数定義
def square(x):
    return x * x

# ラムダ式での定義
square_lambda = lambda x: x * x

print(square_lambda(5))  # 25


Hirokiのアイコン
【Hiroki】 すごく短く書けますね!でも、これっていつ使うんですか?


Yukiのアイコン
【Yuki】 主に、これから説明する「高階関数」の引数として使うことが多いです。 わざわざ def で定義しなくても、その場限りの使い捨て関数として渡せるので、コードがスッキリします。

高階関数:map、filter、そしてreduce


Yukiのアイコン
【Yuki】 ここからが関数型プログラミングの本領発揮です。 関数を引数に取ったり、戻り値として関数を返したりする関数を「高階関数」と呼びます。 Pythonでよく使われるのは、mapfilterreduce の3つですね。


Hirokiのアイコン
【Hiroki】 一つずつ教えてください!


Yukiのアイコン
【Yuki】 まずは map です。これは、リストなどのすべての要素に、特定の関数を適用するものです。


Yukiのアイコン
【Yuki】

numbers = [1, 2, 3, 4, 5]
# すべての要素を2乗する
squared = map(lambda x: x**2, numbers)

print(list(squared))  # [1, 4, 9, 16, 25]


Hirokiのアイコン
【Hiroki】 for文を使わなくても、一気に処理できるんですね。


Yukiのアイコン
【Yuki】 ええ。次は filter です。これは、条件に合う要素だけを抽出します。


Yukiのアイコン
【Yuki】

numbers = [1, 2, 3, 4, 5, 6]
# 偶数だけを取り出す
evens = filter(lambda x: x % 2 == 0, numbers)

print(list(evens))  # [2, 4, 6]


Hirokiのアイコン
【Hiroki】 これも直感的でわかりやすいです!


Yukiのアイコン
【Yuki】 最後に reduce です。これは少し特殊で、要素を順番に処理して1つの値にまとめ上げます。 Python 3では標準組み込み関数ではなく、functools モジュールの中にあります。


Yukiのアイコン
【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のアイコン
【Hiroki】 xy の2つの引数を取るんですね。


Yukiのアイコン
【Yuki】 そうです。最初の2つの要素で計算した結果を次の要素と計算して……というのを繰り返します。 これらを組み合わせることで、複雑なリスト操作を「宣言的」に記述できるようになるんです。

Pythonにおける「関数型」の立ち位置とリスト内包表記


Hirokiのアイコン
【Hiroki】 mapfilter、すごく便利そうですけど、Pythonでは 「リスト内包表記」 の方がよく使われるって聞いたことがあるような……。


Yukiのアイコン
【Yuki】 ……!Hirokiくん、よく知っていますね。 実はその通りなんです。Pythonの生みの親であるGuidoさんは、リスト内包表記の方を好んでいて、mapfilter よりも読みやすいと考えているようです。


Hirokiのアイコン
【Hiroki】 リスト内包表記だとどうなるんですか?


Yukiのアイコン
【Yuki】 先ほどの例を書き換えてみましょう。


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のアイコン
【Hiroki】 確かに、こっちの方が「何をしているか」がパッと見てわかりやすい気がします。


Yukiのアイコン
【Yuki】 そうですね……。Pythonにおいては、リスト内包表記を使うのが「Pythonic」(Pythonらしい)とされることが多いです。 ただ、関数型プログラミングの考え方自体は、どちらを使っても変わりません。 「データがどう加工されていくか」という流れを意識することが大切なんです。


Hirokiのアイコン
【Hiroki】 なるほど。道具が何であれ、考え方の核となる部分は同じなんですね。

実践的なライブラリ:itertoolsとfunctools


Yukiのアイコン
【Yuki】 さらにステップアップしたい時は、Pythonの標準ライブラリである itertoolsfunctools を覗いてみるといいかもしれません。


Hirokiのアイコン
【Hiroki】 どんなことができるんですか?


Yukiのアイコン
【Yuki】 itertools は、効率的なループ処理のための道具箱です。無限に続く数列を作ったり、組み合わせ(コンビネーション)を生成したりできます。 functools には、先ほどの reduce の他に、partial(部分適用)という面白い機能があります。


Yukiのアイコン
【Yuki】

from functools import partial

def multiply(x, y):
    return x * y

# 引数の1つを「2」に固定した新しい関数を作る
double = partial(multiply, 2)

print(double(10))  # 20


Hirokiのアイコン
【Hiroki】 あ、これって関数をカスタマイズして再利用するのに便利そうですね。


Yukiのアイコン
【Yuki】 はい。既存の関数を少しだけ変えて使い回したい時に重宝します。 こうしたツールを使いこなせると、コードの表現力がぐんと広がりますよ……。

まとめ:関数型のエッセンスを日常のコードに


Hirokiのアイコン
【Hiroki】 Yukiさん、ありがとうございます!関数型プログラミング、なんとなくイメージが掴めてきました。 全部を関数型にするのは大変そうですけど、少しずつ取り入れるだけでもコードが綺麗になりそうです。


Yukiのアイコン
【Yuki】 そう言ってもらえると嬉しいです……。 無理にすべてを関数型にする必要はありません。Pythonはマルチパラダイムな言語ですから、オブジェクト指向の良いところと、関数型の良いところを「いいとこ取り」すればいいんです。


Hirokiのアイコン
【Hiroki】 「いいとこ取り」、いい響きですね。


Yukiのアイコン
【Yuki】 最後に、大切なポイントをまとめておきますね。

  • 純粋関数を意識して、副作用を最小限に抑えること。
  • データはなるべく変更せず、不変(イミュータブル)に扱うこと。
  • 高階関数リスト内包表記を活用して、処理の流れを簡潔に書くこと。

これらを意識するだけで、数ヶ月後の自分が見ても「読みやすい」と思えるコードに近づけるはずです……。


Hirokiのアイコン
【Hiroki】 数ヶ月後の自分に感謝されるコード……頑張って書いてみます!


Yukiのアイコン
【Yuki】 ふふ、応援していますね。 もし途中でわからなくなったら、いつでも聞いてください。 私も、Hirokiくんと一緒に勉強するのは……その、嫌いじゃないですから……。


Hirokiのアイコン
【Hiroki】 はい!またよろしくお願いします、Yukiさん!


参考文献・URL - Python 公式ドキュメント: 関数型プログラミング HOWTO - Python 公式ドキュメント: functools --- 高階関数と呼び出し可能オブジェクトに対する操作 - Python 公式ドキュメント: itertools --- 効率的なループのためのイテレータ生成関数



< 遺伝的アルゴリズム
コラム一覧に戻る
オブジェクト指向 >

この記事では基礎を解説しましたが、実務においては「もっと複雑なデータを扱いたい」「独自のシステムに組み込みたい」といった、個別の課題に直面することも多いはずです。

「自分で書く時間は最小限に抑え、プロの品質でツールを完成させたい」という方は、ぜひ一度ご相談ください。

「教わる」だけでなく「形にする」パートナーとして、フリーランスエンジニアのmei_13が最短ルートでの解決をサポートします。

➡ ココナラで制作・相談を依頼する(見積もり無料)


初心者から始められるPythonレッスン

プログラミング未経験者・初心者歓迎!
月額4,000円で質問し放題!!
● 完全オンライン
● 翌日までには必ず返信
● 挫折しない独自の学習メソッド
● 圧倒的高評価!!
テキストベースで時間を選ばない
● 高品質なサンプルコード
詳細はこちら
興味がある方はまず質問だけでもどうぞ!



AIアシスタント Yuki