質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

1263閲覧

関数をcontextmanager, decoratorの両方に対応させることはできるか?

退会済みユーザー

退会済みユーザー

総合スコア0

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

0クリップ

投稿2020/03/13 22:07

関数の実行時間を計る@stopwatchデコレータと、withブロック単位で処理にかかった時間を計るtimerコンテキストマネジャがあります。
どちらがデコレータでどちらがコンテキストマネジャか分からなくなるので名前を統一したいのですが、御存知の通りPythonでは関数のオーバーロードはできません。
文脈によって内部で条件分岐できないかと考えてみたのですが、うまい方法が思いつきません。
どなたかお力を貸していただけないでしょうか?

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

Python

1import time 2from contextlib import contextmanager 3from functools import wraps 4from typing import Callable 5 6 7def stopwatch(func: Callable) -> Callable: 8 @wraps(func) 9 def wrapper(*args, **kwargs): 10 start = time.time() 11 result = func(*args, **kwargs) 12 elapsed_time = time.time() - start 13 print(f"{func.__name__}: {elapsed_time}") 14 return result 15 16 return wrapper 17 18 19@contextmanager 20def timer(label: str): 21 start = time.time() 22 yield 23 elapsed_time = time.time() - start 24 print(f"{label}: {elapsed_time}") 25 26 27@stopwatch 28def sleep(): 29 time.sleep(0.1) 30 31 32def main(): 33 sleep() 34 35 with timer("sleep_with"): 36 time.sleep(0.1) 37 38 39if __name__ == "__main__": 40 main()
s.k👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

contextlib.ContextDecorator がそのためのものかと

コンテキストマネージャをデコレータとしても使用できるようにする基底クラスです。

投稿2020/03/14 00:38

quickquip

総合スコア11038

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/03/14 01:18

きちんと用意されていたのですね。ContextDecoratorを使った書き方も考えてみます。 ありがとうございました。
guest

0

ベストアンサー

デコレータは 別の関数を返す関数 でしか御座いませんが、 with 文で使用するコンテキストマネージャの実体は __enter__ 及び __exit__ という 二つの dunder methods を持つオブジェクト です。関数を @contextlib.contextmanager で wrap するのは、あくまでこのコンテキストマネージャ型オブジェクトを 関数から生成することができるデコレータ によって生成している (正確には、それを生成するファクトリ関数を生成している) に過ぎません。

そのため、どうしても文脈によって使われ方を判断したいのであれば、あるオブジェクトが デコレータとして使われているのか、それともコンテキストマネージャとして使われているのかは、呼び出される dunder method によって区別することが可能 かと存じます。具体的には、 @contextlib.contextmanager を使わず、 Callable 扱いで __call__ が呼ばれた場合はデコレータとして、コンテキストマネージャ扱いで __enter__ が呼ばれた場合はコンテキストマネージャとして動作するようなオブジェクトを作ればよいわけですから、次のような実装が求めるものに近しいのではないかと思われます。

python

1import time 2from functools import wraps 3from typing import Callable 4 5 6class stopwatch: 7 def __init__(self, label=None): 8 self._label = label 9 10 def __call__(self, func: Callable) -> Callable: 11 @wraps(func) 12 def wrapper(*args, **kwargs): 13 start = time.time() 14 result = func(*args, **kwargs) 15 elapsed_time = time.time() - start 16 print(f"{func.__name__}: {elapsed_time}") 17 return result 18 19 return wrapper 20 21 def __enter__(self): 22 self._start = time.time() 23 24 def __exit__(self, exc_type, exc_value, traceback): 25 elapsed_time = time.time() - self._start 26 print(f"{self._label}: {elapsed_time}") 27 28 29@stopwatch() 30def sleep(): 31 time.sleep(0.1) 32 33 34def main(): 35 sleep() 36 37 with stopwatch("sleep_with"): 38 time.sleep(0.1) 39 40 41if __name__ == "__main__": 42 main()

但し、上記コードで @stopwatch() としている通り、 あくまで区別が可能となるのはオブジェクトを生成した後 なので、デコレータとして使うときも新規のオブジェクトを生成する必要性が出て参ります。もしもデコレータは毎回生成せずに静的な Callable として、コンテキストマネージャは label 付で毎回生成する型として実装することも求めるとなると、その場合はデコレータ呼出しとコンテキストマネージャ生成がどちらも関数呼び出し (またはオブジェクト生成) の文脈となってしまい、区別することは出来ないのではないかと思われます。その意味では、厳密には御質問の「関数 を文脈から両方に対応させる」という要件自体は (リフレクションを用いて頑張るのでなければ) 不可能と言えるかもしれません。

そのことを考えれば、文脈を踏まえて、といった考え方では無く、単純に どちらも関数の呼び出しであり、引数が異なるのだから、引数で判別すればよい と考えた方が、実装は単純で分かりやすくなるかもしれません。デコレータとして使われる場合、引数に Callable が渡されますから、その場合はデコレータとして、そうでない場合はコンテキストマネージャのファクトリとして振舞うような分岐を入れれば、これでも概ね求める要件は満たせるように思います。

python

1import time 2from contextlib import contextmanager 3from functools import wraps 4from typing import Callable 5 6 7def stopwatch(func_or_label): 8 def decorator_stopwatch(func: Callable) -> Callable: 9 @wraps(func) 10 def wrapper(*args, **kwargs): 11 start = time.time() 12 result = func(*args, **kwargs) 13 elapsed_time = time.time() - start 14 print(f"{func.__name__}: {elapsed_time}") 15 return result 16 17 return wrapper 18 19 @contextmanager 20 def contextmanager_stopwatch(label: str): 21 start = time.time() 22 yield 23 elapsed_time = time.time() - start 24 print(f"{label}: {elapsed_time}") 25 26 if callable(func_or_label): 27 return decorator_stopwatch(func_or_label) 28 else: 29 return contextmanager_stopwatch(func_or_label)

投稿2020/03/14 00:34

argparse

総合スコア1017

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/03/14 01:16

解答のコードに至るまでの考え方まで詳しく教えていただいて、ありがとうございます。 勉強になりました。 質問とは多少逸れるのですが、内部でyieldするcontextmanager_stopwatch()に、もし返り値のtype hintsを付けるとすれば何になるのでしょうか? with文内は何でもありうるので、詳細なしのGeneratorでしょうか?
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問