分离进度跟踪与循环逻辑

3 投票
3 回答
1087 浏览
提问于 2025-04-16 13:40

假设我想用一个叫做ProgressMeter的进度条打印机来跟踪一个循环的进度(具体内容可以参考这个链接)。

def bigIteration(collection):
    for element in collection:
        doWork(element)

我希望能够随时打开或关闭这个进度条。而且出于性能考虑,我也想每隔x步才更新一次进度条。我最初的做法是

def bigIteration(collection, progressbar=True):
    if progressBar:
        pm = progress.ProgressMeter(total=len(collection))
        pc = 0
    for element in collection:
        if progressBar:
            pc += 1
            if pc % 100 = 0:
                pm.update(pc)
        doWork(element)

不过,我对此并不满意。从“美观”的角度来看,循环的功能代码现在被一些通用的进度跟踪代码“污染”了。

你能想到什么方法来干净地分离进度跟踪代码和功能代码吗?(可以有一个进度跟踪的装饰器之类的吗?)

3 个回答

1

我的方法大概是这样的:

循环的代码会在进度百分比变化时(或者想要报告进度时)输出这个百分比。然后,负责跟踪进度的代码会从这个生成器中读取数据,直到它没有数据为止;每读取一次就更新一次进度条。

不过,这样做也有一些缺点:

  • 你需要一个函数来调用它,即使没有进度条,因为你还是需要从生成器中读取数据直到它没有数据。
  • 你不能轻松地在最后返回一个值。一个解决办法是把返回值包裹起来,这样进度方法就可以判断这个函数是输出了进度更新还是返回了一个值。其实,把进度更新包裹起来可能更好,这样常规的返回值就可以直接输出,而不需要包裹——但这样就需要更多的包裹,因为每次进度更新都需要包裹,而不是只包裹一次。
2

你可以把 bigIteration 改写成一个生成器函数,像这样:

def bigIteration(collection):
    for element in collection:
        doWork(element)
        yield element

然后,你可以在这个之外做很多事情:

def mycollection = [1,2,3]
if progressBar:
    pm = progress.ProgressMeter(total=len(collection))
    pc = 0
    for item in bigIteration(mycollection):
        pc += 1
        if pc % 100 = 0:
            pm.update(pc)
else:
    for item in bigIteration(mycollection):
        pass
6

看起来这段代码可以用到一种叫做空对象模式的技巧。

# a progress bar that uses ProgressMeter
class RealProgressBar:
     pm = Nothing
     def setMaximum(self, max):
         pm = progress.ProgressMeter(total=max)
         pc = 0
     def progress(self):
        pc += 1
        if pc % 100 = 0:
            pm.update(pc)

# a fake progress bar that does nothing
class NoProgressBar:
    def setMaximum(self, max):
         pass 
    def progress(self):
         pass

# Iterate with a given progress bar
def bigIteration(collection, progressBar=NoProgressBar()):
    progressBar.setMaximum(len(collection))
    for element in collection:
        progressBar.progress()
        doWork(element)

bigIteration(collection, RealProgressBar())

(请原谅我的法语,呃,Python,这不是我的母语;)不过我希望你能明白我的意思。)

这样做可以把进度更新的逻辑从循环中移出去,但你在里面还是有一些和进度相关的调用。

如果你从集合中创建一个生成器,能够在你遍历的时候自动跟踪进度,就可以去掉这部分代码。

 # turn a collection into one that shows progress when iterated
 def withProgress(collection, progressBar=NoProgressBar()):
      progressBar.setMaximum(len(collection))
      for element in collection:
           progressBar.progress();
           yield element

 # simple iteration function
 def bigIteration(collection):
    for element in collection:
        doWork(element)

 # let's iterate with progress reports
 bigIteration(withProgress(collection, RealProgressBar()))

这种方法让你的bigIteration函数保持不变,而且非常灵活。例如,假设你还想在这个大迭代中添加取消功能。只需要创建另一个可以取消的生成器就行了。

# highly simplified cancellation token
# probably needs synchronization
class CancellationToken:
     cancelled = False
     def isCancelled(self):
         return cancelled
     def cancel(self):
         cancelled = True

# iterates a collection with cancellation support
def withCancellation(collection, cancelToken):
     for element in collection:
         if cancelToken.isCancelled():
             break
         yield element

progressCollection = withProgress(collection, RealProgressBar())
cancellableCollection = withCancellation(progressCollection, cancelToken)
bigIteration(cancellableCollection)

# meanwhile, on another thread...
cancelToken.cancel()

撰写回答