Pythonでも使える!「関数型プログラミング」の考え方とは?

こんにちは。ゆうせいです。

新人研修では、きっとPythonのクラスやオブジェクト指向について学んでいる最中かもしれませんね。Pythonはオブジェクト指向がとても得意な言語です。

でも、世の中には「オブジェクト指向」とはまた違った、強力なプログラミングの「スタイル」が存在します。それが、今日お話しする「関数型プログラミング」(かんすうがたプログラミング)です。

「Pythonなのに、オブジェクト指向じゃないの?」

「なんだか数学みたいで難しそう…」

大丈夫ですよ! Pythonは、この関数型プログラミングの良いところも、うまく取り入れられるハイブリッドな言語なんです。

今日は、この「関数型プログラミング」とは何か、そしてPythonでどう活かせるのか、基本のキを一緒に見ていきましょう!


そもそも「関数型プログラミング」って?

関数型プログラミング(Functional Programming、略してFP)とは、プログラムを「関数の集まり」として組み立てていく考え方です。

すごくシンプルに例えるなら、こんなイメージです。

  • オブジェクト指向が…「状態(データ)」と「手続き(メソッド)」をセットにした「モノ(オブジェクト)」を主役にする考え方。(例:プレイヤーオブジェクトが、自分のHPを自分で減らす)
  • 関数型プログラミングが…「データ」と「処理(関数)」を完全に分離し、数学の関数のようないつでも答えが変わらない「純粋な関数」を主役にする考え方。(例:「HP 100のデータ」を「10減らす関数」に入れると、「HP 90のデータ」が新しく出てくる)

関数型プログラミングが目指すのは、「副作用」をなくし、プログラムをできるだけシンプルで予測可能にすることなんです。


FPの重要キーワード:「純粋関数」と「不変性」

この「副作用をなくす」ために、FPでは2つの重要なルールを大切にします。

1. 純粋関数 (Pure Function)

これがFPの心臓部です!

「純粋関数」とは、以下の2つのルールを守る、とても行儀の良い関数のことを指します。

  1. 同じ入力からは、必ず同じ出力が返ってくる。数学の $1 + 1$ がいつでも $2$ になるのと同じです。add(1, 1) を実行したら、プログラムの状態や実行した時間に関係なく、絶対に $2$ が返ってこなくてはいけません。
  2. 副作用(Side Effect)がない。これが一番大事かもしれません。「副作用」とは、関数が自分の外側の世界に影響を与えてしまうことです。例えば、
    • グローバル変数を書き換える
    • ファイルに書き込む
    • データベースを更新する
    • (オブジェクト指向でいう)オブジェクトの「状態」を変更するこれらはすべて副作用です。純粋関数は、これらを一切行いません。

2. 不変性 (Immutability)

「副作用がない」というルールを守るため、FPでは「データは一度作ったら変更しない(不変である)」というルールを採用します。

「えっ!?でも、プレイヤーのHPみたいに、データは変わらないと困るじゃないですか!」

そう思いますよね。

FPの答えはこうです。

「元のデータを変更する」のではなく、「変更を加えた新しいデータを作って、そちらを使う」のです。

例えば、HP 100 のリストがあったとします。

# これは「変更」している (FP的ではない)
my_list = [100, 80]
my_list.append(50) # 既存の my_list が変わってしまう

関数型プログラミングでは、このように考えます。

# これは「新しいリスト」を作っている
my_list_v1 = (100, 80) # タプルは不変です
my_list_v2 = my_list_v1 + (50,) # v1 は変わらず、v2 が新しく作られる

元のデータを絶対に汚さない。だから、バグの予測がしやすくなる、というわけです。


Pythonと関数型プログラミング

では、PythonでどうやってFPの考え方を使うのでしょうか?

Pythonは純粋な関数型言語ではありませんが、FPのスタイルをサポートする便利な機能がたくさん備わっています。

1. map関数とfilter関数

これらは、リストなどの「かたまり」データ(イテラブルと言います)を扱うときに、副作用なく処理するための代表的な関数です。

  • map(関数, リスト)リストの全要素に、指定した「関数」を適用して、新しいリスト(正確にはイテレータ)を作ります。
# 元のリスト
numbers = [1, 2, 3, 4]

# やりたいこと:全部を2倍したい
# FP的な書き方 (map)
doubled = map(lambda x: x * 2, numbers)
# doubled は [2, 4, 6, 8] と同じ中身になります

  • filter(関数, リスト)リストの全要素を「関数」でチェックし、Trueになるものだけを集めた新しいリスト(イテレータ)を作ります。
# 元のリスト
numbers = [1, 2, 3, 4, 5]

# やりたいこと:偶数だけ取り出したい
# FP的な書き方 (filter)
evens = filter(lambda x: x % 2 == 0, numbers)
# evens は [2, 4] と同じ中身になります

2. ラムダ関数 (lambda)

上の例で使った lambda というのは、「名前のない小さな関数」を作るための記法です。

map や filter のように、「この処理のためだけに1回だけ使いたい」という簡単な関数を、その場でサッと作るのに便利ですよ。

3. リスト内包表記

実は、Pythonエンジニアの多くは、map や filter よりも「リスト内包表記」を好んで使います。

これは、FPの考え方を、よりPythonらしくシンプルに書けるようにしたものです。

numbers = [1, 2, 3, 4, 5]

# map と同じこと (2倍する)
doubled = [x * 2 for x in numbers] # [2, 4, 6, 8, 10]

# filter と同じこと (偶数だけ)
evens = [x for x in numbers if x % 2 == 0] # [2, 4]

こちらのほうが、直感的で読みやすいと感じませんか?

これも、元の numbers リストを変更せず、新しいリストを作っている(不変性)のがポイントです。


今後の学習指針

オブジェクト指向が「状態」の管理に長けているのに対し、関数型プログラミングは「データの流れ」をシンプルに記述するのが得意です。

  1. まずは「リスト内包表記」をマスターしよう!for ループを使って新しいリストを作っている処理があったら、「これはリスト内包表記で書けないかな?」と考えてみてください。コードがスッキリするはずです。
  2. 「副作用」を意識しよう自分が書いた関数が、「引数以外のデータ(グローバル変数など)をこっそり書き換えていないか?」を意識するだけでも、バグの少ない良いコードにつながります。
  3. map や filter にも触れてみようリスト内包表記に慣れたら、map や filter がどのような場面で使われているのか、他の人のコードも読んでみると勉強になりますよ。

すべてを関数型にする必要はありません。

オブジェクト指向の良いところと、関数型の良いところ。両方を知っておくことで、あなたのコードの引き出しは格段に増えます!

新しい考え方に触れるのは楽しいですよね。これからも応援しています!

セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。