23. ファーストクラス関数と関数型テクニック
前の章では、関数(function)を定義して呼び出す方法、パラメータと引数の扱い方、そして変数スコープの理解を学びました。ここからは、Python を際立たせる強力な機能を探っていきます。それが、関数が ファーストクラスオブジェクト(first-class objects) であることです。これは、関数が他のどんな値と同じように扱える—変数に保存でき、他の関数に引数として渡せ、関数から返すこともできる—という意味です。
この機能により、コードをより柔軟で再利用しやすく、表現力豊かにするエレガントなプログラミング手法が開けます。実用的な例を通してファーストクラス関数を活用する方法を学び、クロージャ(closures)(環境を「記憶する」関数)を理解し、lambda 式(lambda expressions)を使って簡潔に関数を定義し、map()、filter()、any()、all() といった組み込み関数(built-in functions)を適用してコレクション(collections)を効率よく扱う方法を見ていきます。
23.1) ファーストクラスオブジェクトとしての関数
23.1.1) 「ファーストクラス」の意味
Python では、関数は ファーストクラスオブジェクト(first-class objects) であり、次のことができます:
- 変数に代入できる
- データ構造(リスト、辞書など)に格納できる
- 他の関数に引数として渡せる
- 他の関数から値として返せる
これは、関数が特別な扱いを受け、通常の値のように操作できないプログラミング言語もあることと異なります。Python では、関数は整数、文字列、リストと同様に、単なる別の種類のオブジェクトです。
実際に見てみましょう:
# シンプルな関数を定義する
def greet(name):
return f"Hello, {name}!"
# 関数を変数に代入する
say_hello = greet
# 新しい変数経由で関数を呼び出す
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# 両方の名前が同じ関数を参照していることを確認する
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: Truesay_hello = greet と書いたとき、関数を呼び出しているわけではない(丸括弧がない)点に注目してください。同じ関数オブジェクトを参照する新しい名前を作っているのです。greet と say_hello はどちらも同じ関数を指すようになり、is 演算子を使ってそれを確認できます。
23.1.2) データ構造に関数を格納する
関数はオブジェクトなので、リストや辞書、その他の任意のコレクションに格納できます:
# 辞書に演算を格納した電卓
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# 辞書に関数を格納する
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# 辞書を使って計算を行う
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50このパターンは、柔軟なシステムを構築するうえで非常に役立ちます。どの関数を呼ぶかを選ぶために if-elif 文の長い連鎖を書く代わりに、辞書で適切な関数を引いて直接呼び出せます。
23.2) 関数を引数として渡す
23.2.1) 基本概念
ファーストクラス関数の最も強力な使い方の1つが、関数を他の関数の引数として渡すことです。これにより、異なる振る舞いで動ける柔軟で再利用可能なコードを書けるようになります。
簡単な例を示します:
# 別の関数を値に適用する関数
def apply_operation(value, operation):
"""パラメータとして受け取った operation 関数を value に適用します。"""
return operation(value)
# 異なる操作
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# 同じ apply_operation 関数を異なる操作で使う
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5apply_operation 関数は、どの具体的な操作を実行しているかを知る必要も、気にする必要もありません。渡された関数を呼び出すだけです。この関心の分離により、コードはよりモジュール化され、拡張もしやすくなります。
23.2.2) カスタム関数でコレクションを処理する
よくあるパターンとして、引数として渡された関数を使ってコレクションの各要素を処理する方法があります:
# 指定された関数を使ってリストの各要素を処理する
def process_list(items, processor):
"""リスト内の各要素に processor 関数を適用します。"""
results = []
for item in items:
results.append(processor(item))
return results
# 異なる処理関数
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# 同じリストを異なる方法で処理する
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]このパターンは非常に便利なので、Python には map() や filter() のように、同様の形で動く組み込み関数が用意されています(これらは 23.6 節で取り上げます)。
23.2.3) key 関数を渡してソートする(簡単な導入)
Python の sorted() 関数は key パラメータを受け取ります。これは、アイテムをどのように比較するかを決める関数です:
# 学生を異なる基準でソートする
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# 成績を取り出す関数
def get_grade(student):
return student["grade"]
# 名前を取り出す関数
def get_name(student):
return student["name"]
# 成績でソート(昇順)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# 名前でソート(アルファベット順)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95key 関数は各要素につき1回呼び出され、その戻り値が比較に使われます。これはカスタムのソートロジックを書く必要がある場合に比べて、はるかに柔軟です。
このように、関数を渡して振る舞いをカスタマイズするパターンは Python では非常に一般的です。より高度なソート手法は第38章で詳しく見ていきます。
23.3) 関数から関数を返す
23.3.1) 関数を作る関数
引数として関数を渡せるのと同様に、他の関数から関数を返すこともできます。これにより、動的に特化した関数を作成できます:
# 新しい関数を作成して返す関数
def create_multiplier(factor):
"""指定した factor を掛ける関数を作成します。"""
def multiplier(x):
return x * factor
return multiplier
# 特化した乗算関数を作成する
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# 作成した関数を使う
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50ここで何が起きているのでしょうか。create_multiplier 関数は multiplier という内部関数(inner function)を定義して返しています。create_multiplier を異なる factor で呼び出すたびに、その特定の factor を「記憶する」新しい関数が返ってきます。これが クロージャ(closures) の最初の一端で、次のセクションで深掘りします。
23.3.2) カスタマイズされたバリデータを作る
関数を返すことは、カスタマイズされた検証(validation)や処理用の関数を作るのに特に便利です:
# 範囲バリデータを動的に作成する
def create_range_validator(min_value, max_value):
"""数値が範囲内かどうかを検証する関数を作成します。"""
def validator(number):
return min_value <= number <= max_value
return validator
# 特定のバリデータを作成する
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# バリデータを使う
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) クロージャの理解: 記憶する関数
23.4.1) クロージャとは?
クロージャ(closure) とは、作成されたスコープの変数を「記憶する」関数のことで、そのスコープの実行が完了した後でも、その変数にアクセスできます。23.3 節の例では、名前を明示していないだけで、すでにクロージャを使っています。
クロージャがどのように動くかを見てみましょう:
def create_counter(start=0):
"""カウントを記憶するカウンタ関数を作成します。"""
count = start # この変数はクロージャに「捕捉」されます
def counter():
nonlocal count # 捕捉した変数にアクセスする
count += 1
return count
return counter
# 2つの独立したカウンタを作成する
counter1 = create_counter(0)
counter2 = create_counter(100)
# 各カウンタはそれぞれのカウントを維持する
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)内部の counter 関数は count 変数に対してクロージャを形成します。create_counter の実行が終わっていても、返された counter 関数は count にアクセスし続けられます。create_counter を呼び出すたびに、それぞれ独立した count 変数を持つ新しいクロージャが作られます。
23.4.2) クロージャはどのように変数を捕捉するか
ある関数が別の関数の内側で定義されると、外側の関数のスコープにある変数へアクセスできます。これらの変数は「捕捉」され、外側の関数が return した後でもアクセス可能なままです。
Python が内部関数(inner function)を作成するとき、関数コードを保存するだけでなく、内部関数が使用する外側の関数の変数への参照も保存します。このプロセスを、変数を「捕捉(capturing)」すると呼びます。
def create_greeter(greeting):
"""カスタムの挨拶を持つ挨拶関数を作成します。"""
def greet(name):
return f"{greeting}, {name}!"
return greet
# 異なる greeter を作成する
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# 各 greeter は固有の挨拶を記憶する
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!greeting パラメータはクロージャに捕捉されます。各 greeter 関数は、呼び出されるたびに使うための、捕捉された固有の greeting 値を持っています。
23.4.3) 実用例: 設定済み関数
クロージャは、あらかじめ設定済みの振る舞いを持つ関数を作るのに最適です:
# 異なる税率の価格計算機を作成する
def create_price_calculator(tax_rate):
"""特定の tax_rate を適用する計算機を作成します。"""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# 地域ごとの計算機を作成する
us_calculator = create_price_calculator(0.07) # 7% tax
uk_calculator = create_price_calculator(0.20) # 20% VAT
japan_calculator = create_price_calculator(0.10) # 10% consumption tax
# 地域ごとに価格を計算する
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) いつクロージャを使うべきか
クロージャは、特に次のような場合に役立ちます:
- 設定済みの振る舞いを持つ関数を作成したい
- クラスを使わずに関数呼び出し間で状態(state)を維持したい
- コンテキスト(context)を覚えておく必要があるコールバック関数(callback functions)を実装したい
- 特化した関数を生成する関数ファクトリ(function factories)を作りたい
23.5) 短い無名関数に lambda を使う
23.5.1) Lambda 式とは?
lambda 式(lambda expression) は、小さな無名関数(anonymous function)—名前のない関数—を作成します。lambda 式は、短い期間だけ必要なシンプルな関数があり、def で正式に定義したくない場合に便利です。
構文は次のとおりです:
lambda parameters: expressionlambda は(通常の関数と同様に)パラメータを受け取り、式(expression)を評価した結果を返します。簡単な例を示します:
# 通常の関数
def add(x, y):
return x + y
# 同等の lambda 式
add_lambda = lambda x, y: x + y
# どちらも同じように動作する
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8lambda 式は単一の式に制限されており、if、for、複数行のコードのような文(statements)を含められません。この制限により、lambda はシンプルで焦点の定まったものになります。
23.5.2) 引数としての Lambda 式
Lambda 式は、シンプルな関数を引数として渡したいが、別途名前付きの関数を定義したくない場合に特に力を発揮します:
# lambda を使って学生を成績でソートする
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Instead of defining a separate function:
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# We can use a lambda directly:
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95関数が単純で1回しか使わない場合、こちらのほうが簡潔です。lambda lambda student: student["grade"] は、student を受け取って grade を返す関数と等価です。
23.5.3) 複数パラメータの Lambda
lambda 式は、通常の関数と同様に複数のパラメータを取れます:
# lambda を使った電卓の演算
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# lambda 式を使う
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: Error条件式(x / y if y != 0 else "Error")は lambda の中で使える一方で、if 文(複数行が必要)は使えない点に注目してください。
23.5.4) lambda を使う場合と名前付き関数を使う場合
lambda 式を使うのは次のときです:
- 関数がとても単純(1つの式)である
- 関数が1回だけ使われる、または非常に局所的な文脈で使われる
- 名前付き関数を定義すると不要に冗長になる
名前付き関数を使うのは次のときです:
- 関数が複雑、または複数の文が必要である
- 関数が複数箇所で再利用される
- 明確さのために説明的な名前が必要である
- docstring が必要である
23.5.5) Lambda の制限と代替案
lambda 式には重要な制限があります:
# ❌ これは動きません - lambda には文を含められません
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ 代わりに条件式を使う
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ 複数の操作が必要なら、通常の関数を使う
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10lambda 式は特定の状況のためのツールです。コードがより明確で簡潔になるなら使いましょう。理解しづらくなるなら、代わりに通常の名前付き関数を使ってください。
23.6) シンプルな関数で map() と filter() を使う
23.6.1) map() 関数
map() 関数は、与えられた function をイテラブル(iterable)(リスト、タプル、文字列など)の各要素に適用し、その結果を含むイテレータ(iterator)を返します。明示的なループを書かずに、コレクション内のすべての要素を変換する方法です。
map(function, iterable, *iterables)パラメータ:
function(必須): 1つ以上の引数を受け取り、それらを処理して値を返す関数です。この関数はiterable(s)の各要素につき1回呼び出されます。iterable(必須): 要素がfunctionに渡されるシーケンス(リスト、タプル、文字列など)です。*iterables(任意): 複数引数のfunction用の追加のイテラブルです。
複数のイテラブルが提供された場合、function はその数の引数を受け取らなければなりません
map() は最短のイテラブルが尽きた時点で停止します
戻り値:
各入力要素に対して function が返した結果を含む map オブジェクト(イテレータ)。
重要: map オブジェクトはイテレータであり、list のようなシーケンスではありません。
# リスト内のすべての数を2倍にする
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# 各数に double を適用する
doubled = map(double, numbers)
result = list(doubled) # map オブジェクト(イテレータ)をリストに変換する
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) Lambda と map() を使う
lambda 式は、シンプルな変換において map() と非常に相性が良いです:
# 温度を摂氏から華氏へ変換する
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) filter() 関数
filter() 関数は、与えられた function を iterable の各要素に適用し、関数が True を返す要素だけを含むイテレータを返します。明示的なループを書かずに、コレクションから要素を選択する方法です。
filter(function, iterable)パラメータ:
function: 1つの引数を受け取り、それを評価してTrueまたはFalseを返す関数です。この関数はiterableの各要素につき1回呼び出されます。iterable:functionによってテストされる要素を持つシーケンス(リスト、タプル、文字列など)です。
戻り値:
function が True を返した要素だけを含む filter オブジェクト(イテレータ)。
重要: filter オブジェクトはイテレータであり、リストのようなシーケンスではありません。
例:
# 偶数だけを残す
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# 各数に is_even を適用し、True を返すものだけを残す
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # filter オブジェクトをリストに変換する
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) Lambda と filter() を使う
lambda 式は、簡潔なフィルタリングのために filter() と組み合わせてよく使われます:
# 合格した学生をフィルタする(grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) map() と filter() を組み合わせる
map() と filter() の処理を連結して、複雑な変換を行うこともできます:
# 偶数の平方を取得する
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# まず偶数を filter し、それから平方にする
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]視覚的な比較: map() vs filter()
主な違い:
map(): 関数を適用して すべての 要素を変換する → 出力は 同じ長さfilter(): 各要素をテストし、通過したものだけ残す → 出力は 同じか、より短い
この章では、Python の強力な関数型プログラミング(functional programming)機能を学びました。関数がファーストクラスオブジェクトとして、他のどんな値と同じように受け渡しできるため、柔軟で再利用可能なコードパターンを実現できることを学びました。関数が別の関数を返せることで、環境を記憶するクロージャが作れることも確認しました。簡潔な関数定義のために lambda 式を学び、map() と filter() を使ってコレクションをエレガントに処理しました。
これらの概念は、より高度な Python プログラミング手法の土台になります。第38章では、この知識を土台に、Python の最もエレガントな機能の1つであるデコレータ(decorators)を習得していきます。