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?

[復習] LangGraphで結構*argsと**kwargsが出てきて記憶が曖昧だったので振り返り備忘録

Posted at

目次

はじめに

Python プログラミングにおいて、*args**kwargs は非常に強力な機能です。これらは関数の柔軟性を大幅に高め、さまざまな状況に対応できるようにします。単純に言えば、*args は任意の数の位置引数を受け取り、**kwargs は任意の数のキーワード引数を受け取ることができます。

この記事では、これらの概念を基礎から応用まで、具体的な例を使って説明し、最終的には最新の LLM フレームワークである LangGraph での実践的なユースケースにも触れていきます。

基本的な概念

位置引数とキーワード引数

Python の関数には、主に2種類の引数があります:

  1. 位置引数:関数に渡す順序によって識別される引数
  2. キーワード引数:名前(キーワード)で識別される引数
# 位置引数の例
def greet(name, message):
    print(f"{message}, {name}!")

greet("田中", "こんにちは")  # こんにちは、田中!

# キーワード引数の例
greet(message="おはよう", name="佐藤")  # おはよう、佐藤!

アンパック演算子 ***

Python には、コレクション(リスト、タプル、辞書など)を「アンパック」する演算子があります:

  • * 演算子:リストやタプルなどのイテラブルをアンパックして位置引数にする
  • ** 演算子:辞書をアンパックしてキーワード引数にする

*args を理解する

*args の基本的な使い方

*args は関数が任意の数の位置引数を受け取れるようにします。args は単なる慣習的な名前で、アスタリスク (*) が重要です。

def sum_all(*args):
    """任意の数の引数を受け取り、その合計を返す"""
    total = 0
    for num in args:
        total += num
    return total

# 様々な数の引数で呼び出し可能
print(sum_all(1, 2))  # 出力: 3
print(sum_all(1, 2, 3, 4, 5))  # 出力: 15
print(sum_all())  # 出力: 0

関数内では、args は渡された全ての位置引数を含むタプルになります。

実践的な例

def create_profile(name, *hobbies):
    """ユーザープロフィールを作成し、趣味のリストを含める"""
    profile = f"名前: {name}\n趣味: "
    
    if hobbies:
        profile += ", ".join(hobbies)
    else:
        profile += "情報なし"
    
    return profile

print(create_profile("山田"))  
# 出力: 名前: 山田
#      趣味: 情報なし

print(create_profile("鈴木", "読書", "旅行", "料理"))
# 出力: 名前: 鈴木
#      趣味: 読書, 旅行, 料理

**kwargs を理解する

**kwargs の基本的な使い方

**kwargs は関数が任意の数のキーワード引数を受け取れるようにします。kwargs も単なる慣習的な名前で、ダブルアスタリスク (**) が重要です。

def display_info(**kwargs):
    """渡されたキーワード引数を表示する"""
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# 様々なキーワード引数で呼び出し可能
display_info(name="中村", age=30)
# 出力:
# name: 中村
# age: 30

display_info(name="小林", age=25, job="エンジニア", location="東京")
# 出力:
# name: 小林
# age: 25
# job: エンジニア
# location: 東京

関数内では、kwargs は渡された全てのキーワード引数を含む辞書になります。

実践的な例

def create_html_tag(tag_name, content, **attributes):
    """HTMLタグを動的に生成する"""
    attr_str = ""
    for attr, value in attributes.items():
        attr_str += f' {attr}="{value}"'
    
    return f"<{tag_name}{attr_str}>{content}</{tag_name}>"

# 基本的な段落
print(create_html_tag("p", "これは段落です"))
# 出力: <p>これは段落です</p>

# クラスとIDを持つリンク
print(create_html_tag("a", "クリックしてください", href="https://example.com", 
                      class_="button", id="main-link"))
# 出力: <a href="https://example.com" class_="button" id="main-link">クリックしてください</a>

*args**kwargs の組み合わせ

パラメータの順序

関数で *args**kwargs を組み合わせる場合、正しい順序に従う必要があります:

  1. 通常の位置引数
  2. *args
  3. デフォルト値を持つ引数
  4. **kwargs
def example_function(required, *args, default="デフォルト値", **kwargs):
    print(f"必須引数: {required}")
    print(f"位置引数 (args): {args}")
    print(f"デフォルト引数: {default}")
    print(f"キーワード引数 (kwargs): {kwargs}")

組み合わせの例

def process_data(data, *transformations, validate=True, **options):
    """データに変換を適用し、オプションに基づいて処理する"""
    result = data
    
    # データの検証
    if validate and not isinstance(data, (int, float, str, list, dict)):
        return "無効なデータ型です"
    
    # 全ての変換を適用
    for transform in transformations:
        result = transform(result)
    
    # 追加オプションを適用
    if "format" in options:
        if options["format"] == "json":
            import json
            result = json.dumps(result)
        elif options["format"] == "binary":
            result = bytes(str(result), "utf-8")
    
    if "prefix" in options:
        result = f"{options['prefix']}{result}"
    
    return result

# 変換関数の定義
def double(x): return x * 2
def add_10(x): return x + 10
def to_string(x): return str(x)

# 基本的な使用
print(process_data(5, double, add_10))  # 出力: 20

# 検証をスキップし、フォーマットオプションを使用
print(process_data([1, 2, 3], validate=False, format="json"))  # 出力: "[1, 2, 3]"

# 複数の変換とオプションを使用
print(process_data(5, double, add_10, to_string, prefix="結果: ", format="binary"))
# バイナリデータを出力

高度な使用法

関数呼び出し時のアンパック

*** は関数を呼び出す際にもコレクションをアンパックするために使用できます:

def calculate_total(x, y, z):
    return x + y + z

# リストをアンパックして関数に渡す
numbers = [10, 20, 30]
print(calculate_total(*numbers))  # 出力: 60

# 辞書をアンパックして関数に渡す
values = {"x": 5, "y": 15, "z": 10}
print(calculate_total(**values))  # 出力: 30

# 両方を組み合わせることも可能
partial_values = [5, 15]
remaining = {"z": 10}
print(calculate_total(*partial_values, **remaining))  # 出力: 30

引数の転送

*args**kwargs は、ある関数から別の関数に引数を転送する際に非常に便利です:

def log_function_call(func, *args, **kwargs):
    """関数呼び出しをログに記録し、その結果を返す"""
    print(f"関数 {func.__name__} が呼び出されました")
    print(f"位置引数: {args}")
    print(f"キーワード引数: {kwargs}")
    
    # 全ての引数を転送して関数を呼び出す
    result = func(*args, **kwargs)
    
    print(f"結果: {result}")
    return result

def add(a, b):
    return a + b

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

# 関数呼び出しをログに記録
log_function_call(add, 3, 5)
# 出力:
# 関数 add が呼び出されました
# 位置引数: (3, 5)
# キーワード引数: {}
# 結果: 8

log_function_call(greet, "田中", message="おはよう")
# 出力:
# 関数 greet が呼び出されました
# 位置引数: ('田中',)
# キーワード引数: {'message': 'おはよう'}
# 結果: おはよう, 田中さん!

組み込み関数との使用

Python の組み込み関数でも *args**kwargs の概念が活用されています:

# max() 関数で任意の数の引数の最大値を見つける
print(max(5, 12, 3, 8, 9))  # 出力: 12

# zip() で複数のイテラブルを結合
names = ["Aさん", "Bさん", "Cさん"]
ages = [25, 30, 35]
cities = ["東京", "大阪", "名古屋"]

for person in zip(names, ages, cities):
    print(person)
# 出力:
# ('Aさん', 25, '東京')
# ('Bさん', 30, '大阪')
# ('Cさん', 35, '名古屋')

ベストプラクティスと注意点

いつ使うべきか、使うべきでないか

使うべき場合:

  • 任意の数の引数を受け取る必要がある場合
  • デコレータやラッパー関数を作成する場合
  • 引数を別の関数に転送する場合
  • 柔軟性の高いインターフェースが必要な場合

使うべきでない場合:

  • 関数の引数が明確に定義できる場合
  • APIの明確さが重要な場合
  • パフォーマンスが重要な場合(過度に使用すると可読性やパフォーマンスに影響する場合がある)

よくある間違い

  1. 型アノテーションの欠如
# 悪い例
def process(*args, **kwargs):
    # 型が不明瞭
    pass

# 良い例
from typing import Any, Dict, Tuple

def process(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]):
    # 型が明確
    pass
  1. 引数の順序の間違い
# エラーになる例
def wrong_order(**kwargs, *args):  # SyntaxError
    pass

# 正しい例
def correct_order(*args, **kwargs):
    pass
  1. アンパックの誤用
# 悪い例: リストの各要素を別々の引数として渡そうとしている
numbers = [1, 2, 3]
sum(numbers)  # TypeError: sum() takes at most 2 arguments (3 given)

# 良い例
sum(*numbers)  # 6

LangGraph でのユースケース

LangGraph の紹介

LangGraph は LangChain のエコシステムの一部で、大規模言語モデル (LLM) を使った状態を持つマルチアクターアプリケーションを構築するためのフレームワークです。LangGraph はワークフローをグラフとして実装し、ノードは異なる状態を表し、エッジは状態間の遷移を表します。

LangGraph では、*args**kwargs が特に役立つシナリオがいくつかあります:

柔軟なノードハンドラの作成

LangGraph でノードハンドラを作成する際、*args**kwargs を使用すると、様々な入力形式に対応する柔軟なノードを実装できます:

from langgraph.graph import StateGraph
import operator

# 様々な入力を処理できる柔軟なノードハンドラ
def process_inputs(*args, **kwargs):
    """任意の数の入力と設定オプションを処理する"""
    results = []
    for arg in args:
        results.append(f"処理済み: {arg}")
    
    # kwargsで提供された特別な設定を適用
    if "transform" in kwargs:
        transform_func = kwargs["transform"]
        results = [transform_func(r) for r in results]
    
    return {"outputs": results}

# グラフを作成
workflow = StateGraph({"outputs": []})

# 柔軟なハンドラでノードを追加
workflow.add_node("processor", process_inputs)

# エッジを追加
workflow.add_edge("start", "processor")
workflow.add_edge("processor", "end")

# グラフをコンパイル
graph = workflow.compile()

# 異なる引数で実行
result1 = graph.invoke("data1", "data2", transform=str.upper)
result2 = graph.invoke("data3", format_output=True, add_timestamp=True)

動的メッセージルーティング

LangGraph では、メッセージの内容やメタデータに基づいて異なるノードにルーティングする必要があることがよくあります。この場合、*args**kwargs を使用すると柔軟なルーティングロジックを実装できます:

def router(*args, **kwargs):
    """メッセージの内容やメタデータに基づいてルーティングする"""
    if not args and not kwargs:
        return "default_node"
    
    # kwargsのルーティング指示をチェック
    if "destination" in kwargs:
        return kwargs["destination"]
    
    # そうでなければ、最初の引数を分析してルーティングを決定
    if args and isinstance(args[0], dict) and "priority" in args[0]:
        if args[0]["priority"] == "high":
            return "priority_handler"
    
    return "standard_handler"

# 条件分岐を持つグラフを作成
graph = StateGraph()
graph.add_node("router", router)
graph.add_node("priority_handler", lambda msg: {"result": "優先処理されました"})
graph.add_node("standard_handler", lambda msg: {"result": "通常処理されました"})

# 条件付きエッジを追加
graph.add_conditional_edges(
    "router",
    router,
    {
        "priority_handler": "priority_handler",
        "standard_handler": "standard_handler"
    }
)

# 異なる入力で実行
result1 = graph.invoke({"message": "Hello", "priority": "high"})  # priority_handlerにルーティング
result2 = graph.invoke({"message": "Hello"}, destination="standard_handler")  # kwargsで明示的にルーティング

設定可能な LLM インターフェースの実装

LangGraph で LLM を使用する際、*args**kwargs を使うことで異なる入力形式やモデルパラメータに対応できます:

from langgraph.graph import StateGraph
from langchain_openai import ChatOpenAI

def llm_node(*args, **kwargs):
    """異なる入力形式とモデルパラメータを扱える柔軟なLLMノード"""
    # kwargsからモデルパラメータを抽出
    model_kwargs = {}
    for param in ["temperature", "max_tokens", "model_name"]:
        if param in kwargs:
            model_kwargs[param] = kwargs.pop(param)
    
    # 適切なパラメータでLLMを作成
    llm = ChatOpenAI(**model_kwargs)
    
    # 入力メッセージを処理
    message = args[0] if args else kwargs.get("message", "デフォルトメッセージ")
    
    # システム指示があれば追加
    system_message = kwargs.get("system", "あなたは役立つアシスタントです。")
    
    # レスポンスを生成して返す
    response = llm.invoke([{"role": "system", "content": system_message}, 
                          {"role": "user", "content": message}])
    
    return {"response": response.content}

# 柔軟なLLMノードでグラフを作成
graph = StateGraph()
graph.add_node("llm", llm_node)

# 異なる設定で実行
response1 = graph.invoke("Pythonのargsとkwargsについて教えて", 
                        temperature=0.7, 
                        system="あなたはPythonの専門家です。")

response2 = graph.invoke(message="要点をまとめてください", 
                        max_tokens=100,
                        model_name="gpt-4")

このような機能を使うことで、LangGraph アプリケーションをより柔軟で、再利用可能で、拡張しやすいものにすることができます。

まとめ

Python の *args**kwargs は、関数の柔軟性と再利用性を高める強力な機能です。この記事では以下の点について説明しました:

  • *args は任意の数の位置引数を受け取り、内部ではタプルとして処理される
  • **kwargs は任意の数のキーワード引数を受け取り、内部では辞書として処理される
  • これらを組み合わせることで、非常に柔軟な関数インターフェースを作成できる
  • アンパック演算子 *** は関数の定義だけでなく、関数の呼び出し時にも使用できる
  • LangGraph のような最新のAIフレームワークでは、これらの機能を使って柔軟なノードハンドラやルーティングなどを実装できる

これらの概念を習得することで、より簡潔で柔軟性の高い Python コードを書けるようになり、特に大規模なプロジェクトやフレームワーク開発において大きな力を発揮します。

理解度チェッククイズ

  1. *args**kwargs の主な違いは何ですか?

    • *args は文字列のみを受け取り、**kwargs は数値のみを受け取る
    • *args は必須引数で、**kwargs はオプション引数である
    • *args は位置引数を集め、**kwargs はキーワード引数を集める
    • *args は1つの引数、**kwargs は2つの引数を表す
  2. 次のコードの出力は何ですか?

    def func(a, b, *args, c=10, **kwargs):
        return a, b, args, c, kwargs
    
    print(func(1, 2, 3, 4, c=5, d=6))
    
    • (1, 2, 3, 4, 5, {'d': 6})
    • (1, 2, (3, 4), 5, {'d': 6})
    • (1, 2, [3, 4], 5, {'d': 6})
    • (1, 2, (3, 4), 10, {'c': 5, 'd': 6})
  3. LangGraph で *args**kwargs を使用する主なメリットは何ですか?

    • 常に処理速度が向上する
    • グラフのノード数を減らすことができる
    • 異なる入力形式に柔軟に対応できる
    • エラー処理が自動的に行われる
  4. *args**kwargs を持つ関数の正しいパラメータ順序は?

    • def func(**kwargs, *args)
    • def func(*args, **kwargs, default_param)
    • def func(positional, *args, default_param=value, **kwargs)
    • def func(positional, default_param=value, *args, **kwargs)
  5. アンパック演算子 * の主な用途は?

    • 数値の乗算のみ
    • リストやタプルなどのイテラブルを位置引数にアンパックする
    • 変数の型を変換する
    • 関数の上書きを行う

*args**kwargs は、初めは複雑に見えるかもしれませんが、多くの実践的なシナリオで役立ちます。特にライブラリやフレームワークを開発する際には、将来の拡張性を考慮して積極的に活用することをお勧めします。

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?