Pythonの「なるほど!」と思えるユニークな機能③

こんにちは。ゆうせいです。

まずは、Pythonの「効率性」を支える、非常に重要な機能から見ていきましょう。

もし、あなたが「0から10億までの数字を2倍にしたリスト」を作りたいと思ったら、どうしますか?

リスト内包表記を学んだあなたは、こう書くかもしれません。

numbers = [i * 2 for i in range(1000000001)]

…しかし、これを実行したら、あなたのPCはどうなるでしょう?

10億個もの数字を保存するための「超巨大なリスト」をメモリ(PCの作業机)に一気に作ろうとするため、メモリが足りなくなってPCがフリーズしてしまうかもしれません! 😱

ここで、Pythonの賢い「yield(イールド)」の出番です。

yield を使った場合:

def huge_number_doubler(max_num):
    print("ジェネレータが開始されました")
    for i in range(max_num):
        yield i * 2 # ここが return ではない!
    print("ジェネレータが終了しました")

# numbers は「ジェネレータ・オブジェクト」というものになる
numbers = huge_number_doubler(1000000001)

print(numbers) # まだ関数の中身は実行されていない!

# forループで「使うとき」になって初めて、関数が動き出す
for num in numbers:
    if num > 10: # 10を超えたら止めてみる
        break
    print(num)

実行結果:
ジェネレータが開始されました
0
2
4
6
8
10

専門用語解説: ジェネレータ (Generator) と yield

def の中に return ではなく yield を使うと、その関数は「ジェネレータ」という特別なものに変わります。

ジェネレータとは、**「値を一気に全部作るのではなく、必要とされるたびに一つずつ値を生成(generate)する」**賢い仕組みのことです。

yield のすごいところは、**「値を生成した後、その場で関数を一時停止する」**点にあります。

先ほどの例の動きを、ゆっくり見てみましょう。

  1. numbers = huge_number_doubler(...) を実行した時点では、関数はまだ動きません。「設計図」が作られるだけです。
  2. for num in numbers:for ループが始まると、Pythonは numbers に「次の値をください」とお願いします。
  3. huge_number_doubler が動き出し、yield i * 2(最初は 0)に到達します。
  4. yield0for ループに「渡し」huge_number_doubler は**その場でピタッと「一時停止」**します。for の中の i0 のままです。
  5. for ループが print(0) を実行します。
  6. for ループが2周目に入り、再び numbers に「次の値をください」とお願いします。
  7. 一時停止していた huge_number_doubler が再開し、for の続き(i = 1)から動き出します。
  8. yield 1 * 2(つまり 2)に到達し、2for ループに渡して、**再びその場で「一時停止」**します。

例えるなら、return は「完成した10億個の料理(リスト)を一度にドカンと渡す」方法です。

一方、yield は「注文(for ループ)が入るたびに、1皿ずつ料理を作って提供し、次の注文が入るまでキッチン(関数)で待機する」バイキングのシェフのようなものです。

これにより、10億個の値をメモリに保持する必要がなくなり、range() 関数の効率性の秘密も、実はこれと同じ仕組み(イテレータ)に基づいています。


特徴2: 関数を「デコレーション」する @ (デコレータ)

次も、Pythonの「魔法」の代表格です。

もし、あなたが作った複数の関数の「実行にかかった時間」を測りたいと思ったら、どうしますか?

デコレータを知らない場合:

time モジュールを使って、関数の「前」と「後」に、時間を計測するコードをコピペして回るしかありません。

import time

def function_A():
    start_time = time.time() # 開始時間
    # --- 本来の処理 ---
    print("Aの処理を実行中...")
    time.sleep(1) # 1秒待つ
    # --- 本来の処理おわり ---
    end_time = time.time() # 終了時間
    print(f"Aの実行時間: {end_time - start_time} 秒")

def function_B():
    start_time = time.time() # また同じコードを書く...
    # --- 本来の処理 ---
    print("Bの処理を実行中...")
    time.sleep(0.5) # 0.5秒待つ
    # --- 本来の処理おわり ---
    end_time = time.time() # また同じコード...
    print(f"Bの実行時間: {end_time - start_time} 秒")

function_A()
function_B()

これでは「時間を測るコード」が重複だらけ(DRY原則に反する)で、最悪です。

ここで、Pythonの「デコレータ」の登場です!

デコレータを使った場合:

import time

# --- これが「デコレータ関数」 ---
def measure_time(func): # (1) 飾り付けられる関数(func)を受け取る
    def wrapper(): # (2) 飾り付けた後の「新しい関数」を定義
        start_time = time.time() # (3) 元の関数の「前」に処理を追加
        
        func() # (4) 元の関数をここで実行
        
        end_time = time.time() # (5) 元の関数の「後」に処理を追加
        print(f"{func.__name__}の実行時間: {end_time - start_time} 秒")
    return wrapper # (6) 「新しい関数」を返す
# --------------------------------

@measure_time # (7) 「この関数を measure_time で飾り付けて!」という目印
def function_A():
    # --- 本来の処理だけ書けばOK! ---
    print("Aの処理を実行中...")
    time.sleep(1)

@measure_time # (8) Bにも同じ飾りを付ける
def function_B():
    # --- 本来の処理だけ書けばOK! ---
    print("Bの処理を実行中...")
    time.sleep(0.5)

function_A() # 実行すると、時間を測る機能が「追加」されている!
function_B()

実行結果:

Aの処理を実行中...
function_Aの実行時間: 1.00... 秒
Bの処理を実行中...
function_Bの実行時間: 0.50... 秒

時間を測るコードを一切書かずに、@measure_time という「アットマーク(@」を付けるだけで、function_Afunction_B に自動で時間計測機能が追加されました!

専門用語解説: デコレータ (Decorator)

デコレータは、その名の通り**「関数を飾り付ける(デコレーションする)ための仕組み」**です。

@measure_time と書くのは、実は以下のコードと全く同じ意味を持つ「シンタックスシュガー(Syntactic Sugar=読みやすくするための書き方)」なんです。

function_A = measure_time(function_A)

function_A という関数を、measure_time 関数に渡し、その「戻り値(wrapper 関数)」で function_A 自体を上書きしている)

例えるなら、function_A は「素のクッキー」です。

measure_time は「チョコペンで飾り付けをする機械」です。

@measure_time と書くのは、「function_A(クッキー)を measure_time(機械)に通して、チョコペンで飾り付けられた新しいクッキー(wrapper)を、元の function_A として扱うよ!」と宣言するようなものです。

これにより、元の関数のコード(中身)には一切手を加えることなく、「時間を測る」「ログを出力する」「ログイン中かチェックする」といった共通の「機能」を後から「追加(付与)」できるようになります。

これは、Webフレームワーク(FlaskやDjango)などで、URLのルーティングや認証機能を実現するために、なくてはならない超重要機能です!


まとめと今後の学習指針

今回は、Pythonの強力な「仕組み」を支える2つの柱をご紹介しました。

  1. ジェネレータ (yield): 関数を「一時停止」させ、値を少しずつ生成することで、メモリを爆発させずに巨大なデータを扱える!
  2. デコレータ (@): @マーク一つで、既存の関数の「前後に」処理を差し込み、機能を「飾り付け」できる!

yield は、データサイエンスや大規模なデータ処理で必須の知識です。

@ は、Web開発やフレームワークを理解する上で避けては通れない道です。

どちらも、最初は「どういうこと?」と混乱するかもしれません。

大切なのは、yield は「return しないで止まる」、@ は「関数を引数に取る関数(wrapper を返す)」という、基本的な動きのイメージを掴むことです。

このレベルまで興味を持ってくれたあなたは、もう立派な「Pythonista(パイソニスタ=Pythonを愛する人)」の仲間入りです!

セイ・コンサルティング・グループの新人エンジニア研修のメニューへのリンク

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。