関数からの値の返却

Beginner

This tutorial is from open-source community. Access the source code

はじめに

この実験では、Python の関数から複数の値を返す方法を学びます。また、オプションの戻り値とエラーを効果的に処理する方法についても理解します。

さらに、並行プログラミングのための Futures の概念を探索します。値を返すことは簡単に見えるかもしれませんが、さまざまなプログラミングシナリオでは、さまざまなパターンと考慮事項があります。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 91%です。学習者から 100% の好評価を得ています。

関数から複数の値を返す

Python では、関数から複数の値を返す必要がある場合、便利な解決策があります。それはタプル (tuple) を返すことです。タプルは Python のデータ構造の一種で、不変のシーケンスです。つまり、タプルを作成すると、その要素を変更することはできません。タプルは、異なる型の複数の値を一つの場所にまとめることができるため便利です。

name=value 形式の設定行を解析する関数を作成してみましょう。この関数の目的は、この形式の行を受け取り、名前と値を別々の要素として返すことです。

  1. まず、新しい Python ファイルを作成する必要があります。このファイルには、関数のコードとテストコードを記述します。プロジェクトディレクトリ内に return_values.py という名前のファイルを作成します。ターミナルで以下のコマンドを使用してこのファイルを作成できます。
touch ~/project/return_values.py
  1. 次に、コードエディタで return_values.py ファイルを開きます。このファイル内に parse_line 関数を記述します。この関数は、行を入力として受け取り、最初の '=' 記号で分割し、名前と値をタプルとして返します。
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple: A tuple containing (name, value)
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple

この関数では、split メソッドを使用して、入力行を最初の '=' 記号で 2 つの部分に分割します。行が正しい name=value 形式であれば、名前と値を抽出し、タプルとして返します。

  1. 関数を定義した後、関数が期待通りに動作するかどうかを確認するためのテストコードを追加する必要があります。テストコードは、サンプル入力で parse_line 関数を呼び出し、結果を出力します。
## Test the parse_line function
if __name__ == "__main__":
    result = parse_line('email=guido@python.org')
    print(f"Result as tuple: {result}")

    ## Unpacking the tuple into separate variables
    name, value = parse_line('email=guido@python.org')
    print(f"Unpacked name: {name}")
    print(f"Unpacked value: {value}")

テストコードでは、まず parse_line 関数を呼び出し、返されたタプルを result 変数に格納します。そして、このタプルを出力します。次に、タプルのアンパッキングを使用して、タプルの要素を直接 namevalue 変数に割り当て、それぞれを出力します。

  1. 関数とテストコードを記述したら、return_values.py ファイルを保存します。その後、ターミナルを開き、以下のコマンドを実行して Python スクリプトを実行します。
python ~/project/return_values.py

以下のような出力が表示されるはずです。

Result as tuple: ('email', 'guido@python.org')
Unpacked name: email
Unpacked value: guido@python.org

解説:

  • parse_line 関数は、split メソッドを使用して入力文字列を '=' 文字で分割します。このメソッドは、指定された区切り文字に基づいて文字列を部分に分割します。
  • return (name, value) という構文を使用して、両方の部分をタプルとして返します。タプルは、複数の値をまとめる方法です。
  • 関数を呼び出すときには、2 つのオプションがあります。result 変数のように、タプル全体を 1 つの変数に格納することもできます。また、name, value = parse_line(...) という構文を使用して、タプルを直接別々の変数に「アンパッキング」することもできます。これにより、個々の値を扱いやすくなります。

複数の値をタプルとして返すこのパターンは、Python で非常に一般的です。これにより、関数は呼び出し元のコードに複数の情報を提供できるため、より汎用的になります。

オプションの値を返す

プログラミングでは、関数が有効な結果を生成できない場合があります。たとえば、関数が入力から特定の情報を抽出することを想定しているが、入力が期待される形式ではない場合です。Python では、このような状況を処理する一般的な方法は、None を返すことです。None は Python の特殊な値で、有効な戻り値がないことを示します。

入力が期待される基準を満たさない場合を処理するために関数をどのように変更できるか見てみましょう。parse_line 関数を対象に作業します。この関数は、'name=value' 形式の行を解析し、名前と値の両方を返すように設計されています。

  1. return_values.py ファイル内の parse_line 関数を更新します。
def parse_line(line):
    """
    Parse a line in the format 'name=value' and return both the name and value.
    If the line is not in the correct format, return None.

    Args:
        line (str): Input line to parse in the format 'name=value'

    Returns:
        tuple or None: A tuple containing (name, value) or None if parsing failed
    """
    parts = line.split('=', 1)  ## Split at the first equals sign
    if len(parts) == 2:
        name = parts[0]
        value = parts[1]
        return (name, value)  ## Return as a tuple
    else:
        return None  ## Return None for invalid input

この更新された parse_line 関数では、まず split メソッドを使用して入力行を最初の等号で分割します。結果のリストにちょうど 2 つの要素がある場合、その行は正しい 'name=value' 形式であることを意味します。その後、名前と値を抽出し、タプルとして返します。リストに 2 つの要素がない場合、入力が無効であることを意味し、None を返します。

  1. 更新された関数を実証するためのテストコードを追加します。
## Test the updated parse_line function
if __name__ == "__main__":
    ## Valid input
    result1 = parse_line('email=guido@python.org')
    print(f"Valid input result: {result1}")

    ## Invalid input
    result2 = parse_line('invalid_line_without_equals_sign')
    print(f"Invalid input result: {result2}")

    ## Checking for None before using the result
    test_line = 'user_info'
    result = parse_line(test_line)
    if result is None:
        print(f"Could not parse the line: '{test_line}'")
    else:
        name, value = result
        print(f"Name: {name}, Value: {value}")

このテストコードは、有効な入力と無効な入力の両方で parse_line 関数を呼び出し、結果を出力します。parse_line 関数の結果を使用する際には、まずそれが None かどうかを確認することに注意してください。これは重要です。なぜなら、None 値をタプルのようにアンパッキングしようとすると、エラーが発生するからです。

  1. ファイルを保存して実行します。
python ~/project/return_values.py

スクリプトを実行すると、以下のような出力が表示されるはずです。

Valid input result: ('email', 'guido@python.org')
Invalid input result: None
Could not parse the line: 'user_info'

解説:

  • 関数は現在、行に等号が含まれているかどうかを確認します。これは、行を等号で分割し、結果のリストの長さを確認することで行われます。
  • 行に等号が含まれていない場合、解析が失敗したことを示すために None を返します。
  • このような関数を使用する際には、結果を使用する前にそれが None かどうかを確認することが重要です。そうしないと、None 値の要素にアクセスしようとしたときにエラーが発生する可能性があります。

設計に関する議論: 無効な入力を処理する別のアプローチは、例外 (exception) を発生させることです。このアプローチは、特定の状況で適しています。

  1. 無効な入力が本当に例外的で、期待されるケースではない場合。たとえば、入力が信頼できるソースから来ることが想定され、常に正しい形式である場合です。
  2. 呼び出し元にエラーを処理させたい場合。例外を発生させることで、プログラムの通常の流れが中断され、呼び出し元は明示的にエラーを処理する必要があります。
  3. 詳細なエラー情報を提供する必要がある場合。例外はエラーに関する追加情報を持つことができ、デバッグに役立ちます。

例外ベースのアプローチの例:

def parse_line_with_exception(line):
    """Parse a line and raise an exception for invalid input."""
    parts = line.split('=', 1)
    if len(parts) != 2:
        raise ValueError(f"Invalid format: '{line}' does not contain '='")
    return (parts[0], parts[1])

None を返すか例外を発生させるかの選択は、アプリケーションのニーズに依存します。

  • 結果が存在しないことが一般的で期待される場合には、None を返します。たとえば、リスト内でアイテムを検索し、それが存在しない可能性がある場合です。
  • 失敗が予期せず、通常の流れを中断する必要がある場合には、例外を発生させます。たとえば、常に存在するはずのファイルにアクセスしようとする場合です。

並行プログラミングでの Future の使用

Python では、関数を同時に、つまり並行して実行する必要がある場合、スレッドやプロセスなどの便利なツールが用意されています。しかし、ここで一般的な問題に直面します。別のスレッドで実行されている関数が返す値をどのように取得できるのでしょうか。ここで Future という概念が非常に重要になります。

Future は、後で利用可能になる結果のプレースホルダーのようなものです。関数がまだ実行を完了していなくても、将来生成する値を表す方法です。この概念を簡単な例でもっと理解してみましょう。

ステップ 1: 新しいファイルを作成する

まず、新しい Python ファイルを作成する必要があります。これを futures_demo.py と呼びましょう。ターミナルで以下のコマンドを使用してこのファイルを作成できます。

touch ~/project/futures_demo.py

ステップ 2: 基本的な関数コードを追加する

次に、futures_demo.py ファイルを開き、以下の Python コードを追加します。このコードは、単純な関数を定義し、通常の関数呼び出しがどのように機能するかを示しています。

import time
import threading
from concurrent.futures import Future, ThreadPoolExecutor

def worker(x, y):
    """A function that takes time to complete"""
    print('Starting work...')
    time.sleep(5)  ## Simulate a time-consuming task
    print('Work completed')
    return x + y

## Part 1: Normal function call
print("--- Part 1: Normal function call ---")
result = worker(2, 3)
print(f"Result: {result}")

このコードでは、worker 関数は 2 つの数値を受け取り、それらを足し合わせますが、最初に 5 秒間一時停止することで時間のかかるタスクをシミュレートします。この関数を通常の方法で呼び出すと、プログラムは関数が完了するのを待ち、その後戻り値を取得します。

ステップ 3: 基本コードを実行する

ファイルを保存し、ターミナルで以下のコマンドを使用して実行します。

python ~/project/futures_demo.py

以下のような出力が表示されるはずです。

--- Part 1: Normal function call ---
Starting work...
Work completed
Result: 5

これは、通常の関数呼び出しが関数が完了するのを待ち、その後結果を返すことを示しています。

ステップ 4: 別のスレッドで関数を実行する

次に、worker 関数を別のスレッドで実行した場合に何が起こるかを見てみましょう。futures_demo.py ファイルに以下のコードを追加します。

## Part 2: Running in a separate thread (problem: no way to get result)
print("\n--- Part 2: Running in a separate thread ---")
t = threading.Thread(target=worker, args=(2, 3))
t.start()
print("Main thread continues while worker runs...")
t.join()  ## Wait for the thread to complete
print("Worker thread finished, but we don't have its return value!")

ここでは、threading.Thread クラスを使用して、新しいスレッドで worker 関数を起動しています。メインスレッドは worker 関数が完了するのを待たずに、実行を続けます。しかし、worker スレッドが終了したときに、戻り値を簡単に取得する方法がありません。

ステップ 5: スレッド化されたコードを実行する

再度ファイルを保存し、同じコマンドを使用して実行します。

python ~/project/futures_demo.py

メインスレッドが続行し、worker スレッドが実行されますが、worker 関数の戻り値にアクセスできないことに気付くでしょう。

ステップ 6: Future を手動で使用する

スレッドから戻り値を取得する問題を解決するために、Future オブジェクトを使用することができます。futures_demo.py ファイルに以下のコードを追加します。

## Part 3: Using a Future to get the result
print("\n--- Part 3: Using a Future manually ---")

def do_work_with_future(x, y, future):
    """Wrapper that sets the result in the Future"""
    result = worker(x, y)
    future.set_result(result)

## Create a Future object
fut = Future()

## Start a thread that will set the result in the Future
t = threading.Thread(target=do_work_with_future, args=(2, 3, fut))
t.start()

print("Main thread continues...")
print("Waiting for the result...")
## Block until the result is available
result = fut.result()  ## This will wait until set_result is called
print(f"Got the result: {result}")

このコードでは、Future オブジェクトを作成し、新しい関数 do_work_with_future に渡します。この関数は worker 関数を呼び出し、その後 Future オブジェクトに結果を設定します。メインスレッドは、結果が利用可能になったときに Future オブジェクトの result() メソッドを使用して結果を取得できます。

ステップ 7: Future を使用したコードを実行する

ファイルを保存し、再度実行します。

python ~/project/futures_demo.py

これで、スレッドで実行されている関数から戻り値を正常に取得できることがわかります。

ステップ 8: ThreadPoolExecutor を使用する

Python の ThreadPoolExecutor クラスは、並行タスクの処理をさらに簡単にします。futures_demo.py ファイルに以下のコードを追加します。

## Part 4: Using ThreadPoolExecutor (easier way)
print("\n--- Part 4: Using ThreadPoolExecutor ---")
with ThreadPoolExecutor() as executor:
    ## Submit the work to the executor
    future = executor.submit(worker, 2, 3)

    print("Main thread continues after submitting work...")
    print("Checking if the future is done:", future.done())

    ## Get the result (will wait if not ready)
    result = future.result()
    print("Now the future is done:", future.done())
    print(f"Final result: {result}")

ThreadPoolExecutor は、Future オブジェクトの作成と管理を代行します。関数とその引数を渡すだけで、結果を取得するために使用できる Future オブジェクトが返されます。

ステップ 9: 完全なコードを実行する

最後にファイルを保存し、実行します。

python ~/project/futures_demo.py

解説

  1. 通常の関数呼び出し: 関数を通常の方法で呼び出すと、プログラムは関数が完了するのを待ち、直接戻り値を取得します。
  2. スレッドの問題: 関数を別のスレッドで実行することには欠点があります。そのスレッドで実行されている関数の戻り値を取得する組み込みの方法がありません。
  3. 手動での Future の使用: Future オブジェクトを作成してスレッドに渡すことで、Future に結果を設定し、メインスレッドから結果を取得することができます。
  4. ThreadPoolExecutor: このクラスは並行プログラミングを簡素化します。Future オブジェクトの作成と管理を代行するため、関数を並行して実行し、その戻り値を取得することが容易になります。

Future オブジェクトにはいくつかの便利なメソッドがあります。

  • result(): このメソッドは、関数の結果を取得するために使用されます。結果がまだ準備されていない場合、準備ができるまで待機します。
  • done(): このメソッドを使用して、関数の計算が完了したかどうかを確認できます。
  • add_done_callback(): このメソッドを使用すると、結果が準備できたときに呼び出される関数を登録することができます。

このパターンは、並行プログラミング、特に並列で実行されている関数から結果を取得する必要がある場合に非常に重要です。

まとめ

この実験では、Python の関数から値を返すいくつかの重要なパターンを学びました。まず、Python の関数は複数の値をタプルにパックすることで返すことができ、これにより値の返却とアンパックがきれいで読みやすくなります。次に、常に有効な結果を生成できない関数の場合、値が存在しないことを示す一般的な方法は None を返すことであり、例外 (exception) を発生させる方法も紹介しました。

最後に、並行プログラミングでは、Future は将来の結果のプレースホルダーとして機能し、別のスレッドまたはプロセスで実行されている関数から戻り値を取得できるようにします。これらのパターンを理解することで、Python コードの堅牢性と柔軟性が向上します。さらなる練習として、異なるエラー処理戦略を試し、他の並行実行タイプで Future を使用し、async/await を使った非同期プログラミングでのそれらのアプリケーションを探索してみてください。