🐍

精読「独習Python」(ユーザー定義関数 応用)

2024/12/31に公開



独習Python
「独習Python」は、初心者から中級者までを対象に、Pythonの基礎から応用までを体系的に学べる入門書で、豊富な例題と練習問題を通じて実践的なスキルを身につけられる一冊です。

関連記事

デコレーター

デコレーターの基本

関数やメソッドに追加の機能を付加するために使用される非常に便利な仕組み(実態は、関数を受け取り関数を返す関数)

import logging

# ログの設定(INFOレベルでコンソールに出力)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# デコレーター関数の定義
def log_decorator(func):
    """
    このデコレーターは、関数の呼び出し時にログを記録する。

    Args:
        func (callable): デコレーターを適用する対象の関数。
    
    Returns:
        callable: ラッパー関数を返す。
    """
    def wrapper(*args, **kwargs):
        """
        ラッパー関数(装飾された関数を実行し、ログを記録する)。

        Args:
            *args: 任意の位置引数。
            **kwargs: 任意のキーワード引数。
        
        Returns:
            関数funcの実行結果。
        """
        # 関数の呼び出し前にログを記録
        logging.info(f"関数 {func.__name__} が呼び出されました")
        logging.info(f"引数: {args}, キーワード引数: {kwargs}")
        
        # 元の関数を実行し、結果を取得
        result = func(*args, **kwargs)
        
        # 関数の実行結果をログに記録
        logging.info(f"関数 {func.__name__} の結果: {result}")
        
        # 結果を返す
        return result
    
    return wrapper  # デコレーターとしてラッパー関数を返す

# デコレーターを適用した関数の例
@log_decorator
def add(a, b):
    return a + b

@log_decorator
def greet(name, greeting="こんにちは"):
    return f"{greeting}{name}さん!"

# 実行例
result1 = add(5, 3)  # 5 + 3 = 8
result2 = greet("田中")  # デフォルトの挨拶
result3 = greet("佐藤", greeting="こんばんは")  # カスタム挨拶

# 2025-01-01 10:15:00,001 - INFO - 関数 add が呼び出されました
# 2025-01-01 10:15:00,001 - INFO - 引数: (5, 3), キーワード引数: {}
# 2025-01-01 10:15:00,002 - INFO - 関数 add の結果: 8
# 2025-01-01 10:15:00,003 - INFO - 関数 greet が呼び出されました
# 2025-01-01 10:15:00,003 - INFO - 引数: ('田中',), キーワード引数: {}
# 2025-01-01 10:15:00,004 - INFO - 関数 greet の結果: こんにちは、田中さん!
# 2025-01-01 10:15:00,005 - INFO - 関数 greet が呼び出されました
# 2025-01-01 10:15:00,005 - INFO - 引数: ('佐藤',), キーワード引数: {'greeting': 'こんばんは'}
# 2025-01-01 10:15:00,006 - INFO - 関数 greet の結果: こんばんは、佐藤さん!

引数を受け取るデコレーター

import logging

# ログの設定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

def log_with_level(level):
    """
    ログレベルを指定できるデコレーター。

    Args:
        level (str): ログレベル ("INFO", "WARNING", "ERROR"など)。
    
    Returns:
        callable: デコレーター関数を返す。
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            # 指定されたログレベルに応じてログを出力
            log_message = f"関数 {func.__name__} が呼び出されました。引数: {args}, キーワード引数: {kwargs}"
            if level == "INFO":
                logging.info(log_message)
            elif level == "WARNING":
                logging.warning(log_message)
            elif level == "ERROR":
                logging.error(log_message)
            else:
                logging.debug(log_message)
            
            # 元の関数を実行し、その結果を取得
            result = func(*args, **kwargs)
            
            # 実行結果をログに記録
            logging.info(f"関数 {func.__name__} の結果: {result}")
            return result
        return wrapper
    return decorator

# デコレーターを適用
@log_with_level("INFO")
def add(a, b):
    """2つの数値を加算する"""
    return a + b

@log_with_level("WARNING")
def divide(a, b):
    """2つの数値を除算する"""
    if b == 0:
        raise ValueError("0で割ることはできません")
    return a / b

# 実行例
result1 = add(10, 5)  # INFOレベルのログが出力される
try:
    result2 = divide(10, 0)  # WARNINGレベルのログが出力される
except ValueError as e:
    logging.error(f"エラーが発生しました: {e}")

# 2025-01-01 10:30:00,001 - INFO - 関数 add が呼び出されました。引数: (10, 5), キーワード引数: {}
# 2025-01-01 10:30:00,002 - INFO - 関数 add の結果: 15
# 2025-01-01 10:30:00,003 - WARNING - 関数 divide が呼び出されました。引数: (10, 0), キーワード引数: {}
# 2025-01-01 10:30:00,004 - ERROR - エラーが発生しました: 0で割ることはできません

クロージャー(関数閉方)

クロージャーとは、上位のローカル変数を参照した入れ子の関数

def outer_function(x):
    """
    外側の関数。内側の関数に値を閉じ込める。
    
    Args:
        x (int): 外側の関数の引数。
    
    Returns:
        function: 内側の関数を返す。
    """
    def inner_function(y):
        """
        内側の関数。外側の変数 x を参照して利用する。
        
        Args:
            y (int): 内側の関数の引数。
        
        Returns:
            int: x + y の結果を返す。
        """
        return x + y
    
    return inner_function  # 内側の関数を返す

# outer_functionを実行してinner_functionを取得
closure_func = outer_function(10)

# inner_functionを実行
result = closure_func(5)  # x=10, y=5 なので結果は 15
print(result)  # 出力: 15

ジェネレーター

ジェネレーター(Generator)は、Pythonで大規模データ処理遅延評価を効率的に行う仕組み。
yield を使う関数で、実行時にイテレーターとして動作するジェネレーターオブジェクトを返す。値を逐次生成するためメモリ効率が良いのが特徴。

def simple_generator():
    """
    簡単なジェネレーター関数。
    """
    yield 1
    yield 2
    yield 3

# ジェネレーターオブジェクトを生成
gen = simple_generator()

# 値を順次取得
print(next(gen))  # 出力: 1
print(next(gen))  # 出力: 2
print(next(gen))  # 出力: 3

# すべての値を取得した後に次を呼ぶとエラー
# print(next(gen))  # StopIteration例外が発生

例)素数を求めるジェネレーター

def prime_generator():
    """
    素数を順次生成するジェネレーター。
    
    Yields:
        int: 素数
    """
    number = 2  # 素数は2から始まる
    while True:
        if is_prime(number):
            yield number
        number += 1

def is_prime(num):
    """
    数値が素数かどうかを判定するヘルパー関数。
    
    Args:
        num (int): 判定する数値。
    
    Returns:
        bool: 素数ならTrue、そうでなければFalse。
    """
    if num < 2:
        return False
    for i in range(2, int(num**0.5) + 1):  # 2から√numまで試し割り
        if num % i == 0:
            return False
    return True

# 使用例
prime_gen = prime_generator()

for _ in range(10):  # 最初の10個の素数を表示
    print(next(prime_gen))

ジェネレーターの主なメソッド

メソッド 説明
__next__() 次の値を生成。yield に到達。終了時に StopIteration を発生させる。
send(value) ジェネレーターに値を送りつつ再開。yield 式の値を更新。
throw(type, ...) ジェネレーター内で例外を発生させる。
close() ジェネレーターを終了させる。GeneratorExit を発生させてリソースを解放。

ジェネレーターで一部の処理を別のジェネレーターに委譲する

yield from を使うと、他のジェネレーターやイテラブルから値を受け取り、委譲したジェネレーターを通じて処理を行う

def generator_a():
    yield 1
    yield 2
    yield 3

def generator_b():
    yield 10
    yield 20
    yield 30

def delegating_generator():
    yield "Start delegating"
    
    # 別のジェネレーターに処理を委譲
    yield from generator_a()  # generator_a()の値を順次委譲
    yield from generator_b()  # generator_b()の値を順次委譲
    
    yield "End delegating"

# ジェネレーターの実行
gen = delegating_generator()

# 結果を表示
for value in gen:
    print(value)

# Start delegating
# 1
# 2
# 3
# 10
# 20
# 30
# End delegating

ジェネレーター式

リスト内包表記のように簡潔に記述できる、ジェネレーターを作成するための構文。
リスト内包表記の代わりに、ジェネレーター式を使うとメモリ効率が向上し、必要なときに遅延評価(必要な分だけ値を生成)することができる

# 1から10までの整数の二乗を生成するジェネレーター式
gen = (x**2 for x in range(1, 11))

# ジェネレーターを使用して値を取得
for value in gen:
    print(value)

関数のモジュール化

モジュールの定義

モジュールを定義するには、通常のPythonコードを書いて、それを .pyファイルに保存する

# math_utils.py

def add(a, b):
    """2つの数値を加算する関数"""
    return a + b

def subtract(a, b):
    """2つの数値を減算する関数"""
    return a - b

PI = 3.14159  # 定数

if __name__ == "__main__": は、Pythonスクリプトが直接実行された場合にのみ実行されるコードを定義するための構文。モジュールがインポートされた場合には、この部分のコードは実行されない。これにより、モジュールとして使うときと、スクリプトとして実行する場合を使い分けることができる。

# calculator.py

def add(a, b):
    """加算を行う関数"""
    return a + b

def subtract(a, b):
    """減算を行う関数"""
    return a - b

def multiply(a, b):
    """乗算を行う関数"""
    return a * b

def divide(a, b):
    """除算を行う関数"""
    if b == 0:
        raise ValueError("Division by zero is not allowed")
    return a / b

# モジュールが直接実行された場合のみテストを実行
if __name__ == "__main__":
    print("Running tests for calculator module...")

    # テスト1: 加算
    result = add(2, 3)
    assert result == 5, f"Expected 5, but got {result}"

    # テスト2: 減算
    result = subtract(5, 3)
    assert result == 2, f"Expected 2, but got {result}"

    # テスト3: 乗算
    result = multiply(2, 3)
    assert result == 6, f"Expected 6, but got {result}"

    # テスト4: 除算
    result = divide(6, 3)
    assert result == 2, f"Expected 2, but got {result}"

    # 除算エラーテスト
    try:
        divide(5, 0)
    except ValueError as e:
        assert str(e) == "Division by zero is not allowed", f"Expected error message, but got {e}"

    print("All tests passed!")

パッケージ

Pythonのパッケージは、複数のモジュール(Pythonファイル)をまとめて一つのディレクトリで管理できるようにしたもの

Pythonのモジュール、パッケージ、ライブラリ徹底解説!より

非同期処理

時間のかかるタスク(I/O操作など)を効率的に処理するための手法。非同期処理を使うことで、並行して複数のタスクを実行し、待機時間を有効に活用できる

コルーチンの基本

コルーチンは通常、非同期のタスクを扱うために使われ、await を使って他のコルーチンを待機することができる点が関数とは異なる

関数とコルーチンの違い

特徴 関数 コルーチン
定義方法 def キーワードを使用 async def キーワードを使用
実行方法 呼び出し時に同期的に実行 呼び出し時に非同期的に実行
結果の返却 return キーワードで値を返す await キーワードで他の非同期関数を待機し、yield で部分的に結果を返すことも可能
実行の特徴 実行順序がブロッキングされる 実行順序が非ブロッキング(待機中に他のタスクが実行される)
使用例 一般的な関数や計算処理に使用 非同期処理(I/O待機、並行処理など)に使用

asyncioというモジュールが非同期処理を実現するための主要な方法。また、非同期処理にはasync/await構文を使う。

キーワード 説明
async 非同期関数を定義するために使う。この関数は必ず await を使って呼び出す必要がある
await 非同期関数が結果を返すまで待機する。非同期関数が完了するのを待ってから、次の処理を実行する
import asyncio

async def fetch_data(name, delay):
    print(f"Fetching data for {name}...")
    await asyncio.sleep(delay)  # 非同期で待機(I/Oをシミュレート)
    print(f"Data fetched for {name}")
    return f"Data for {name}"

async def main():
    # 複数の非同期関数を並行して実行
    task1 = asyncio.create_task(fetch_data("Task1", 2))
    task2 = asyncio.create_task(fetch_data("Task2", 1))
    
    # 両方のタスクが終了するのを待つ
    result1 = await task1
    result2 = await task2

    print(result1)
    print(result2)

# 非同期イベントループを開始
asyncio.run(main())

タスクの作成と実行

タスクは非同期関数(コルーチン)を実行するための管理単位。asyncio.create_task() を使用することで、非同期関数をタスクとして作成し、非同期イベントループに渡して実行できる。

import asyncio

# 非同期関数を定義
async def sample_task(name, delay):
    print(f"Task {name} started")
    await asyncio.sleep(delay)  # 非同期で待機
    print(f"Task {name} finished")

async def main():
    # タスクの作成
    task1 = asyncio.create_task(sample_task("A", 2))
    task2 = asyncio.create_task(sample_task("B", 1))
    
    # タスクが完了するのを待つ
    await task1
    await task2

# 非同期イベントループを実行
asyncio.run(main())

ドキュメンテーション

docstringの基本

関数、クラス、モジュールのコード内にコメントとして埋め込む形式で、ドキュメントを提供するための標準的な方法。docstringは、主に関数やクラスの使い方や動作、引数、戻り値について説明するために使われる

def add(a, b):
    """
    2つの数を足し算して、その結果を返します。

    引数:
    a (int or float): 加算する最初の数
    b (int or float): 加算する2番目の数

    戻り値:
    int or float: aとbを足した結果

    例:
    >>> add(3, 5)
    8
    >>> add(2.5, 3.5)
    6.0
    """
    return a + b

ドキュメント生成ツール「Sphinx」

Pythonのドキュメントを自動で生成するためのツール。主に、ソースコードに埋め込まれたdocstringを元にHTML、PDF、EPUBなどの形式でドキュメントを生成する。

関数アノテーション

関数の引数や戻り値の型を示すために使用される構文。アノテーションは、コードの動作に影響を与えない純粋なメタデータであり、型チェックやIDEでの補完などで活用される。

ただ、Python自体には型の強制はなく、主にコードの理解を助けたり、静的解析ツールで型のチェックを行ったりするために使われる。

def greet(name: str = "World") -> str:
    return f"Hello, {name}!"

型の複雑な使用例(リストや辞書など)

from typing import List, Dict

def process_data(data: List[int], mappings: Dict[str, int]) -> float:
    return sum(data) / len(mappings)

参考

https://amzn.to/41RlbuT
https://zenn.dev/ryo_kawamata/articles/learn_decorator_in_python

Discussion