まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。
今回から「ユーザー定義関数の引数」について扱います。それでは始めましょう!
Q.16-1
仮引数にリストなどのミュータブルオブジェクトを使う際に、初期値を None にする慣習的パターンの理由を述べよ。
問題背景
【1】ミュータブルオブジェクトとは
Pythonにおけるオブジェクトは、ミュータブル(変更可能) と イミュータブル(変更不可能) の2種類に分かれる。リスト、辞書、集合などはミュータブルオブジェクトであり、その内容を変更することができる。一方で、文字列やタプルはイミュータブルオブジェクトであり、一度作成されると内容を変更することができない。
# ミュータブルなリスト
lst = [1, 2, 3]
lst[0] = 10 # 【1】リストの内容を変更
# イミュータブルなタプル
tup = (1, 2, 3)
# tup[0] = 10 # 【2】エラー: タプルの内容は変更できない
【2】関数の引数におけるミュータブルオブジェクトの問題
関数において、ミュータブルオブジェクト(例えばリスト)を仮引数に使う場合、その初期値を None
にするという慣習が広く採用されている。この理由は、ミュータブルオブジェクトが関数の呼び出しによって変更される可能性があるため。
もし仮引数にミュータブルオブジェクト(例えばリスト)を直接初期化して渡すと、関数が複数回呼び出されるたびに同じリストが使い回されることになり、予期しない動作やバグを引き起こすことがある。
def append_to_list(lst=[]): # 【3】リストを初期値として設定
lst.append(1)
return lst
print(append_to_list()) # 【4】出力: [1]
print(append_to_list()) # 【5】出力: [1, 1] (同じリストが使い回される)
このように、デフォルト引数にミュータブルなオブジェクトを使用することは避けるべき。
None
を初期値にする理由
Pythonでは、ミュータブルオブジェクトをデフォルト引数にする場合、初期値を None に設定し、関数内で最初にその引数が None であるかどうかをチェックして、必要に応じて新たにリストなどを作成するというパターンが広く使われている。この方法により、関数が呼び出されるたびに新しいリストが作成され、同じリストを使い回す問題を防ぐことができる。
def append_to_list(lst=None): # 【6】初期値を None に設定
if lst is None: # 【7】引数が None の場合、新たにリストを作成
lst = []
lst.append(1) # 【8】リストに要素を追加
return lst
# 関数を呼び出す
print(append_to_list()) # 【9】出力: [1]
print(append_to_list()) # 【10】出力: [1](新しいリストが作成される)
【4】この方法の利点
・ 新しいリストが毎回作成される:関数が呼び出されるたびに新しいリストが作成されるため、同じリストを使い回すことがなく、予期しない副作用が発生しない。
・ 柔軟性:引数に None
を渡すことで、リストや他のミュータブルオブジェクトを柔軟に使用できる。
・ 可読性の向上:関数内でリストを初期化することが明示的であり、他の開発者がコードを理解しやすくなる。
解答例とコードによる実践
【A】 基礎的なコード例
# 【1】関数の定義
def append_to_list(lst=None): # 【2】デフォルト引数を None に設定
if lst is None: # 【3】引数が None の場合、新しいリストを作成
lst = [] # 【4】新しいリストを作成
lst.append(1) # 【5】リストに 1 を追加
return lst # 【6】リストを返す
# 【7】関数を呼び出す
print(append_to_list()) # 【8】出力: [1]
print(append_to_list()) # 【9】出力: [1](毎回新しいリストが作成される)
[1]
[1]
【B】応用的なコード例
# 【1】誤った関数:デフォルト引数 log に空の辞書を設定(この辞書は関数定義時に一度だけ生成される)
def log_message_bad(user, message, log={}):
# 【2】user が辞書に含まれていない場合
if user not in log:
# 【3】新しいユーザに空のリストを設定
log[user] = []
# 【4】ユーザのリストにメッセージを追加
log[user].append(message)
# 【5】更新されたログを返す
return log
# 【6】正しい関数:log の初期値を None にして、毎回新しく辞書を生成するようにする
def log_message_good(user, message, log=None):
# 【7】log が None の場合、新しく辞書を生成
if log is None:
# 【8】新しい空の辞書を作る
log = {}
# 【9】辞書に user がいなければ
if user not in log:
# 【10】その user に空のリストを設定
log[user] = []
# 【11】メッセージを追加
log[user].append(message)
# 【12】辞書を返す
return log
# 【13】誤った関数の呼び出し(最初)
log1 = log_message_bad("alice", "Login success")
# 【14】誤った関数の呼び出し(2回目)→ 前の内容が残っている
log2 = log_message_bad("bob", "Upload started")
# 【15】正しい関数の呼び出し(alice)
safe_log1 = log_message_good("alice", "Login success")
# 【16】正しい関数の呼び出し(bob)
safe_log2 = log_message_good("bob", "Upload started")
# 【17】誤った関数は内部の辞書 log を共有している → どちらのログにも前回の結果が残る
print("Unsafe Log:", log2)
# 【18】【19】正しい関数では、それぞれの呼び出しで新しい辞書が生成される → 安全
print("Safe Log 1:", safe_log1)
print("Safe Log 2:", safe_log2)
Unsafe Log: {'alice': ['Login success'], 'bob': ['Upload started']}
Safe Log 1: {'alice': ['Login success']}
Safe Log 2: {'bob': ['Upload started']}
コードの解説
- デフォルト引数の設定
lst=None
としてデフォルト引数にNone
を指定している。これにより、引数が渡されない場合にはlst
はNone
となる。 - リストの初期化
関数内でif lst is None:
としてlst
がNone
の場合に新しいリストを作成している。これにより、関数が呼び出されるたびに新しいリストが作成され、同じリストが使い回されることを防いでいる。 - 結果の表示
関数が2回呼び出されるが、それぞれ異なるリストが作成され、リスト内に1
が追加される。リストは毎回新しく作成されるため、[1]
と表示される。
本問のまとめ
・ ミュータブルオブジェクトをデフォルト引数として使う際に、初期値を None
にする慣習は、予期しない副作用を防ぐために重要。
・ None
を使って引数の初期化を遅延させ、関数内で適切に新しいオブジェクトを作成することで、関数の呼び出しごとに新しいリストが作成される。
・ この慣習により、同じオブジェクトを使い回すことによるバグやエラーを避け、安全で予測可能な動作を実現することができる。
Q.16-2
引数名を変更すると関数の外部呼び出しが壊れる場合がある理由を、特にキーワード引数として使用されていた場合に注目して説明せよ。
問題背景
【1】関数の引数とキーワード引数
Pythonでは、関数の引数として、位置引数とキーワード引数(keyword arguments)を使用することができる。位置引数は、関数呼び出し時に引数の位置に依存して値が渡される。一方、キーワード引数は、引数名を指定して値を渡す方法であり、順番に依存せず、明示的に名前を指定することで、呼び出し元から関数へ引数を渡すことができる。
例えば、以下のように関数を定義し、キーワード引数を使って呼び出すことができる。
def greet(name, age):
print(f"Hello {name}, you are {age} years old.")
greet(name="Alice", age=30)
この場合、name="Alice"
と age=30
はキーワード引数であり、引数の順番に関係なく、引数名を指定して値を渡すことができる。
【2】引数名の変更と外部呼び出しの壊れ
関数の引数名を変更すると、特にキーワード引数を使用している場合、呼び出し元でその引数名を変更する必要がある。引数名が変更されると、呼び出し元で使用している引数名との対応が取れなくなり、エラーが発生する可能性がある。これが「関数の外部呼び出しが壊れる」と言われる理由である。
例えば、次のように関数の引数名を変更した場合を考えよう。
def greet(name, age):
print(f"Hello {name}, you are {age} years old.")
greet(name="Alice", age=30) # 最初の呼び出し
引数名を変更した場合、呼び出し元のコードも変更しなければならない。
def greet(full_name, years_old): # 引数名を変更
print(f"Hello {full_name}, you are {years_old} years old.")
greet(name="Alice", age=30) # エラー: 引数名が一致しない
この場合、引数名が変更されたため、greet
関数を呼び出すときに name
と age
を渡そうとしても、full_name
と years_old
という新しい引数名に一致しないため、エラーが発生する。
【3】関数の引数名変更による影響
関数の引数名を変更すると、次のような問題が発生する。
・ エラーが発生する: 引数名が変更されたことにより、呼び出し元で指定された引数名と一致しなくなり、エラーが発生する。キーワード引数は名前に依存するため、引数名が一致しないと正しく値を渡すことができない。
・ 外部のコードが壊れる: 引数名を変更した場合、その関数を利用している他のコードも変更する必要がある。これにより、プログラム全体の修正が必要になり、保守性が低下する。
解答例とコードによる実践
【A】基礎的なコード例
# 【1】最初の関数定義
def greet(name, age): # 【2】引数名を name と age として定義
print(f"Hello {name}, you are {age} years old.") # 【3】名前と年齢を表示
# 【4】キーワード引数を使用して関数を呼び出す
greet(name="Alice", age=30) # 【5】引数名を指定して呼び出し
Hello Alice, you are 30 years old.
引数名を変更した場合は以下
# 【6】引数名を変更した関数
def greet(full_name, years_old): # 【7】引数名を変更
print(f"Hello {full_name}, you are {years_old} years old.") # 【8】変更後の引数を使用
# 【9】呼び出し時に引数名が一致しないため、エラーが発生
greet(name="Alice", age=30) # 【10】エラー:引数名が変更されているため一致しない
TypeError: greet() got an unexpected keyword argument 'name'
コードの解説
・ 最初の関数定義と呼び出し
最初に greet
関数を定義し、引数 name
と age
を使用して呼び出しています。この場合、呼び出し元で name="Alice"
と age=30
を使って関数を正しく呼び出しています。
・ 引数名の変更とエラー発生
関数の引数名を name
と age
から full_name
と years_old
に変更しました。その後、呼び出し元では旧引数名(name
と age
)を使ったまま関数を呼び出しているため、エラーが発生します。エラーメッセージは、引数名が一致しないことを示しています。
【B】応用的なコード例
# 【1】新しい内部関数:引数名を変更したバージョン(呼び出しはこの関数を使う)
def _calculate_area_internal(w, h): # 【1】
return w * h # 【2】
# 【3】外部互換性を保つラッパー関数:元の引数名で受け取る
def calculate_area(*, width=None, height=None, **kwargs): # 【3】
if width is None or height is None: # 【4】引数不足チェック
raise ValueError("Both 'width' and 'height' must be specified.") # 【5】
return _calculate_area_internal(w=width, h=height) # 【6】
# 【7】既存の呼び出しコードは影響を受けない(引数名は旧名を使用)
result1 = calculate_area(width=3, height=5) # 【7】
print("Calculated Area:", result1) # 【8】出力: 15
# 【9】キーワード引数を柔軟に受け取れる:将来的な拡張にも対応可能
extra_args = {"width": 4, "height": 6, "unit": "cm^2"} # 【9】
result2 = calculate_area(**extra_args) # 【10】
print("Calculated Area (via kwargs):", result2) # 【11】出力: 24
Calculated Area: 15
Calculated Area (via kwargs): 24
本問のまとめ
・ 関数の引数名を変更すると、その関数を呼び出す外部コードも変更しなければならなくなる。特に、キーワード引数を使って関数を呼び出す場合、引数名に依存するため、引数名が変更されると外部呼び出しが壊れる。
・ キーワード引数は引数名を指定して値を渡すため、引数名の変更が呼び出し元で壊れる原因となる。そのため、引数名を変更する場合には、呼び出し元のコードもすべて修正する必要がある。
・ 引数名を変更する際は、関数の使用箇所が広範囲にわたる場合、呼び出し元のコードの修正も忘れずに行うことが重要である。
Q.16-3
*args
, **kwargs
を仮引数に含む関数に対して、異なる実引数の与え方がどのように動作するかを動的検査で記述せよ。
問題背景
【1】*args
と **kwargs
の概要
Pythonでは、関数定義において引数の個数が不定である場合に、*args
と **kwargs
を使うことができる。
*args
: 位置引数(位置に依存する引数)をタプルとして受け取るための構文である。*args
によって、任意の個数の位置引数を受け取ることができる。
**kwargs
: キーワード引数(名前付き引数)を辞書として受け取るための構文である。**kwargs
によって、任意の個数のキーワード引数を受け取ることができる。
【2】*args
と **kwargs
の動的検査
*args
と **kwargs
に与える引数は、関数呼び出し時に動的に決定される。これにより、関数呼び出し時に与えた引数の個数やタイプに応じて、関数内でどのように処理されるかが決まる。
*args
は、位置引数を順番に受け取る。
**kwargs
は、キーワード引数を名前と値のペアで受け取る。
関数に与える引数の与え方によって、これらの動作がどのように異なるかを検査することができる。
【3】異なる実引数の与え方
以下に、*args
と **kwargs
を使った関数定義と、異なる引数の与え方について説明する。
・ 位置引数を直接渡す
位置引数をそのまま渡すことができる。
・ キーワード引数を渡す
名前付き引数を渡すことができる。
・ *args
と **kwargs
の両方を使う
*args
と **kwargs
を同時に使うこともでき、その場合の順番や構文に注意する必要がある。
解答例とコードの実践
【A】基礎的なコード例
# 【1】関数定義
def test_function(*args, **kwargs): # 【2】*args と **kwargs を受け取る関数
print(f"args: {args}") # 【3】位置引数を出力
print(f"kwargs: {kwargs}") # 【4】キーワード引数を出力
# 【5】位置引数とキーワード引数を混在させて呼び出し
test_function(1, 2, 3, name="Alice", age=30) # 【6】位置引数 (1, 2, 3) と キーワード引数 (name="Alice", age=30) を渡す
args: (1, 2, 3)
kwargs: {'name': 'Alice', 'age': 30}
【B】応用的なコード例
# 【1】任意個の位置引数とキーワード引数を受け取る関数を定義
def inspect_args(*args, **kwargs):
# 【2】位置引数をタプル形式で表示
print(f"args (tuple): {args}")
# 【3】キーワード引数を辞書形式で表示
print(f"kwargs (dict): {kwargs}")
# 【4】実行確認用のメッセージを出力
print("=== Test 1: Positional Only ===")
# 【5】位置引数のみを渡して呼び出す
inspect_args(1, 2, 3)
# 【6】実行確認用のメッセージ
print("\n=== Test 2: Keyword Only ===")
# 【7】キーワード引数のみを渡して呼び出す
inspect_args(a=10, b=20)
# 【8】実行確認用のメッセージ
print("\n=== Test 3: Mixed Positional + Keyword ===")
# 【9】両方混合して渡す
inspect_args(100, 200, x="foo", y="bar")
# 【10】実行確認用のメッセージ
print("\n=== Test 4: Using *args and **kwargs ===")
# 【11】リストとしての位置引数
args_list = [7, 8]
# 【12】辞書としてのキーワード引数
kwargs_dict = {"alpha": "A", "beta": "B"}
# 【13】リストと辞書を展開して関数に渡す
inspect_args(*args_list, **kwargs_dict)
# 【14】実行確認用のメッセージ
print("\n=== Test 5: No Arguments ===")
# 【15】引数を一切渡さずに呼び出す
inspect_args()
=== Test 1: Positional Only ===
args (tuple): (1, 2, 3)
kwargs (dict): {}
=== Test 2: Keyword Only ===
args (tuple): ()
kwargs (dict): {'a': 10, 'b': 20}
=== Test 3: Mixed Positional + Keyword ===
args (tuple): (100, 200)
kwargs (dict): {'x': 'foo', 'y': 'bar'}
=== Test 4: Using *args and **kwargs ===
args (tuple): (7, 8)
kwargs (dict): {'alpha': 'A', 'beta': 'B'}
=== Test 5: No Arguments ===
args (tuple): ()
kwargs (dict): {}
コードの解説
・ 関数の定義
関数 test_function
は、*args
と **kwargs
を受け取る引数を持っている。この関数は、位置引数(*args
)とキーワード引数(**kwargs
)をそれぞれ表示する。
・ 位置引数とキーワード引数を混在させて呼び出し
関数 test_function
を呼び出す際に、位置引数 1, 2, 3
とキーワード引数 name="Alice", age=30
を渡している。*args
は位置引数をタプルとして受け取り、**kwargs
はキーワード引数を辞書として受け取る。
本問のまとめ
・ *args
は位置引数を受け取り、タプルとして関数内で使用できる。**kwargs
はキーワード引数を受け取り、辞書として関数内で使用できる。
・ 位置引数とキーワード引数 を一緒に渡す場合、*args
に位置引数が、**kwargs
にキーワード引数が渡される。
・ *args
と **kwargs
を両方使う場合、*args
を先に、**kwargs を後に記述する必要がある。順番を間違えると構文エラーが発生する。
・ 動的に引数を渡すことで、関数の呼び出し時に柔軟な引数の設定が可能であり、特に不定個数の引数を受け取る場合に便利である。
Q.16-4
仮引数の定義順序(位置引数→デフォルト→可変長引数→キーワード)を誤ると構文エラーになる理由とその修正方法を説明せよ。
問題背景
【1】仮引数の種類と順序
Pythonの関数定義では、複数の種類の引数を使用することができる。これらの引数には、位置引数、デフォルト引数、可変長引数、およびキーワード引数がある。引数は定義時に特定の順序で記述する必要があり、この順序を誤ると構文エラーが発生する。
Pythonでの仮引数の順序は以下の通り。
・ 位置引数: 最初に記述されるべきで、位置に依存して値が渡される引数である。
・ デフォルト引数: 仮引数にデフォルト値を設定する場合、位置引数の後に記述されるべきである。
・ 可変長引数 (*args
): 複数の位置引数をタプルとして受け取る引数で、デフォルト引数の後に記述される。
・ キーワード引数 (**kwargs
): 複数のキーワード引数を辞書として受け取る引数で、*args
の後に記述される。
この順序を守らない場合、構文エラーが発生する。
####【2】順序を誤った場合のエラー
順序を誤った場合、Pythonはその関数定義を正しく解釈できず、エラーが発生する。例えば、位置引数の後に *args
や **kwargs
を書いたり、デフォルト引数を可変長引数の前に書いたりすると、Pythonはそれらを正しく扱うことができないため、構文エラーを発生させる。
【3】構文エラーの原因
Pythonの関数定義における引数の順序は厳格である。例えば、*args
は位置引数やデフォルト引数の後に書かれ、**kwargs
は *args
の後に書かれなければならない。この順序を誤ると、Pythonはそれを正しい関数定義として解釈できず、エラーを発生させる。
解答例とコードによる実践
【A】 順序を守った関数定義
# 【1】位置引数、デフォルト引数、可変長引数、キーワード引数の順に定義
def example_function(a, b=10, *args, **kwargs): # 【2】順序通りに引数を並べる
print(f"a: {a}, b: {b}") # 【3】位置引数とデフォルト引数を表示
print(f"args: {args}") # 【4】可変長引数を表示
print(f"kwargs: {kwargs}") # 【5】キーワード引数を表示
# 【6】関数呼び出し
example_function(1, 2, 3, 4, 5, name="Alice", age=30) # 【7】位置引数、可変長引数、キーワード引数を渡す
a: 1, b: 2
args: (3, 4, 5)
kwargs: {'name': 'Alice', 'age': 30}
コードの解説
・ 位置引数 (a) は必須であり、最初に渡されるべきである。
・ デフォルト引数 (b) は、位置引数の後に続き、必要に応じてデフォルト値が使われる。
・ 可変長引数 (*args) は、任意の数の位置引数を受け取るため、デフォルト引数の後に記述される。
・ キーワード引数 (**kwargs) は、最後に記述され、名前付き引数として受け取られる。
このように、順番を守ることで、関数定義が正しく動作する。
【B】引数順序を誤った場合の構文エラー
# 【1】誤った順序で関数を定義
def invalid_function(a, *args, b=10, **kwargs): # 【2】順序が間違っている
print(f"a: {a}, b: {b}") # 【3】位置引数とデフォルト引数を表示
print(f"args: {args}") # 【4】可変長引数を表示
print(f"kwargs: {kwargs}") # 【5】キーワード引数を表示
SyntaxError: non-default argument follows default argument
コードの解説
・ エラー原因
*args
をデフォルト引数 b=10
の前に配置したため、Pythonは b=10
が位置引数より後ろにあることを期待しない。位置引数やデフォルト引数は必ず *args
より前に定義されなければならない。このため、「non-default argument follows default argument
」という構文エラーが発生する。
【C】応用的なコード例
# 【1】関数を定義:順序は「位置→デフォルト→*args→キーワード専用→**kwargs」の正しい順
def fixed_function(a, b=1, *args, c=100, **kwargs):
# 【2】位置引数aを表示
print(f"a: {a}")
# 【3】デフォルト引数b(指定がなければ1)を表示
print(f"b (default): {b}")
# 【4】追加の位置引数(*args)を表示(タプル)
print(f"args: {args}")
# 【5】キーワード専用引数c(明示的にc=…と指定しないと渡せない)を表示
print(f"c (keyword-only): {c}")
# 【6】その他のキーワード引数(辞書として受け取る)を表示
print(f"kwargs: {kwargs}")
# 【7】関数呼び出し:a=10, b=20, args=(30, 40), c=200, kwargs={'x': 9, 'y': 8}
fixed_function(10, 20, 30, 40, c=200, x=9, y=8)
a: 10
b (default): 20
args: (30, 40)
c (keyword-only): 200
kwargs: {'x': 9, 'y': 8}
本問のまとめ
・ 引数の順序は厳格である。位置引数は最初に、デフォルト引数はその後、可変長引数(*args
)はその後、キーワード引数(**kwargs
)は最後に定義する必要がある。
・ 順序を誤ると構文エラーが発生する。特に、デフォルト引数の後に位置引数や可変長引数を置くとエラーになる。
・ 順序を守ることで関数が正しく動作し、呼び出し時にエラーが発生しないようになる。
Q.16-5
実引数として関数を渡すことで、引数の型の柔軟性と関数設計の拡張性がどのように高まるかを論ぜよ。
問題背景
【1】関数は「第一級オブジェクト」
Pythonでは**関数が第一級オブジェクト(first-class object)**である。これは、関数を以下のように扱えることを意味する:
・変数に代入できる
・他の関数に引数として渡せる
・関数から関数を返せる(高階関数)
・リストなどのコレクションに格納できる
これにより、関数を動的に扱う柔軟な設計が可能となる。
【2】「関数を引数に取る関数」=高階関数(higher-order function)
Pythonでは、ある関数に別の関数を引数として渡すことがよく行われる。これを用いることで、関数の処理の一部だけを外部から変更でき、柔軟なカスタマイズや処理の抽象化が可能になる。
list(map(str.upper, ["apple", "banana"]))
ここで str.upper
という「関数」を map
に渡している。
【3】型の柔軟性と設計の拡張性
関数を引数にすることで、以下のようなメリットがある:
・型に依存しない汎用的な関数設計が可能
・処理の振る舞いを外部から指定できる
・単体テストや保守がしやすくなる
この手法は、関数型プログラミングの考え方や、戦略パターン(Strategy Pattern)などのオブジェクト指向設計にも通じる。
# 【1】関数を引数に取る高階関数を定義する
def apply_to_list(data, func): # 【1】
# data: 任意のリスト
# func: 各要素に適用する関数
return [func(x) for x in data] # 【2】funcを使ってリストの各要素を変換
# 【3】変換対象のリストを用意する
numbers = [1, 2, 3, 4] # 【3】整数のリストを用意
# 【4】平方計算用の関数を定義する
def square(x): # 【4】
return x ** 2 # 【5】渡された値を2乗して返す
# 【6】平方関数を適用する
squared = apply_to_list(numbers, square) # 【6】高階関数にsquare関数を渡す
# 【7】結果を表示する
print(squared) # 【7】出力: [1, 4, 9, 16]
[1, 4, 9, 16]
解答例とコードによる実践
# 【1】関数 apply_pipeline は、データリストと処理関数のリストを受け取り、すべて順番に適用する
def apply_pipeline(data, funcs): # 【1】
for func in funcs: # 【2】処理関数を1つずつ適用
data = [func(x) for x in data] # 【3】各要素に現在の関数を適用
return data # 【4】最終的に処理されたリストを返す
# 【5】対象となる文字列リスト(前処理対象データ)
texts = [" Alice ", "Bob", "CHARLIE ", " dave"] # 【5】
# 【6】空白を除去する関数
def strip_spaces(s): # 【6】
return s.strip() # 【7】前後の空白を除去
# 【8】小文字に変換する関数(lambda式で定義)
to_lower = lambda s: s.lower() # 【8】
# 【9】先頭を大文字、それ以外を小文字にする関数
def capitalize_name(s): # 【9】
return s.capitalize() # 【10】最初の文字だけ大文字に
# 【11】パイプラインとして処理関数をリストに格納
pipeline = [strip_spaces, to_lower, capitalize_name] # 【11】
# 【12】apply_pipelineを用いて一連の処理を実行
processed = apply_pipeline(texts, pipeline) # 【12】
# 【13】結果の出力
print(processed) # 【13】出力: ['Alice', 'Bob', 'Charlie', 'Dave']
['Alice', 'Bob', 'Charlie', 'Dave']
本問のまとめ
Pythonにおいて関数を実引数として他の関数に渡すことで、型に依存しない抽象化された柔軟な処理が可能になる。この設計は関数の一部の振る舞いを外部から切り替えられるため、汎用性が高く、拡張にも強い構造となる。また、テストや保守性にも優れるため、実務においても有用である。
あとがき
今回は「ユーザー定義関数における引数」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「ユーザー定義関数における戻り値」について扱う予定です。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)