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

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

新規登録して質問してみよう
ただいま回答率
85.48%
コードレビュー

コードレビューは、ソフトウェア開発の一工程で、 ソースコードの検査を行い、開発工程で見過ごされた誤りを検出する事で、 ソフトウェア品質を高めるためのものです。

Python

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

Q&A

解決済

3回答

1193閲覧

contextmanagerを使ってIPythonの%%timeitのようなものをつくりたい

退会済みユーザー

退会済みユーザー

総合スコア0

コードレビュー

コードレビューは、ソフトウェア開発の一工程で、 ソースコードの検査を行い、開発工程で見過ごされた誤りを検出する事で、 ソフトウェア品質を高めるためのものです。

Python

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

0グッド

0クリップ

投稿2020/07/09 19:02

編集2020/07/11 05:15

contextmanagerを使ってwithブロック内の処理を複数回実行し平均の速度を計る機能を考えています。
JupyterNotebooksなどのIPython系での%%timeitの簡易版と思っていただければと思います。

下記のコードを書いてみたのですが、AttributeError: __enter__が出てしまいます。
contextmanager_timer()の部分をreturn contextmanager_timer()とするとエラーは出ないのですが、1度しか実行されないためにコードの意味がなくなります。
どう書き換えればよいでしょうか?

また、計測する処理にprint()などが含まれていた場合os.devnullに流して静かに実行することも考えているのですが、そちらについてもアドバイスがあれば伺いたいです。

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

Python

1import time 2from contextlib import contextmanager 3 4 5def simple_timeit(label: str, exec_limit: int = 1000, time_limit: int = 5): 6 @contextmanager 7 def contextmanager_timer(): 8 start = time.time() 9 yield 10 nonlocal total_time 11 total_time += time.time() - start 12 13 cnt = 0 14 total_time = 0 15 while cnt < exec_limit and total_time < time_limit: 16 cnt += 1 17 contextmanager_timer() 18 print(f"{label}: {total_time / cnt:.8f}s on average of {cnt} runs.") 19 20 21def main(): 22 with simple_timeit("for loops"): 23 for _ in range(10 ** 7): 24 pass 25 26 27if __name__ == "__main__": 28 main() 29

error

1for loops: 0.00000000s on average of 1000 runs. 2Traceback (most recent call last): 3 File "prog.py", line 28, in <module> 4 main() 5 File "prog.py", line 22, in main 6 with simple_timeit("for loops"): 7AttributeError: __enter__

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

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

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

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

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

guest

回答3

0

余談です。(このデコレータ自体は参考にはしないでください

with my_timeitfor _ in my_timeit に置き換えるデコレータ

python

1@recompile_with_timeit 2def main(): 3 with my_timeit(repeat=3): 4 print("TEST") 5 6 7if __name__ == '__main__': 8 main()

Output:

start TEST TEST TEST end

デコレーターが必要になる時点で本末転倒感がありますが、興味本位で実装してみました。
(コードは後述)

※ 対象のコードから、目的の動作を得られるように強引に実装しただけなので、
決して質問に対する回答としてのお勧めの解決策ではありません(重要)。デメリットが多数 あります。

  • pylint 等でエラーになる
  • デバッガのステップ実行でも対応するコードが得られない

もし with -> for _ in のタイプ量が問題なら、スニペット登録する方が賢い解決方法です。

興味深い点があって、こちらが本題
IPython の timeit でも、使い方は異なりますが
実行時にコードから関数を作り、何か構文木(AST)を操作するような事をしてました。

IPythonより引用

# This codestring is taken from timeit.template - we fill it in as an # AST, so that we can apply our AST transformations to the user code # without affecting the timing code. timeit_ast_template = ast.parse('def inner(_it, _timer):\n' ' setup\n' ' _t0 = _timer()\n' ' for _i in _it:\n' ' stmt\n' ' _t1 = _timer()\n' ' return _t1 - _t0\n') timeit_ast = TimeitTemplateFiller(ast_setup, ast_stmt).visit(timeit_ast_template) timeit_ast = ast.fix_missing_locations(timeit_ast)

python

1def inner(_it, _timer): 2 setup 3 _t0 = _timer() 4 for _i in _it: 5 stmt 6 _t1 = _timer() 7 return _t1 - _t0

という関数の構文木を生成して、setupstmt をコードに置き換える。
推測ですが、stmt 置き換えは、大量に繰り返す場合の関数呼び出しのオーバーヘッド削減でしょうか。

時間計測の処理を最適化するのに、timeitの実装は参考になると思いました。
因みに、標準モジュールのtimeitでも、文字列ベースで同様の事を行ってます。


デコレータの実装

python

1#!/usr/bin/env python3.8 2""" 3rewrite with-generator AST 4""" 5 6import ast 7import inspect 8 9 10def my_timeit(repeat): 11 print("start") 12 yield from range(repeat) 13 print("end") 14 15 16class Transformer(ast.NodeTransformer): 17 """ 18 AST変換 19 20 - 関数の再定義にデコレータが2重に呼ばれてしまうので、削除 21 - with my_timeit -> for _ in my_timeit に書き換え 22 23 """ 24 def visit_FunctionDef(self, node): 25 self.generic_visit(node) 26 node.decorator_list = [ 27 x for x in node.decorator_list if x.id != "recompile_with_timeit" 28 ] 29 return node 30 31 def visit_With(self, node): 32 expr = node.items[0].context_expr 33 if isinstance(expr, ast.Call) and expr.func.id == "my_timeit": 34 return ast.For( 35 target=ast.Name(id="_", ctx=ast.Store()), 36 iter=expr, 37 body=node.body, 38 orelse=[]) 39 return node 40 41 42def recompile_with_timeit(func): 43 src = inspect.getsource(func) 44 45 node = ast.parse(src) 46 node = Transformer().visit(node) 47 node = ast.fix_missing_locations(node) 48 49 new_code = compile(node, "<string>", "exec") 50 namespace = {} 51 exec(new_code, globals(), namespace) 52 return namespace.get(func.__name__) 53 54 55@recompile_with_timeit 56def main(): 57 "Test code" 58 59 with my_timeit(repeat=10): 60 print("TEST") 61 62 for _ in range(10): 63 pass 64 65 # 他の with ステートメントに影響がないことを確認 66 with open(__file__, encoding="utf-8") as stream: 67 for line in map(str.rstrip, stream): 68 print(line) 69 70 71if __name__ == '__main__': 72 main() 73

投稿2020/07/12 01:02

編集2020/07/12 21:29
teamikl

総合スコア8664

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

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

退会済みユーザー

退会済みユーザー

2020/07/12 01:59

ちょうど最近ASTの勉強をしていたので、こういう風でも使えるのかーと驚いています。 recompile_with_timeitを理解できるように、処理の流れを丹念に追いかけてみます。
guest

0

ベストアンサー

contextmanagerを使ってwithブロック内の処理を複数回実行し

with ではできません。

同様の事が実現できる言語(Smalltalk等)では、
ブロック・オブジェクトを引数に取ることができますが、
Pythonでは code オブジェクト、関数オブジェクト辺りがこれに相当します。

問題点は、コンテキストマネージャからwith のブロックを再利用する手段がない事。
with では yield により実行コンテキストが「切り替わる」といった方が解りやすいかもしれません。

テストとして実行することを考えると、
デコレーター + 関数として実装が率直な解決策です。
※ 実装は略

python

1@timeit_dec(repeat=10) 2def test_for_loop(): # <- デコレータに渡るのはここの関数オブジェクト 3 print("TEST")

一時的なケースで使いたいなら代案
記述は with -> for _ in になりますが、
ジェネレーターとして実装する方法 ... こちらの方がやりたいことに近いのではないでしょうか。

python

1def my_timeit(repeat): 2 print("start") 3 4 yield from range(repeat) 5 6 print("end") 7 8 9if __name__ == '__main__': 10 for _ in my_timeit(repeat=10): 11 print("TEST")

ここから更に前処理・後処理の部分は、
デコレーターやコンテキストマネージャーに括り出せます。


計測する処理にprint()などが含まれていた場合os.devnullに流して静かに実行すること

  • Python では sys.stdout にファイルライクなオブジェクトを指定するとリダイレクトできます。
  • 局所的なリダイレクトは contextlib.redirect_stdout も併せて調べて見て下さい。
  • print のみなら

__builtins__.print 自体を差し替えると、若干内部のメソッド呼び出しが減らせるかもしれません。
が、logging モジュールを使っている場合等も考慮すると、標準入力のリダイレクトに分があり。

但し、マルチスレッドの場合等は、単純にリダイレクトだけとはいきません、
テストは別プロセスで実行する等の工夫が必要です。

IOの速度を測るのでなければ、基本的に単体テストが必要なコードからはprint文は省けるはずです。
可能ならIOは切り離して関数を実装した方がテストしやすくなります。


問題点: 時間制限を実装しようとしてますが
以下のコードはループが完了するまで、時間チェックに処理が戻らない為
意図した時間で終わりません。

for _ in range(10 ** 7): pass 経過時間チェック

繰り返し毎に毎回チェックされるようにする必要があります。

for _ in range(10 * 7): 経過時間のチェック pass

→ テスト対象のコードを直ぐに終わるような小さなコードに分割する
while 文で単純な分割が難しい場合は、ジェネレーターにするなどの工夫が必要です。

python

1import time 2 3def my_timeit(label, repeat, timeout=float("inf"), count=0): 4 start = time.time() 5 time_limit = start + timeout 6 7 for _ in range(repeat): 8 if time.time() > time_limit: 9 break 10 count += 1 11 yield 12 13 total_time = time.time() - start 14 15 print(f"{label}: {total_time / count:.8f}s on average of {count} runs.") 16 17 18if __name__ == '__main__': 19 for _ in my_timeit("test", repeat=20, timeout=10): 20 print("TEST") 21 time.sleep(1)

追記: 処理内容によっては誤差かもしれないけど、
「経過時間チェック」により時間は遅くなるので、用途次第ではこの実装は適切でないかもしれません。
他の案: 一定時間経過後にシグナルを送る等、テストコードに影響しない他の仕組みで解決。
参考: pytest-timeout

追記2: yymmt さんの投稿を見て、気づいた点
time.time は time.perf_timer の方が適切でした。

time.time()はシステムに設定された時刻に依存するので

この関数が返す値は通常減少していくことはありませんが、この関数を 2 回呼び出し、その呼び出しの間にシステムクロックの時刻を巻き戻して設定した場合には、以前の呼び出しよりも低い値が返ることがあります。

timeitの実装も timer.perf_counter を利用しています。

投稿2020/07/11 09:41

編集2020/07/11 23:12
teamikl

総合スコア8664

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

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

0

少し考えてみましたがwithブロックの中をループする処理は出来ないように思います。contextmanagerは単に__enter__と__exit__を使いやすくしたものですので

python

1with loop_func(): 2 do_something()

という構文は

python

1__enter__で定義した前処理 2do_something() 3__exit__で定義した後処理

として展開されます。この形式でループされるようには記述出来ないためです。そこで外で回すか、中で回すか、という話になりますが、これをコードにして表すと

python

1# 外で回す 2for _ in range(n): 3 __enter__で定義した前処理 4 do_something() 5 __exit__で定義した後処理 6 7# 以下と等価な処理 8# for _ in range(n): 9# with simple_timeit(): 10# do_something()

もしくは

python

1# 中で回す 2__enter__で定義した前処理 3for _ in range(n): 4 do_something() 5__exit__で定義した後処理 6 7# 以下と等価な処理 8# with simple_timeit(): 9# for _ in range(n): 10# do_something()

となり、両者ともwith構文でシンプルに記述するという目的とは異なっていると思います。with構文を諦めて単純なforループにするのであれば対応方法はありますが、やはりこれもやりたい事とは違うのだろうなぁと思います。参考までにサンプルコードを示します。

python

1import io 2import sys 3import time 4from contextlib import redirect_stdout, redirect_stderr 5 6 7def simple_timeit( 8 label: str, 9 exec_limit: int = 1000, 10 time_limit: int = 5, 11 stdout: io.TextIOWrapper = sys.stdout, 12 stderr: io.TextIOWrapper = sys.stderr, 13): 14 with redirect_stdout(stdout), redirect_stderr(stderr): 15 cnt = 0 16 total_time = 0.0 17 while cnt < exec_limit and total_time < time_limit: 18 start = time.perf_counter() 19 yield cnt 20 total_time += time.perf_counter() - start 21 cnt += 1 22 print(f"{label}: {total_time / cnt:.8f}s on average of {cnt} runs.") 23 return cnt 24 25 26def main(): 27 for i in simple_timeit("for loops", stdout=None, stderr=None): 28 print(i) 29 print("stdout: hello") 30 print("stderr: world", file=sys.stderr) 31 32 33if __name__ == "__main__": 34 main()

投稿2020/07/11 09:34

yymmt

総合スコア1615

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問