0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

演習形式で学ぶPythonプログラミング vol.17 ~関数の戻り値~

Posted at

まえがき

Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。

今回から「ユーザー定義関数の戻り値」について扱います。それでは始めましょう!

Q.17-1

関数の戻り値を変数に格納せずに連鎖的に利用する設計(例:print(sorted(get_data())))の可読性と拡張性を評価せよ。

問題背景

【1】関数の「戻り値の連鎖利用」とは

Pythonにおいて関数は戻り値(return値)を持つオブジェクトであり、それを直ちに他の関数の引数に渡す構文がしばしば用いられる。たとえば以下のようなコード:

print(sorted(get_data()))

このような記述では、関数 get_data() の戻り値が即座に sorted() に渡され、さらにその結果が print() に渡される。これは関数の連鎖的利用(function chaining)と呼ばれる。

【2】利点と欠点

・利点(簡潔性と即時性)
中間変数を使わずに記述できるため、コードが短くなる
一時的な処理や副作用のない関数に対して有効
パイプライン的な処理を簡潔に表現できる

・欠点(可読性と拡張性)
中間結果を参照・再利用できない
処理の各段階に名前をつけられないため、意味が見えにくくなる
デバッグや単体テストが困難になる
処理を後で変更・挿入する際に、全体を見直す必要がある

したがって、状況に応じた使い分けが求められる。

解答例とコードによる実践

# 【1】仮のデータを取得する関数(例:データベースやAPIから取得)
def get_data():  # 【1】
    return ["banana", "apple", "cherry"]  # 【2】リストを返す

# 【3】構文1:関数の戻り値を変数に格納せずに連鎖的に利用(簡潔だが拡張性は低い)
print("=== Inline chaining ===")  # 【3】
print(sorted(get_data()))  # 【4】get_data() の戻り値を直接 sorted() に渡し、結果を print する

# 【5】構文2:中間結果に名前をつけて可読性と拡張性を高めた形式
print("\n=== Named intermediate steps ===")  # 【5】
data = get_data()  # 【6】get_data() の結果を変数 data に格納
sorted_data = sorted(data)  # 【7】data を sorted に渡し、結果を別名で保持
print(sorted_data)  # 【8】整形済みのリストを出力
実行結果
=== Inline chaining ===
['apple', 'banana', 'cherry']

=== Named intermediate steps ===
['apple', 'banana', 'cherry']

本問のまとめ

Pythonにおける関数の戻り値の連鎖的利用は、構文上の簡潔性という利点を持つ一方で、可読性・拡張性・保守性においては中間変数による分割記述に劣る場合がある。以下に、評価軸ごとの比較を示す:

評価軸 連鎖的記述(Chaining) 分割記述(Named Steps)
簡潔さ 高い 低い(行数が増える)
可読性 低い(複雑になりやすい) 高い(処理単位が明確)
拡張性 低い(挿入・変更に弱い) 高い(処理単位で変更可)
再利用性 低い(再呼び出し不可) 高い(途中結果を使える)

結論として、スクリプトのような一時的処理や非常に単純な関数連鎖では chaining は有効だが、中〜大規模の処理・保守対象のコードでは、中間変数を用いた段階的処理のほうが適していると評価される。

Q.17-2

複数の return がある関数において、それらが返す値の型が異なるとき、呼び出し側でどのような対応が必要かを記述せよ。

問題背景

【1】Python関数は異なる型を返すことが可能である

Pythonの関数は、複数の return 文を含み、異なる状況に応じて異なる型の値を返すことができる。これは動的型付け言語であるPythonの柔軟性の一つであるが、呼び出し側で戻り値の型に応じた処理を行わないと、エラーやバグの温床になりうる。

def f(x):
    if x > 0:
        return x * 2    # int型
    else:
        return "error"  # str型

このような関数の呼び出し元では、f()int を返すか str を返すかを事前に予測し、対応する処理を設計する必要がある。

【2】想定される問題と対処方法

課題 典型的な失敗例 解決策
型が異なると処理が失敗する "error" + 1TypeError を引き起こす isinstance() による判定
IDEや型チェッカーが警告を出さない Pythonでは静的型付けがないため、型誤りを見逃しやすい typing モジュールによる型注釈
処理の分岐が煩雑になる 多くの if 文で型ごとに処理が分かれて読みにくい 明確な return ポリシーと Union の導入

【3】推奨される呼び出し側の設計方針

isinstance() による戻り値の型判定
・ 戻り値に "status""type" などのメタ情報を埋め込んで分岐
・ 型ヒント(Union, Optional)による明示的な設計

解答例とコードによる実践

def process_input(data):
    # 【1】dataが数値ならば2乗して返す
    if isinstance(data, (int, float)):  # 【1】dataが数値型(intまたはfloat)であるかを確認する
        return data ** 2  # 【2】dataの2乗を計算して返す(型: intまたはfloat)

    # 【3】dataがリストなら、長さを返す
    elif isinstance(data, list):  # 【3】dataがリストであるかを確認する
        return len(data)  # 【4】リストの要素数を返す(型: int)

    # 【5】それ以外ならエラーメッセージを返す
    else:
        return {"error": "Unsupported type"}  # 【5】辞書型でエラー情報を返す(型: dict)

# 呼び出し側の処理(各戻り値の型に応じて分岐)

inputs = [5, [1, 2, 3], "hello"]  # 【6】int型、list型、str型の3つの異なる入力を用意する

for item in inputs:  # 【7】各入力に対してループを行う
    result = process_input(item)  # 【8】関数を呼び出し、戻り値をresultに格納する

    # 【9】戻り値の型ごとに処理を分岐する
    if isinstance(result, (int, float)):  # 【9】戻り値が数値であれば
        print(f"数値結果: {result}")  # 【10】数値として出力する

    elif isinstance(result, dict) and "error" in result:  # 【11】エラー辞書であれば
        print(f"エラー: {result['error']}")  # 【12】エラーメッセージを出力する

    else:
        print(f"その他の出力: {result}")  # 【13】それ以外のケース(リストの長さなど)を出力する
実行結果
数値結果: 25
数値結果: 3
エラー: Unsupported type

まとめ

・ 関数が返す値の型が複数ある場合は、呼び出し側で明確な型判定と分岐処理を行うことが必要である。
・ Pythonでは isinstance() を使って柔軟に型判定ができ、さらに dict を返すことで処理のメタ情報も同時に提供できる。
typing.Union による型注釈は、開発者にとってのドキュメントとなり、IDEによる補完や静的解析を助ける。
・ 型が複数になるような関数設計を行う場合、戻り値の統一性と明確な仕様の共有が保守性を高める鍵である。

Q.17-3

関数が副作用を含む処理を行う場合、その戻り値の有無と意味を明確化する設計方針を述べよ。

問題背景

【1】副作用を持つ関数とは何か

Pythonにおける副作用(side effect)とは、「関数が外部の状態を変化させる処理」を指す。これは純粋関数(pure function)の対義概念である。

純粋関数の特徴
・同じ入力に対して必ず同じ出力を返す
・関数の外部に影響を及ぼさない(副作用を持たない)

副作用を持つ関数の例
・ファイルやデータベースへの書き込み
・グローバル変数の変更
・ユーザーへの通知(print, log, alert)
・外部APIの呼び出し

副作用は現実のプログラムでは避けて通れない。しかし、副作用が「成功したか」「何を起こしたか」が分からないとバグの原因となる。

【2】副作用を含む関数における戻り値の設計指針

戻り値の形式 意味 利点
None 出力の必要がない副作用(例:ログ) 処理が意図的に完了したことを示す
bool 成否だけを示す 呼び出し側で簡潔に分岐可能
strdict 成否 + エラー情報 + 副作用の内容 ロギング・エラーハンドリングと組み合わせやすい
カスタムクラス 状態と結果をオブジェクトとして保持 可読性・拡張性が高く、設計が疎結合になりやすい

副作用の結果に意味を持たせたいときは戻り値を利用する。逆に、意図的に「戻り値に依存させたくない」処理なら None を明示的に返すことが良い設計とされる。

【3】副作用と戻り値の設計原則(ベストプラクティス)

・副作用に成功したかどうかを必ず戻り値で伝える
・副作用の内容(影響対象、処理結果)を必要に応じて構造化して返す
・処理失敗の詳細を戻り値または例外で渡す(ログやUI表示へ)
・副作用処理を「値の返却」と混在させない設計を優先する(副作用関数を別に切り出す)

解答例とコードによる実践

【A】基礎的なコード例

# 副作用(ファイル書き込み)と戻り値の両立設計を示す
import os

def save_user_data(username: str, score: int, file_path: str = "user_data.txt") -> bool:
    # 【1】関数名や引数で、ファイル書き込みが行われることが明示されている
    try:
        # 【2】指定されたファイルに書き込みを行う(副作用)
        with open(file_path, "w") as f:
            f.write(f"{username},{score}\n")  # 【3】CSV形式で保存する
        return True  # 【4】成功した場合はTrueを返す
    except Exception as e:
        print(f"エラー発生: {e}")  # 【5】エラー内容を表示(副作用)
        return False  # 【6】失敗時はFalseを返す

# 呼び出し側の設計:戻り値に応じて処理を分岐

# 【1】ユーザー名とスコアを定義する
user = "alice"  # 【1】
score = 85  # 【2】

# 【3】関数を呼び出し、副作用とともに成功可否を受け取る
result = save_user_data(user, score)  # 【3】

# 【4】戻り値によって結果の確認と処理の分岐を行う
if result:  # 【4】戻り値がTrue(成功)なら
    print("データの保存に成功しました")  # 【5】
else:
    print("データの保存に失敗しました")  # 【6】
実行結果(成功時)
データの保存に成功しました
実行結果(失敗時)
エラー発生: [Errno 13] Permission denied: '/root/user_data.txt'
データの保存に失敗しました

【B】応用的なコード例

# ユーザーデータをJSONファイルに保存する処理
# 成功すれば保存先パスと保存件数を返す
# 失敗すれば error フィールドを含む辞書を返す
# 保存は副作用、戻り値は処理の意味を表す

import json
import os
from datetime import datetime
from typing import Union, Dict

def save_users_to_json(users: list[dict], directory: str = "output") -> Union[Dict[str, Union[str, int]], Dict[str, str]]:
    # 【1】保存先ディレクトリが存在しない場合は作成する
    if not os.path.exists(directory):  # 【1】
        os.makedirs(directory)  # 【2】副作用:フォルダを作成

    # 【3】ファイル名にタイムスタンプを含めて保存する
    filename = f"users_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"  # 【3】
    path = os.path.join(directory, filename)  # 【4】

    try:
        with open(path, "w", encoding="utf-8") as f:  # 【5】
            json.dump(users, f, ensure_ascii=False, indent=2)  # 【6】副作用:ファイルに書き込み
        return {  # 【7】
            "status": "success",
            "saved_path": path,
            "user_count": len(users)
        }
    except Exception as e:
        return {  # 【8】
            "status": "error",
            "error": str(e)
        }

# 呼び出し側の処理(戻り値の意味に応じた分岐)
# 【1】保存対象のユーザーリストを定義する
users = [  # 【1】
    {"name": "Alice", "score": 90},  # 【2】
    {"name": "Bob", "score": 75}     # 【3】
]

# 【4】関数を呼び出して、戻り値に応じて処理を分岐する
result = save_users_to_json(users)  # 【4】

# 【5】結果が正常終了かどうかを判定する
if result.get("status") == "success":  # 【5】
    print(f"{result['user_count']}件のユーザーを保存しました: {result['saved_path']}")  # 【6】
else:
    print(f"保存に失敗しました: {result['error']}")  # 【7】
実行結果(成功時)
2件のユーザーを保存しました: output/users_20250507_153322.json
実行結果(失敗時)
保存に失敗しました: [Errno 13] Permission denied: 'output/...'

まとめ

・関数が副作用(例:ファイル書き込み)を含む場合、戻り値はその実行結果の明示的な「報告書」であるべきである。
None を返すだけの設計では、呼び出し側が処理の成否を把握できず、再現困難なバグの原因となる。
・Pythonicな設計では、処理の意図と結果を戻り値で表し、副作用を明確に制御することが好まれる。
・拡張性が高い設計として、戻り値を辞書化またはクラス化し、状態・メタ情報・成否を構造化する手法が有効である。

Q.17-4

関数の戻り値を後続処理で if 条件判定に利用する場合、真偽値以外の戻り値(リストや空文字列など)がどう評価されるかを説明せよ。

問題背景

【1】Python における if 判定の基本

Pythonの if 文では、式の**真偽値(boolean value)**に基づいて条件を評価する。つまり、

if 条件式:
    # True のときに実行される

ここで「条件式」が True であると判断されるためには、暗黙の型変換(truth value testing)において真と判定される必要がある。

【2】「真偽値ではない値」の評価基準(truthy / falsy)

Pythonでは、真偽値(True, False)以外の値も、その値の“空”かどうかなどに応じて暗黙的に bool 型に変換される。

データ型 if での評価
数値 (int) 0 False(偽)
数値 (int) 非ゼロ(例:42) True(真)
文字列 (str) ""(空文字列) False(偽)
文字列 (str) "abc" True(真)
リスト (list) [](空リスト) False(偽)
リスト (list) [1, 2] True(真)
None None False(偽)

この仕様により、関数が戻り値として空のリストや空文字列を返した場合、それを if に渡すと False とみなされる。

【3】この評価が問題を引き起こす例

以下のようなコードを見てみよう。

def get_items():
    return []

if get_items():
    print("アイテムがあります")
else:
    print("アイテムは空です")

このコードは "アイテムは空です" を出力する。
つまり、リストが空であることだけを判定しているにもかかわらず、if の構文上は明示的に len(items) == 0 などと書かなくても済む。

【4】なぜ注意が必要なのか

0, [], "", None などはすべて False 扱いになるため、意図しない分岐が発生することがある。
特に 0"" は意味のある戻り値(例:文字列処理や個数カウント)であるにもかかわらず、誤って False と判定してしまう危険性がある。
したがって、戻り値を if 条件に使う場合、その型と意味が適切に意図されているかを確認することが極めて重要である。

解答例とコードによる実践

以下のコードでは、関数 analyze_data が異なる入力に対して異なる型(リスト、文字列、None)を返し、それを if 条件でどのように評価すべきかを検証している。

# 関数定義:入力に応じて異なる戻り値(リストや文字列、None)を返す
def analyze_data(data):
    # 【1】データがリストであり、空でないならリストを返す
    if isinstance(data, list) and data:  # 【1】
        return data  # 【2】非空リスト(truthy)

    # 【3】文字列が空でないならメッセージを返す
    elif isinstance(data, str) and data.strip():  # 【3】
        return f"Received string: {data.strip()}"  # 【4】非空文字列(truthy)

    # 【5】その他の場合は None を返す
    else:
        return None  # 【5】明示的に偽(falsy)を返す


# 呼び出しと評価の例:さまざまな戻り値に対する if 判定
inputs = [
    [1, 2, 3],     # 【6】非空リスト
    [],            # 【7】空リスト
    "hello",       # 【8】非空文字列
    "     ",       # 【9】空白文字列(strip()後は空)
    None,          # 【10】None
    0,             # 【11】0(数値)
]

# それぞれの戻り値を取得し、if文で条件分岐
for i, val in enumerate(inputs):  # 【12】各入力データに対してループ処理
    result = analyze_data(val)  # 【13】関数の戻り値を取得

    print(f"[{i+1}] 入力: {repr(val)}")  # 【14】入力の内容を表示

    # 【15】戻り値を if 条件で評価(truthy / falsy の確認)
    if result:
        print(f"  → 条件判定: 真(True)として扱われた")  # 【16】
        print(f"  → 処理結果: {result}")  # 【17】
    else:
        print(f"  → 条件判定: 偽(False)として扱われた")  # 【18】
        print(f"  → 処理結果: {result}")  # 【19】

    print("-" * 50)  # 【20】出力の区切り
実行結果
[1] 入力: [1, 2, 3]
  → 条件判定: 真(True)として扱われた
  → 処理結果: [1, 2, 3]
--------------------------------------------------
[2] 入力: []
  → 条件判定: 偽(False)として扱われた
  → 処理結果: None
--------------------------------------------------
[3] 入力: 'hello'
  → 条件判定: 真(True)として扱われた
  → 処理結果: Received string: hello
--------------------------------------------------
[4] 入力: '     '
  → 条件判定: 偽(False)として扱われた
  → 処理結果: None
--------------------------------------------------
[5] 入力: None
  → 条件判定: 偽(False)として扱われた
  → 処理結果: None
--------------------------------------------------
[6] 入力: 0
  → 条件判定: 偽(False)として扱われた
  → 処理結果: None
--------------------------------------------------

まとめ

・Pythonにおいて if 文は真偽値だけでなく、任意のオブジェクトを暗黙的に評価する。
[]""0None など、「空」や「ゼロ」の状態を持つ値は False として扱われる。
・この仕様により、関数の戻り値を if に直接使う場合は、暗黙の bool() 評価が意図と合致するか確認する必要がある。
・より安全に書くには、次のように 明示的に型チェックや長さ判定を行う方が可読性と保守性に優れる:

if result is not None and isinstance(result, list) and len(result) > 0:
    ...

Q.17-5

yield を含む関数と return を含む関数の違いを、戻り値の観点から比較し、ジェネレータとの違いを示せ。

問題背景

【1】return は関数の「一度限りの終了と値の返却」

通常の関数において return は、「値を返して処理を終了する」という意味である。
return 文を実行した瞬間に関数の実行は停止し、それ以降の処理は一切実行されない。
return は1つの値(またはタプル、リストなど1つのオブジェクト)を返す。

def f():
    return 42

この関数は f() を呼び出すと 42 を返すが、返す値は1回限りで、それ以降の処理はない。

【2】yield は「値の逐次的な返却(遅延評価)」を行う

yield は、関数を一時停止して値を返すことを意味する。関数の状態を中断して保持する。
yield を含む関数を呼び出すと、即座には関数本体は実行されず、ジェネレータオブジェクトが返る。
for ループや next() を使ってそのジェネレータを評価すると、1つずつ値が生成される。

def g():
    yield 1
    yield 2

この関数は 逐次的に複数の値を返すことができるため、大量のデータ処理においてパフォーマンスとメモリ効率の面で非常に有用である。

【3】ジェネレータの特性

yield を含む関数を評価すると、ジェネレータ(generator)オブジェクトが生成される。
ジェネレータは イテレータ(iterator)であり、__iter__()__next__() を持つ。
状態が保存されるため、「次にどこから再開するか」が記録される。

gen = g()       # ジェネレータオブジェクトが返る(実行はされていない)
next(gen)       # 1 を返す。内部状態は 2 の位置で停止している

【4】主な違いの比較表

観点 return yield
処理の終了 その場で終了(後続処理なし) 状態を一時停止(再開可能)
戻り値 単一値(複数値でも1オブジェクト) イテレータとして複数の値を逐次返す
メモリ使用 すべての値を一度に生成 1つずつ遅延生成(メモリ効率が良い)
使われ方 通常の関数 for文、next関数、ジェネレータ式など

解答例とコードによる実践

以下では、「正規表現によって文字列から数字だけを抽出し、それを returnyield で処理する2通りの関数を比較」する。

import re

# 【1】returnを使って全数字を一括抽出する関数
def extract_numbers_return(text):
    # 【1】正規表現で全ての数字(連続したもの)をリストとして取得
    numbers = re.findall(r"\d+", text)  # 【2】
    return [int(n) for n in numbers]  # 【3】intに変換して一括返却

# 【2】yieldを使って数字を1つずつ返すジェネレータ関数
def extract_numbers_yield(text):
    # 【4】正規表現のイテレータを使って逐次的にマッチング
    for match in re.finditer(r"\d+", text):  # 【5】
        yield int(match.group())  # 【6】1つずつ int にして返却(逐次的)

# 【3】テスト対象の文字列
sample = "User A has 42 points, user B has 85 points, and user C has 0."  # 【7】

# 【4】return方式の結果を取得・表示
numbers_return = extract_numbers_return(sample)  # 【8】
print("【return使用】一括抽出の結果:", numbers_return)  # 【9】

# 【5】yield方式の結果を逐次取得・表示
print("【yield使用】逐次抽出の結果:", end=" ")  # 【10】
for number in extract_numbers_yield(sample):  # 【11】
    print(number, end=" ")  # 【12】
print()  # 【13】改行
実行結果
【return使用】一括抽出の結果: [42, 85, 0]
【yield使用】逐次抽出の結果: 42 85 0

まとめ

return は値を一度にまとめて返すのに対し、yield は処理の中断と再開を繰り返しながら値を1つずつ返す仕組みである。
yield を用いた関数はジェネレータ(遅延評価されるイテレータ)を返すため、特に「大量データ」「逐次処理」に適している。
メモリ消費やパフォーマンスの観点からは、一度にすべて処理する return に対して、yield は必要なときに必要なだけ処理できるという利点がある。
yield の理解には「状態が関数内に保持される」「for 文や next で実行される」点を踏まえることが重要である。

Q.17-6

戻り値の型が常に一定でない関数(例:成功時は数値、失敗時は None)に対して、安全に使うための呼び出し設計を論ぜよ。

問題背景

【1】Pythonは動的型付けである

Pythonでは、関数の戻り値に型を指定する義務がなく、ある場合は int を返し、別の条件では None を返すといった設計が簡単に可能である:

def get_score(name):
    if name in db:
        return 100  # 数値
    else:
        return None  # 失敗時

このような関数設計は柔軟だが、戻り値の型が状況によって変化するため、呼び出し側が安全に取り扱わないとバグや例外の原因となる。

【2】型が一定でない関数の使用リスク

戻り値 呼び出し側での危険な使い方 発生するエラー
None score + 5 のような算術処理を行う TypeError
int 正常系として処理可能 問題なし
None を見逃す if not score: などで 0 と誤判定 意図しないロジック破綻

【3】安全な呼び出し設計とは何か

以下のような構造を持つことが望ましい:

・型判定(is None)を明示的に行う
Optional[int] を使って型ヒントを明示する
try-exceptNone に対する操作を防ぐ
・共通のエラーハンドリング戦略を持つ

解答例とコードによる実践

「社員IDから給与を取得するが、存在しない場合は None を返す」というコードを考える

from typing import Optional

# 【1】給与データベース(仮想的な辞書)
salary_db = {
    "emp001": 5000,  # 【1】
    "emp002": 6200,  # 【2】
    "emp003": 4800   # 【3】
}

# 【2】関数定義:給与取得。該当がなければ None を返す
def get_salary(emp_id: str) -> Optional[int]:  # 【4】
    return salary_db.get(emp_id)  # 【5】存在しない場合は None を返す

# 安全な呼び出し側の設計(None 対応を含む)
# 【6】確認対象の社員IDリスト(存在するものとしないもの)
emp_ids = ["emp001", "emp004", "emp002", "emp999"]  # 【6】

# 【7】各社員IDに対して処理を行う
for i, emp_id in enumerate(emp_ids):  # 【7】
    salary = get_salary(emp_id)  # 【8】戻り値は int もしくは None

    print(f"[{i+1}] 社員ID: {emp_id}")  # 【9】

    # 【10】戻り値が None(未登録)かどうかを明示的に確認する
    if salary is None:  # 【10】
        print("  → 給与データが存在しません")  # 【11】
    else:
        print(f"  → 基本給: {salary} 円、年収見込: {salary * 12}")  # 【12】ここで安全に int として使用可能

    print("-" * 50)  # 【13】出力の区切り
実行結果
[1] 社員ID: emp001
  → 基本給: 5000 円、年収見込: 60000 円
--------------------------------------------------
[2] 社員ID: emp004
  → 給与データが存在しません
--------------------------------------------------
[3] 社員ID: emp002
  → 基本給: 6200 円、年収見込: 74400 円
--------------------------------------------------
[4] 社員ID: emp999
  → 給与データが存在しません
--------------------------------------------------

【補足:より厳密な型安全性の実現】

Python 3.10以降では、match 文(構造的パターンマッチ)を用いて、None 判定をより明示的に行うことができる:

from typing import Optional

# 【1】給与データベース(仮想的な辞書)
salary_db = {
    "emp001": 5000,  # 【1】
    "emp002": 6200,  # 【2】
    "emp003": 4800   # 【3】
}

# 【2】関数定義:給与取得。該当がなければ None を返す
def get_salary(emp_id: str) -> Optional[int]:  # 【4】
    return salary_db.get(emp_id)  # 【5】存在しない場合は None を返す

# 【6】確認対象の社員IDリスト(存在するものとしないもの)
emp_ids = ["emp001", "emp004", "emp002", "emp999"]  # 【6】

# 【7】各社員IDに対して処理を行う
for i, emp_id in enumerate(emp_ids):  # 【7】
    salary = get_salary(emp_id)  # 【8】戻り値は int または None

    print(f"[{i+1}] 社員ID: {emp_id}")  # 【9】

    # 【10】match文による構造的パターンマッチを使用
    match salary:  # 【10】
        case int():  # 【11】int型のとき(正常な給与)
            print(f"  → 基本給: {salary} 円、年収見込: {salary * 12}")  # 【12】
        case None:  # 【13】None型のとき(給与未登録)
            print("  → 給与データが存在しません")  # 【14】
        case _:  # 【15】それ以外の型(意図しない戻り値)
            print("  → 不明な戻り値の型です")  # 【16】

    print("-" * 50)  # 【17】出力の区切り

また、静的型チェッカー(mypy)で Optional[int] を利用すれば、Noneチェックを行わずに int として使用しようとした場合に警告が出るようにできる。

Q.17-7

戻り値が関数オブジェクトである場合、それを f()() のように連鎖的に使う構造の意味と使用例を記述せよ。

問題背景

【1】関数も「第一級オブジェクト(first-class object)」である

Pythonでは、関数は「第一級オブジェクト」として扱われ、変数に代入したり、引数として渡したり、戻り値として返すことができる。

def outer():
    def inner():
        return "Hello"
    return inner  # ← 呼び出すのではなく「関数そのもの」を返す

上記の outer() を呼び出すと、戻り値は "Hello" ではなく、inner という関数そのものになる。
このような構造では、outer()() のように連鎖的に呼び出すことが可能となる。

【2】構造的に f()() はどう解釈されるのか

result = f()()

この式は次のように解釈される:

  1. f():最初の関数 f を呼び出して関数オブジェクトを返す
  2. ():その戻ってきた関数オブジェクトをさらに呼び出す

つまり f()() は「関数を返す関数を呼び出して、その返された関数をさらに呼び出す」という2段階の処理である。

【3】この構造の活用例

・クロージャの生成
・関数カスタマイズのファクトリ
・関数デコレータ
・引数を遅延的に与えるカリー化
などの高度な設計に用いられる。

解答例とコードによる実践

以下の例では、「あいさつ言語を切り替え可能な関数ファクトリ」を設計し、f("ja")() のようにして日本語あいさつを得るなど、関数オブジェクトの連鎖的な利用を表現する。

# 【1】関数定義:言語コードに応じて、対応する「挨拶関数」を返すファクトリ関数
def greeting_factory(lang: str):  # 【1】
    # 【2】日本語用の挨拶関数(内側の関数)
    def greet_ja():  # 【2】
        return "こんにちは!"  # 【3】

    # 【4】英語用の挨拶関数(内側の関数)
    def greet_en():  # 【4】
        return "Hello!"  # 【5】

    # 【6】デフォルト関数(未対応の言語)
    def greet_default():  # 【6】
        return "Unsupported language."  # 【7】

    # 【8】言語コードに応じて対応する関数を返す(関数そのものを返す)
    if lang == "ja":  # 【8】
        return greet_ja  # 【9】
    elif lang == "en":  # 【10】
        return greet_en  # 【11】
    else:
        return greet_default  # 【12】

# 【13】関数を使って「関数オブジェクト」を取得する
ja_greeter = greeting_factory("ja")  # 【13】ja_greeter は greet_ja 関数オブジェクト
en_greeter = greeting_factory("en")  # 【14】
unknown_greeter = greeting_factory("xx")  # 【15】

# 【16】それぞれの関数オブジェクトを「呼び出す」ことで挨拶メッセージを取得
print(ja_greeter())  # 【16】→ "こんにちは!"
print(en_greeter())  # 【17】→ "Hello!"
print(unknown_greeter())  # 【18】→ "Unsupported language."

# 【19】上記を一行で連鎖的に書くこともできる(f()()構造)
print(greeting_factory("ja")())  # 【19】
print(greeting_factory("en")())  # 【20】
print(greeting_factory("xx")())  # 【21】
実行結果
こんにちは!
Hello!
Unsupported language.
こんにちは!
Hello!
Unsupported language.

本問のまとめ

・Pythonにおいて f()() の構造は、「関数を返す関数」の連鎖呼び出しを意味する。
・この構造は、高階関数、関数ファクトリ、クロージャ、カリー化、デコレータなどの設計に用いられる高度な技法である。
f() で返されるのは「関数オブジェクト」であり、返された関数に () をつけることで、その本体を実行するという二段階評価が行われる。
・この設計は、柔軟なAPI設計、動的な振る舞い切り替え、共通処理の抽象化などに非常に有用である。

【補足事項】

【1】lambda 式との組み合わせによる f()() 構造

lambda 式は無名関数(名前を持たない関数)を定義する構文であり、関数オブジェクトを即座に返すのに適している。
lambda を使うことで、関数オブジェクトを返す関数を簡潔に書ける。
f = lambda: lambda: "Hello" とすれば f()() の形で呼び出せる。

コード例:lambda による f()() 構造

# 【1】外側のlambdaが関数オブジェクト(内側のlambda)を返す
f = lambda: (lambda: "Lambda連鎖!")  # 【1】

# 【2】最初の()で外側のlambdaを実行し、内側のlambdaを得る
# 【3】次の()で内側のlambdaを実行し、結果の文字列を得る
print(f()())  # 【2】【3】
実行結果
Lambda連鎖!

【2】lambdaを活用したパラメータ付きクロージャ(f(x)(y)

f(x) が関数を返し、それを f(x)(y) の形で呼び出す構造は、「引数を段階的に与えるカリー化」とも呼ばれる。
lambda を使うとこの構造をシンプルに記述できる。

コード例:2段階で引数を渡す加算関数

# 【1】xを受け取るlambda式が、さらにyを受け取ってx + yを返すlambdaを返す
add = lambda x: (lambda y: x + y)  # 【1】

# 【2】add(10) → lambda y: 10 + y
# 【3】そのlambdaに5を渡す → 10 + 5 = 15
print(add(10)(5))  # 【2】【3】→ 15 を出力
実行結果
15

【3】デコレータとの応用パターン:関数の実行をログ出力付きでラップする

デコレータは「関数を引数として受け取り、別の関数を返す関数」である。つまり、関数の戻り値が関数オブジェクト(=高階関数)となる構造であり、f()() の延長にある。

@decorator 構文は func = decorator(func) の糖衣構文である。

コード例:関数の前後にログを出力するデコレータ

# 【1】デコレータ関数定義(関数を引数にとり、関数を返す)
def with_logging(func):  # 【1】
    # 【2】ラップされた関数を定義(実行前後にログを出力)
    def wrapper(*args, **kwargs):  # 【2】
        print(f"[LOG] {func.__name__} を呼び出します")  # 【3】
        result = func(*args, **kwargs)  # 【4】元の関数を呼び出す
        print(f"[LOG] 結果: {result}")  # 【5】
        return result  # 【6】
    return wrapper  # 【7】

# 【8】@構文で関数を修飾(f = with_logging(f) に等価)
@with_logging  # 【8】
def multiply(x, y):  # 【9】
    return x * y  # 【10】

# 【11】修飾された関数を実行
print(multiply(3, 4))  # 【11】
実行結果
[LOG] multiply を呼び出します
[LOG] 結果: 12
12

まとめ

構造 説明
f()() 関数を返す関数を呼び出し、さらにその関数を実行 greeting_factory("ja")()
lambda: lambda: 無名関数で f()() 構造を簡潔に記述 lambda: (lambda: "ok")
f(x)(y) 引数を段階的に与える「カリー化」 add = lambda x: lambda y: x + y
@decorator 関数をラップして別の関数として返す(f -> wrapper(f)) @with_logging

Q.17-8

return を含む try ブロック内で finally が値を書き換える構造と、それが戻り値に与える影響を例とともに説明せよ。

問題背景

【1】try–finally における制御フローの特殊性

Python において try ブロック内で return が実行されても、finally 節はその後に必ず実行される。この特性により、次のような構造が可能になる:

def func():
    try:
        return 1
    finally:
        print("finally 実行")

上記の func() を呼び出すと、1 が返る前に finally の中身が実行される。

【2】戻り値への影響の有無

ここで重要なのは、finally 節内で 戻り値を格納した変数を書き換えたとしても、それが実際の戻り値に反映されるかは「変数の型と再代入の有無」によって異なるという点である。

状況 結果
return が数値(immutable)で、finally で変数を再代入 影響なし(戻り値は変更されない)
return がリストなどのmutable型で、finally で要素を書き換え 影響あり(戻り値が変更される)

これは、return の直後に 「戻り値が一時的に保存される」→「finally 節が実行される」→「戻り値が返される」 というPythonの実行順序に起因する。

解答例とコードによる実践

ここでは以下の2例を示す:

【例1】immutableなオブジェクト(int)での再代入
【例2】mutableなオブジェクト(list)での変更

【A】immutable(int型)で finally が変数を書き換えるが戻り値には影響しない

def test_immutable():
    result = 0               # 【1】整数(immutable)型の変数を定義
    try:
        result = 1           # 【2】resultに1を代入
        return result        # 【3】return文の値は一時的に保持される
    finally:
        result = 999         # 【4】resultを書き換えるが、戻り値には影響しない
        print("finallyでresultを変更")  # 【5】

print("戻り値(immutable):", test_immutable())  # 【6】
実行結果
finallyでresultを変更
戻り値(immutable): 1

【B】mutable(list型)で finally が内容を変更すると戻り値にも影響する

def test_mutable():
    result = [1, 2, 3]              # 【1】リスト(mutable)型の変数を定義
    try:
        return result              # 【2】resultリストが戻り値として一時保存される
    finally:
        result[0] = 999            # 【3】resultの中身を書き換える(インプレース)
        print("finallyでリストを変更")  # 【4】

print("戻り値(mutable):", test_mutable())  # 【5】
実行結果
finallyでリストを変更
戻り値(mutable): [999, 2, 3]

本問のまとめ

・Pythonでは、try ブロック内の return 実行後であっても、finally 節は必ず実行されるという構造的特性を持つ。
return に使用された変数が immutable(int, strなど) であり、finally 内で「変数を再代入」しても、戻り値は変化しない。
・一方、戻り値が mutable(list, dictなど) なオブジェクトであり、finally 内で その内容を書き換えると、実際に返されるオブジェクトにも影響する。
・このため、関数設計において 戻り値を try 内で確定させたつもりでも、finally の副作用により期待と異なる値が返る可能性がある。
・特に finally 内で 戻り値オブジェクトを「変更」するコードが紛れていると、非常に発見しづらいバグの温床となる。

【補足】

finally 内で return を使うと、try 内の return はどのように上書きされるか。その構造と挙動を例とともに説明せよ。

問題背景

【1】基本仕様:finally は必ず実行される

Pythonの try–finally において、try 内で return が発動されたとしても、その値が「即座に」返るわけではない。
Pythonインタプリタは一度、戻り値を記憶した状態で finally を実行し終えるのを待つ。

def func():
    try:
        return "A"
    finally:
        return "B"

このようなコードでは、"A" を返すように見えて、実際には "B" が返る。
これは finally 節の中で return が書かれていると、それが最終的な戻り値として採用される(上書きされる)ためである。

【2】制御フロー上の優先度

制御文の場所 優先度(実際に使われるか)
tryreturn 下位
finallyreturn 上位(強制的に上書き)

この仕様は、JavaやC#とは異なる挙動であり、Pythonに特有な注意点である。

【3】設計的なリスク

try 内の return に期待してロジックを書くと、finallyreturn によってすり替えられる
・例外の再送出やエラー情報の喪失が起きやすい
・可読性が著しく低下するため、実務では基本的に finallyreturn を書くべきでない

解答例とコードによる実践

# 【1】returnの上書きを行う関数
def return_override_demo():
    try:
        print("tryブロック内: 処理開始")        # 【1】
        return "値A(tryのreturn)"             # 【2】戻り値Aが発生
    finally:
        print("finallyブロック内: 処理開始")     # 【3】
        return "値B(finallyのreturn)"         # 【4】戻り値Aが上書きされる

# 【5】関数の呼び出しと戻り値の確認
result = return_override_demo()  # 【5】
print("実際の戻り値:", result)   # 【6】
実行結果
tryブロック内: 処理開始
finallyブロック内: 処理開始
実際の戻り値: 値B(finallyのreturn)

補充:例外送出も同様に上書きされる

次の例では try 内で例外を発生させているが、finally の return によって例外が無視される:

def exception_swallowed():
    try:
        raise ValueError("例外A")       # 【1】例外が発生
    finally:
        return "値B(例外が飲み込まれる)"  # 【2】例外を握りつぶして値Bを返す

print(exception_swallowed())  # 【3】
実行結果
値B(例外が飲み込まれる)

raise が行われたにもかかわらず、finallyreturn によって「例外が発生しなかった」ことになる。
これは 例外のサプレッション(抑制)と呼ばれる動作であり、非常に危険であるため実務では避けるべきである。

あとがき

今回は「ユーザー定義関数における戻り値」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「関数の設計と記述スタイル」について扱う予定です。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?