👌

【Python】関数の呼び出し回数を制限する方法

に公開

今日は少し変わった方法をご紹介します。

それは「関数の呼び出し回数を制限する方法」。

「えっ、なんでそんなことする必要があるの?」と思った方、そんなあなたはとっても正常です(笑)

でもじつは、APIの利用回数が制限されている場合や、テスト時に特定の関数の呼び出し回数を制御したい場合にとても役立ちます。

それでは、さっそく見ていきましょう。

1. デコレータを使った方法

Pythonには「デコレータ」という、関数をかっこよく着飾る機能があります。

これを使って、関数の呼び出し回数を制限してみましょう。

def limit_calls(max_calls):
    """指定した回数までしか呼び出せないようにするデコレータ"""
    def decorator(func):
        # 呼び出し回数を記録する変数
        calls = {"count": 0}
        
        def wrapper(*args, **kwargs):
            if calls["count"] >= max_calls:
                print(f"⚠️ 関数 {func.__name__} は最大 {max_calls} 回までしか呼び出せません!")
                return None
            
            calls["count"] += 1
            result = func(*args, **kwargs)
            print(f"🔢 現在の呼び出し回数: {calls['count']}/{max_calls}")
            return result
            
        return wrapper
    return decorator


# 使用例
@limit_calls(max_calls=3)
def say_hello(name):
    """挨拶をする関数"""
    return f"こんにちは、{name}さん!"

# 関数を呼び出してみる
print(say_hello("太郎"))  # 1回目
print(say_hello("花子"))  # 2回目
print(say_hello("次郎"))  # 3回目
print(say_hello("三郎"))  # 4回目(制限オーバー)

このコードを実行すると、次のような結果が得られます:

🔢 現在の呼び出し回数: 1/3
こんにちは、太郎さん!
🔢 現在の呼び出し回数: 2/3
こんにちは、花子さん!
🔢 現在の呼び出し回数: 3/3
こんにちは、次郎さん!
⚠️ 関数 say_hello は最大 3 回までしか呼び出せません!
None

デコレータを使うと、元の関数を変更することなく、「呼び出し回数を制限する」という新しい機能を追加できるんです。

三層構造について

デコレータが三層構造になっている理由をわかりやすく説明します。

1. 三層構造の基本

まず、上記のコードには3つの関数が入れ子になっています:

  1. limit_calls(max_calls) - 一番外側の関数
  2. decorator(func) - 真ん中の関数
  3. wrapper(*args, **kwargs) - 一番内側の関数

2. なぜ三層構造が必要なのか

それぞれの層には明確な役割があります:

第1層: limit_calls(max_calls)

  • この層は設定を受け取るためにあります
  • この例では「最大呼び出し回数」という設定を受け取ります
  • この層がないと、デコレータにパラメータを渡せません

第2層: decorator(func)

  • この層はデコレートする関数を受け取るためにあります
  • Pythonのデコレータの仕組み上、この層は必須です
  • @limit_calls(5) と書くと、Pythonはその下の関数を自動的にこの層に渡します

第3層: wrapper(*args, **kwargs)

  • この層は元の関数の代わりに実行される関数です
  • *args, **kwargs は「どんな引数でも受け取れる」という特別な書き方です
  • *args は「位置引数」(普通の引数)をいくつでも受け取ります
  • **kwargs は「キーワード引数」(名前付き引数)をいくつでも受け取ります
  • 元の関数と同じ引数を受け取り、追加機能を提供した上で元の関数を呼び出します

*args, **kwargs が必要な理由を説明します:

  1. デコレータは「どんな関数でも」飾れないといけません
  2. 関数にはいろんな引数の数や種類があります
  3. でも、デコレータはその関数がどんな引数を取るか前もって知りません
  4. だから、「何でも受け取れる」ようにしておくのです

3. 具体例で考える

こんなコードを書いたとします:

@limit_calls(5)
def こんにちは(名前):
    print(f"こんにちは、{名前}さん!")

これがどう動くかというと:

  1. limit_calls(5) が実行され、max_calls = 5 という情報を保持した decorator 関数を返します
  2. その decorator 関数が こんにちは 関数を受け取ります
  3. decoratorwrapper 関数を作り、それを返します
  4. この wrapper 関数が、元の こんにちは 関数の代わりになります
  5. こんにちは("太郎") と呼び出すと、実際には wrapper("太郎") が実行されます
  6. wrapper は呼び出し回数をチェックして、元の こんにちは 関数を呼び出します

4. 三層構造が必要な理由をまとめると

  1. 設定を渡すために一番外側の層が必要
  2. デコレートする関数を受け取るために真ん中の層が必要
  3. 元の関数を置き換えるために一番内側の層が必要

もし設定が不要なら、二層構造のデコレータも可能です。

しかし、多くの場合、デコレータに設定を渡したいので、三層構造が一般的になっています。

calls = {"count": 0} の説明

デコレータの中にある calls = {"count": 0} についてすこし説明します。

これは少し不思議に見えるかもしれませんが、とても大切な役割を持っています。

まず基本から理解しましょう

変数の種類: この calls辞書型の変数です。Pythonの辞書は {キー: 値} という形で情報を保存します。この場合、"count" というキーに対して 0 という値を持っています。

なぜ単なる数値変数ではないのか: たとえば count = 0 ではなく、なぜ辞書を使っているのでしょうか?

じつはPythonでは、関数の中で外側の変数を変更しようとすると、特別な扱いが必要になります。

たとえば次のようなコードを考えてみましょう:

def outer():
   count = 0
   
   def inner():
       count += 1  # エラーになる!
       return count
       
   return inner

このコードは実行するとエラーになります。

なぜなら、inner 関数内で count を変更しようとしていますが、Pythonはこれを「新しい変数 count を作ろうとしている」と誤解するからです。

解決策:辞書を使う

辞書の特性: Pythonでは、辞書やリストのような「可変オブジェクト」の中身を変更する場合、上記のエラーは発生しません。

修正例:

def outer():
   counts = {"count": 0}
   
   def inner():
       counts["count"] += 1  # これはOK!
       return counts["count"]
       
   return inner

デコレータ内での使用理由

デコレータの構造: デコレータも同じ問題に直面しています。関数を包むラッパー関数の中で、呼び出し回数を追跡する必要があります。

具体的な流れ:

  • デコレータが適用されると、calls = {"count": 0} で呼び出しカウンターを初期化
  • ラッパー関数 wrapper が呼び出されるたびに calls["count"] += 1 でカウントを増やす
  • カウントは辞書の中にあるので、関数呼び出しの間でも保持される

辞書の重要な役割: 辞書は「参照型」なので、関数が終了してもその内容が保持されます。デコレータが生成したラッパー関数が呼び出されるたびに、同じ辞書オブジェクトを参照します。

わかりやすい例え

これを「クッキーの箱」に例えると:

  • count = 0 は「頭の中でクッキーの数を覚えておく」方法
  • calls = {"count": 0} は「箱にクッキーの数を書いておく」方法

関数が終わるとあなたの頭の中の記憶はリセットされますが、箱に書いた数字は残ります!

次回、関数が呼ばれた時も同じ箱を見るので、以前の状態を覚えていられるのです。

これが、デコレータで関数の呼び出し回数を追跡するために辞書を使う理由です。

nonlocalを使う方法

じつはnonlocal キーワードを使うと、もっとシンプルに書けます。

nonlocal を使って書き直すと、このようになります:

def limit_calls(max_calls):
    """指定した回数までしか呼び出せないようにする魔法のデコレータ(nonlocal版)"""
    def decorator(func):
        # 呼び出し回数を記録する変数
        count = 0
        
        def wrapper(*args, **kwargs):
            nonlocal count  # 外側の関数のcount変数を使うことを宣言
            
            if count >= max_calls:
                print(f"⚠️ 関数 {func.__name__} は最大 {max_calls} 回までしか呼び出せません!")
                return None
            
            count += 1
            result = func(*args, **kwargs)
            print(f"🔢 現在の呼び出し回数: {count}/{max_calls}")
            return result
            
        return wrapper
    return decorator


# 使用例
@limit_calls(max_calls=3)
def say_hello(name):
    """挨拶をする関数"""
    return f"こんにちは、{name}さん!"

# 関数を呼び出してみる
print(say_hello("太郎"))  # 1回目
print(say_hello("花子"))  # 2回目
print(say_hello("次郎"))  # 3回目
print(say_hello("三郎"))  # 4回目(制限オーバー)

こちらの方法では、辞書を使う代わりに nonlocal キーワードを使っています。

これにより:

  1. 単純に count = 0 という整数変数を使えます
  2. wrapper 関数内で nonlocal count と宣言することで、「この count 変数は親関数の count 変数と同じものですよ」とPythonに伝えます
  3. これにより、wrapper 関数の中から親関数の count 変数を直接変更できるようになります

こちらの方が:

  • コードが短くなる
  • 理解しやすくなる
  • パフォーマンス的にもわずかに有利(辞書のルックアップが不要)

nonlocal キーワードは、Python 3から導入された機能で、まさにこのような「クロージャ内で外側の変数を変更したい」という状況のために設計されています。

最初に辞書を使った方法を紹介しましたが、実際のコードでは nonlocal を使った方がモダンでクリーンな解決策だと言えます。

2. クラスを使った方法

次は、クラスを使って制限する方法です。

オブジェクト指向が好きな方におすすめです!

class LimitedFunction:
    """関数の呼び出し回数を制限するクラス"""
    
    def __init__(self, func, max_calls):
        self.func = func
        self.max_calls = max_calls
        self.calls = 0
        self.func_name = func.__name__
    
    def __call__(self, *args, **kwargs):
        if self.calls >= self.max_calls:
            print(f"⚠️ 関数 {self.func_name} は最大 {self.max_calls} 回までしか呼び出せません!")
            return None
        
        self.calls += 1
        result = self.func(*args, **kwargs)
        print(f"🔢 現在の呼び出し回数: {self.calls}/{self.max_calls}")
        return result


# 使用例
def greet(name):
    """挨拶をする関数"""
    return f"やぁ、{name}!元気?"

# 関数を制限付きにラップする
limited_greet = LimitedFunction(greet, max_calls=2)

# 関数を呼び出してみる
print(limited_greet("友達"))  # 1回目
print(limited_greet("先生"))  # 2回目
print(limited_greet("校長先生"))  # 3回目(制限オーバー)

実行結果はこんな感じです:

🔢 現在の呼び出し回数: 1/2
やぁ、友達!元気?
🔢 現在の呼び出し回数: 2/2
やぁ、先生!元気?
⚠️ 関数 greet は最大 2 回までしか呼び出せません!
None

クラスを使うと、もう少し柔軟な制御ができるようになります。

さらに、呼び出し回数をリセットする機能なども簡単に追加できます。

3. グローバル変数とラッパー関数を使った方法

デコレータやクラスが難しいと感じる方は、シンプルで分かりやすいこちらの方法がおすすめです。

# グローバル変数で呼び出し回数を管理
call_counter = {}

def limit_function_calls(func, max_calls):
    """関数の呼び出し回数を制限するラッパー関数を作成"""
    # 関数名をカウンターのキーとして使用
    func_name = func.__name__
    
    # 関数の呼び出し回数を初期化
    if func_name not in call_counter:
        call_counter[func_name] = 0
    
    def wrapper(*args, **kwargs):
        # 呼び出し回数をチェック
        if call_counter[func_name] >= max_calls:
            print(f"⚠️ 関数 {func_name} は最大 {max_calls} 回までしか呼び出せません!")
            return None
        
        # 呼び出し回数を増やす
        call_counter[func_name] += 1
        
        # 元の関数を呼び出す
        result = func(*args, **kwargs)
        
        print(f"🔢 現在の呼び出し回数: {call_counter[func_name]}/{max_calls}")
        return result
    
    return wrapper


# 使用例
def calculate_sum(a, b):
    """2つの数字を足し算する関数"""
    return a + b

# 関数を制限付きにラップする
limited_sum = limit_function_calls(calculate_sum, max_calls=3)

# 関数を呼び出してみる
print(f"計算結果: {limited_sum(5, 3)}")  # 1回目
print(f"計算結果: {limited_sum(10, 20)}")  # 2回目
print(f"計算結果: {limited_sum(7, 8)}")  # 3回目
print(f"計算結果: {limited_sum(1, 1)}")  # 4回目(制限オーバー)

# 他の関数も制限できる
def say_goodbye():
    return "さようなら!"

limited_goodbye = limit_function_calls(say_goodbye, max_calls=1)
print(limited_goodbye())  # 1回目
print(limited_goodbye())  # 2回目(制限オーバー)

実行結果はこんな感じです:

🔢 現在の呼び出し回数: 1/3
計算結果: 8
🔢 現在の呼び出し回数: 2/3
計算結果: 30
🔢 現在の呼び出し回数: 3/3
計算結果: 15
⚠️ 関数 calculate_sum は最大 3 回までしか呼び出せません!
計算結果: None
🔢 現在の呼び出し回数: 1/1
さようなら!
⚠️ 関数 say_goodbye は最大 1 回までしか呼び出せません!
None

この方法のいいところは、グローバル変数 call_counter を使っているので、複数の関数の呼び出し回数を同時に管理できることです。

4. 実用的な例:API呼び出しの制限

最後に、実際の使用例として、APIの呼び出し回数を制限する方法を見てみましょう。

APIには「1分間に3回まで」のような制限があることが多いので、そういう場合に役立ちます。

import time
from functools import wraps

def rate_limiter(max_calls, time_frame):
    """
    API呼び出しを制限するデコレータ
    max_calls: 最大呼び出し回数
    time_frame: 時間枠(秒)
    """
    def decorator(func):
        calls = []  # 呼び出し時間を記録するリスト
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_time = time.time()
            
            # time_frame秒前よりも古い呼び出し記録を削除
            calls[:] = [t for t in calls if current_time - t < time_frame]
            
            # 呼び出し回数が制限を超えているかチェック
            if len(calls) >= max_calls:
                wait_time = time_frame - (current_time - calls[0])
                print(f"⚠️ API呼び出し制限に達しました!あと{wait_time:.1f}秒待ってください。")
                return None
            
            # 現在の呼び出し時間を記録
            calls.append(current_time)
            
            # 元の関数を実行
            result = func(*args, **kwargs)
            
            # 残りの呼び出し回数を表示
            print(f"🔄 残り呼び出し可能回数: {max_calls - len(calls)}/{max_calls}")
            
            return result
        
        return wrapper
    
    return decorator


# 使用例:APIをシミュレート
@rate_limiter(max_calls=3, time_frame=10)  # 10秒間に3回まで
def fake_api_call(query):
    """APIから情報を取得する(仮想的な関数)"""
    print(f"APIリクエスト: '{query}'...")
    # 実際のAPIコールはここで行う
    return f"'{query}'の検索結果です"


# テスト実行
print("連続して4回APIを呼び出してみます:")
print(fake_api_call("Python"))
print(fake_api_call("デコレータ"))
print(fake_api_call("関数"))
print(fake_api_call("制限"))  # 制限オーバー

print("\n少し待ってから再度呼び出します...")
time.sleep(10)  # 10秒待つ
print(fake_api_call("再挑戦"))

実行結果はこんな感じです(時間によって結果が変わります):

連続して4回APIを呼び出してみます:
APIリクエスト: 'Python'...
🔄 残り呼び出し可能回数: 2/3
'Python'の検索結果です
APIリクエスト: 'デコレータ'...
🔄 残り呼び出し可能回数: 1/3
'デコレータ'の検索結果です
APIリクエスト: '関数'...
🔄 残り呼び出し可能回数: 0/3
'関数'の検索結果です
⚠️ API呼び出し制限に達しました!あと9.7秒待ってください。
None

少し待ってから再度呼び出します...
APIリクエスト: '再挑戦'...
🔄 残り呼び出し可能回数: 2/3
'再挑戦'の検索結果です

この例では、10秒間に3回までというAPI制限を実装しています。時間枠内に制限以上の呼び出しがあると、「あとどれくらい待てば良いか」も教えてくれる親切設計です。

実際のAPIを使うときにとても役立ちますよ!

まとめ

今回は、Pythonで関数の呼び出し回数を制限する方法を4つ紹介しました。

それぞれの方法を表にまとめてみましょう:

方法 メリット 向いている場面
デコレータ 元の関数を変更せずに済む コードをきれいに保ちたい場合
クラス 柔軟な機能拡張が可能 オブジェクト指向で設計したい場合
ラッパー関数 シンプルでわかりやすい 初心者や簡単な制限をしたい場合
レートリミッター 時間ベースの制限ができる API呼び出しなど実用的な場面

どの方法を選ぶかは、あなたのプロジェクトの状況や好みによって変わるでしょう

でも、どの方法も「関数の呼び出し回数を制限する」という力を与えてくれます!

Discussion