web-dev-qa-db-ja.com

関数のステータスを更新する方法

関数を呼び出すとき、関数がいくつかのマイルストーンに達したときに更新を受け取りたいです。

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でこれを構築する方法があり、両方の短所を回避できるかどうかを知りたいです。

4
bustawin

あなたが言及したアプローチはすべて有効であり、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ループを使用して少し単純化できますが、それでも異なるイベントを手動で処理する必要があります。単一タイプの進行状況イベントがある場合にのみ、コールバックベースのソリューションと同じか、それよりも低い複雑さになります。

3
amon

表示している関数は、少なくとも4つの他の関数を呼び出します。したがって、呼び出し元と合わせて5になります。

これは、その周りにカプセル化クラスを構築するのに最適な候補のようです。

必要なコールバックをそのクラスのコンストラクターに渡して、メンバー変数に格納するだけです。このようにして、do_somethingは署名を変更せずにコールバックを使用できます。もちろん、最初に新しいクラスのオブジェクトを作成する必要があるので、それを呼び出す方法は少し異なりますが、do_somethingへの呼び出しから切り離されたオブジェクトの作成(コールバックの受け渡し)を行うことができます。

2
Doc Brown

これは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を返すことです。イベントとして機能します。

2
candied_orange