関数を呼び出すとき、関数がいくつかのマイルストーンに達したときに更新を受け取りたいです。
def do_something():
start_with_something()
# update
for x in iterate_something():
# update each iteration
do_something_expensive()
# update again
finish_with_something_else()
# final update
return the_Nice_result
これにより、たとえば、進行状況バーでUIを更新できます。または、最初の更新後にユーザーにいくつかの初期結果を表示します。
これを行う方法は、コールバックを使用することです。
def do_something(initial_update, iteration_update, update_again, final_update):
some_initial_data =start_with_something()
initial_update(some_initial_data)
for x in iterate_something():
iteration_update(x)
...
Pythonでの別の方法は、ジェネレータを(乱用?)することです。
def do_something():
some_initial_data= start_with_something()
yield some_initial_data
for x in iterate_something():
yield x
do_something_expensive()
yield ...
どちらの状況にも長所と短所がありますが、私はそれらのどれも実際には空想していません。たとえば、最初のものは関数シグネチャを乱雑にし、渡された情報を更新し、2番目のものは反復メカニズムを悪用します。
最後に私はdo_something
関数をUI要素から分離しました。
この状況に対するアプローチ、または特にPythonでこれを構築する方法があり、両方の短所を回避できるかどうかを知りたいです。
あなたが言及したアプローチはすべて有効であり、Pythonで完全に自然に感じられるアプローチはありません。
コールバックを引数として渡すことは、関数の使用が複雑にならないという単純な理由で通常最も適切なアプローチであり、イベントハンドラーは低いセレモニーで実装できます。このようなもの:
def slow_function(*arguments, on_iteration=None, on_final=None):
state = init(arguments)
for x in whatever():
state += x
if on_iteration is not None:
on_iteration(x)
if on_final is not None:
on_final()
return state
def on_iteration(x):
print("iteration:", x)
slow_function(1, 2, 3,
on_iteration=on_iteration,
on_final=lambda: print("done"))
関数にそのようなイベントが多数ある場合は、オブジェクトを使用する方が便利です。
# by default, handlers do nothing (null object pattern)
class SlowFunctionProgressHandler:
def on_iteration(self, x): pass
def on_final(self): pass
def slow_function(*arguments, event=SlowFunctionProgressHandler()):
...
# can implement/override a single event type
class MyProgessHandler(SlowFunctionProgressHandler):
def on_iteration(self, x): print("iteration:", x)
slow_function(1, 2, 3, event=MyProgressHandler())
これは型注釈でもうまく機能します!
ジェネレーターを使用してyield
進行状況メッセージをエレガントに聞こえるかもしれませんが、適切な場合もありますが、実際にはさまざまな問題が発生します。
return
を使用することは可能ですが、実際にはかなりトリッキーです最後のポイントを説明するには、次のようにする必要があります。
def slow_function(*arguments):
state = init(arguments)
for x in whatever():
state += x
yield ('iteration', x)
yield ('final',)
return state
progress = slow_function(1, 2, 3)
result = None
while True:
try:
message, *args = next(progress)
except StopIteration as e:
result = e.value
break
if message == 'iteration':
...
Elif message == 'final':
...
return
が可能なジェネレータの使用は非常に面倒です。ジェネレーターが返らない場合は、forループを使用して少し単純化できますが、それでも異なるイベントを手動で処理する必要があります。単一タイプの進行状況イベントがある場合にのみ、コールバックベースのソリューションと同じか、それよりも低い複雑さになります。
表示している関数は、少なくとも4つの他の関数を呼び出します。したがって、呼び出し元と合わせて5になります。
これは、その周りにカプセル化クラスを構築するのに最適な候補のようです。
必要なコールバックをそのクラスのコンストラクターに渡して、メンバー変数に格納するだけです。このようにして、do_something
は署名を変更せずにコールバックを使用できます。もちろん、最初に新しいクラスのオブジェクトを作成する必要があるので、それを呼び出す方法は少し異なりますが、do_something
への呼び出しから切り離されたオブジェクトの作成(コールバックの受け渡し)を行うことができます。
これはIOです。確かに、あなたはそれを別のように聞こえるようにしようとしていますが、これはIOです。
それについて「Pythonicではない」ことは何もありません。これは、printステートメントで実行できます。ただし、これは純粋に機能的ではありません。あなたは副作用を求めています。ただし、完全にオブジェクト指向です。
def do_something(out=Null()):
start_with_something()
out.update()
for x in iterate_something():
out.iteration()
do_something_expensive()
out.again()
finish_with_something_else()
out.final()
return the_Nice_result
nullオブジェクトパターンout
のバージョン 呼び出されても何もしない を使用して、out
を無効にすることができます。これは良いデフォルトになります。
ここで重要なのは、これらのoutメソッドはすべてvoidを返すことです。イベントとして機能します。