デコレータ(Decorator)は、Pythonを読み始めたときにつまずきやすいポイントですが、「JavaのAOP(アスペクト指向)」や「WebフレームワークのInterceptor/Filter」のイメージをお持ちであれば、まさにそれです。
開発をしていてしばしば「共通処理をビジネスロジックから切り離したい(関心の分離)」という欲求がありますが、デコレータはそのための仕組みです。
機能追加系のデコレータ
ドキュメント生成ツールやWeb開発でよく見る、3つの具体的な「あるある」パターンを紹介します。
1. 「時間計測」のデコレータ
ドキュメント生成は「重い処理」になりがちです。「PDF生成に何秒かかったか知りたい」という場合、関数の前後に時間を測るコードを書くと、コードが汚れますよね。
デコレータがない場合(コードが汚れる)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span class="hljs-keyword">import</span> time <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_heavy_pdf</span>():</span> start = time.time() <span class="hljs-comment"># 計測開始(本質じゃないコード)</span> <span class="hljs-comment"># ... 重いPDF生成処理 ...</span> print(<span class="hljs-string">"PDFを作っています..."</span>) end = time.time() <span class="hljs-comment"># 計測終了(本質じゃないコード)</span> print(<span class="hljs-string">f"処理時間: <span class="hljs-subst">{end - start}</span>秒"</span>) create_heavy_pdf() |
デコレータがある場合(スッキリ!)
|
1 2 3 4 5 6 7 8 9 10 11 |
<span class="hljs-comment"># これがデコレータ(共通部品として定義されている想定)</span> <span class="hljs-comment"># 「関数の実行時間を測る」という機能を持ったラッパー</span> <span class="hljs-meta">@measure_time</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_heavy_pdf</span>():</span> <span class="hljs-comment"># ... 重いPDF生成処理 ...</span> print(<span class="hljs-string">"PDFを作っています..."</span>) <span class="hljs-comment"># 使う側は @measure_time をつけるだけ!</span> <span class="hljs-comment"># 実行すると自動的に時間がログに出力されます。</span> create_heavy_pdf() |
解説: ソースコードを読む際、@measure_time のような記述を見つけたら、「あ、この関数は実行時間をログに残すんだな」と理解して、中身のロジックに集中すればOKです。
2. 「認証・権限チェック」のデコレータ
「管理者(Admin)しか実行できない処理」を作るときによく使われます。Javaの Filter に近いです。
|
1 2 3 4 5 6 7 8 |
<span class="hljs-comment"># ユーザーが管理者かどうかチェックするデコレータ</span> <span class="hljs-meta">@login_required</span> <span class="hljs-meta">@admin_only</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_all_documents</span>():</span> <span class="hljs-comment"># ここには「削除ロジック」だけを書く。</span> <span class="hljs-comment"># 権限チェックのif文はデコレータが肩代わりしてくれる。</span> print(<span class="hljs-string">"全データを削除しました"</span>) |
解説: 関数が実行される前に、デコレータが割り込んで「ログインしてる?」「管理者?」をチェックします。もし条件を満たしていなければ、delete_all_documents 本体は実行されずにエラーが返されます。
3. 「リトライ(再試行)」のデコレータ
ドキュメント生成では、外部のAPIからデータを取ってくることがあります。ネットワークが一瞬切れただけでエラーにして良いでしょうか? 数回はリトライしたいですよね。
|
1 2 3 4 5 6 7 |
<span class="hljs-comment"># 失敗しても、最大3回まで1秒待ってやり直してくれる</span> <span class="hljs-meta">@retry(<span class="hljs-params">max_tries=<span class="hljs-number">3</span>, delay=<span class="hljs-number">1</span></span>)</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_data_from_api</span>():</span> <span class="hljs-comment"># 外部APIへの接続処理</span> <span class="hljs-comment"># ネットワークエラーが起きても、デコレータが勝手に再実行してくれる</span> <span class="hljs-keyword">pass</span> |
解説: これを自力で while 文と try-except で書くと、可読性が一気に下がります。デコレータのおかげで、「データ取得」という本来の目的が明確になります。
番外編:最近よく見る @dataclass
厳密には「処理を挟む」というより「クラスを改造する」デコレータですが、最近のPythonプロジェクトでは必須知識です。
Java/C#のDTO(Data Transfer Object) を作るイメージです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass <span class="hljs-meta">@dataclass</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span>:</span> <span class="hljs-built_in">id</span>: <span class="hljs-built_in">int</span> name: <span class="hljs-built_in">str</span> email: <span class="hljs-built_in">str</span> <span class="hljs-comment"># これだけで、コンストラクタ(__init__)や、</span> <span class="hljs-comment"># 文字列化メソッド(__str__)、比較メソッド(__eq__)などが</span> <span class="hljs-comment"># 自動生成されます(Lombokに近いイメージ)。</span> user = User(<span class="hljs-built_in">id</span>=<span class="hljs-number">1</span>, name=<span class="hljs-string">"Taro"</span>, email=<span class="hljs-string">"test@example.com"</span>) print(user) <span class="hljs-comment"># 出力: User(id=1, name='Taro', email='test@example.com')</span> |
読み方のコツ
ソースコードを読んでいて @ が出てきたら、以下の手順で解読してください。
-
名前を見る:
@transaction(DBトランザクション管理かな?)、@cache(結果をキャッシュするのかな?)など、名前で推測できることが多いです。 -
無視して本体を読む: デコレータはあくまで「付加機能」です。まずは関数本体(ビジネスロジック)が何をしているかを理解してください。
-
気になったら定義元へジャンプ: 「このデコレータ、具体的に何をしてるんだろう?」と思ったら、定義元(F12キーなど)に飛んでみてください。中に
def wrapper(...):という関数があり、そこでfunc()を呼び出している構造が見えるはずです。
振る舞い変更系(設定系)の組み込みデコレータ
先ほど説明した「ログ出力」や「認証」のような「機能追加系」のデコレータとは少し毛色が違い、@staticmethod は「振る舞い変更系(設定系)」の組み込みデコレータです。
@staticmethod は、Java/C# の static メソッドと同じです。
なぜわざわざ @ を付けて宣言する必要があるのか、Pythonの仕組みと合わせて解説します。
1. なぜ @staticmethod が必要なのか?
Pythonのクラス内の関数(メソッド)は、デフォルトで「インスタンス・メソッド」として扱われます。 これは、「第一引数に必ずインスタンス自身(self)を受け取る」という決まりがあるからです。
|
1 2 3 4 5 6 7 |
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add</span>(<span class="hljs-params">self, a, b</span>):</span> <span class="hljs-comment"># self が勝手に渡される</span> <span class="hljs-keyword">return</span> a + b calc = Calculator() calc.add(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-comment"># 内部では Calculator.add(calc, 1, 2) のように呼ばれている</span> |
しかし、クラスの中には「インスタンスのデータ(self)を使わない、単なる便利関数」を置きたいときがありますよね? そこで @staticmethod をつけると、Pythonに対して「この関数を呼ぶときは self を渡さないでください(ただの関数として扱ってください)」と指示することができます。
2. コードで比較すると分かりやすい
例えば、ドキュメント生成ツールなどであれば、「ファイルパスの操作」や「日付のフォーマット」などの便利関数でよく使われます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DocumentGenerator</span>:</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, output_dir</span>):</span> self.output_dir = output_dir <span class="hljs-comment"># インスタンス変数</span> <span class="hljs-comment"># 【1】通常のメソッド (Instance Method)</span> <span class="hljs-comment"># self を使う。インスタンスの状態にアクセスする。</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span>(<span class="hljs-params">self, filename</span>):</span> <span class="hljs-comment"># self.output_dir にアクセスしている</span> full_path = <span class="hljs-string">f"<span class="hljs-subst">{self.output_dir}</span>/<span class="hljs-subst">{filename}</span>"</span> print(<span class="hljs-string">f"保存しました: <span class="hljs-subst">{full_path}</span>"</span>) <span class="hljs-comment"># 【2】スタティックメソッド (@staticmethod)</span> <span class="hljs-comment"># self を受け取らない! インスタンスの状態には興味がない。</span> <span class="hljs-comment"># 単に「このクラスに関連する便利機能」としてここに置いてあるだけ。</span> <span class="hljs-meta"> @staticmethod</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_extension</span>(<span class="hljs-params">filename</span>):</span> <span class="hljs-comment"># self は使えない</span> <span class="hljs-keyword">return</span> filename.endswith(<span class="hljs-string">".pdf"</span>) <span class="hljs-comment"># 使い方</span> gen = DocumentGenerator(<span class="hljs-string">"/tmp"</span>) <span class="hljs-comment"># 通常メソッド呼び出し</span> gen.save(<span class="hljs-string">"report.pdf"</span>) <span class="hljs-comment"># スタティックメソッド呼び出し</span> <span class="hljs-comment"># インスタンスを作らなくても、クラスから直接呼べる(Javaのstaticと同じ)</span> is_pdf = DocumentGenerator.check_extension(<span class="hljs-string">"report.pdf"</span>) |
3. いつ使うべき?
コードレビューの際、以下のような基準で見ると良いです。
-
「この関数内で
selfを一度も使っていないな」と気づいたら、@staticmethodにすることを検討します。 -
目的: 「この関数は、クラスの状態(メンバ変数)を書き換えたり読み込んだりしませんよ(=副作用が少ないですよ)」ということを読み手に明示するためです。
補足:もうひとつの兄弟 @classmethod
似たものに @classmethod があります。これもよく出てきます。
-
@staticmethod: 引数に何も特別なものを受け取らない。(ただの関数) -
@classmethod: 第一引数に クラス自身 (cls) を受け取る。
これは「ファクトリーメソッド(インスタンスを生成するメソッド)」を作るときによく使われます。
