Python 通过 REPL 与阻塞循环交互的最佳实践
Python best practice for interacting with blocking loop via REPL
我的程序涉及阻塞循环。我想在程序处于 运行ning.
时使用 python REPL 修改程序状态
上下文:
我的 python 代码中经常有一个阻塞循环:
# main.py
import itertools
import time
global_state = 123
if __name__ == "__main__":
print("Starting main loop")
m = 0
for n in itertools.count():
time.sleep(1) # "computation"
global_state += 1
m += 10
print(f"{n=}, {m=}, {global_state=}")
当我在命令行 运行 这个程序时,我得到这样的东西:
$ python -i main.py
Starting main loop
n=0, m=10, global_state=124
n=1, m=20, global_state=125
n=2, m=30, global_state=126
...
循环将 运行 数小时或数天。有时,在循环 运行ning 期间,我希望以交互方式修改某些程序状态。例如,我想设置 global_state = -1000
或 m = 75
。但是 loop
函数正在阻塞...
交互式修改程序状态的方法
方法 1:停止并重新启动循环
我可以使用 KeyboardInterrupt
来停止循环。由于我已经使用 -i
交互式标志调用了 python
,因此我可以在交互式 REPL 中修改全局状态。然后我重新启动循环。这是一个简单的方法,但它有缺点:
- 如果循环修改了全局状态(或有任何副作用),则在引发 KeyboardInterrupt 时全局状态可能会处于不一致的状态;异常可能在循环控制流中的任何一点引发,正确处理此异常可能很棘手,尤其是在业务逻辑复杂的情况下。
- 当重新开始循环时,计数器
n
将被重置为零。循环使用的其他状态变量(例如 m
)也可能被重置或处于不一致状态。
方法 2:使用线程
我可以将循环包装在一个函数中,然后使用 threading
到 运行 另一个线程中的函数:
def loop():
print("Starting main loop")
m = 0
for n in itertools.count():
time.sleep(1) # "computation"
global_state += 1
m += 10
print(f"{n=}, {m=}, {global_state=}")
import threading
thread = threading.Thread(target=loop)
thread.start()
当我调用 python -i main.py
时,我可以使用 REPL 与全局状态交互,作为另一个线程中的循环 运行。 loop
函数仍然像以前一样从 print
到 stdout
。这种方法的缺点是:
- 我无法再与循环的局部状态交互(例如,我无法修改
m
),因为局部状态被包裹在一个函数中。这是一个主要缺点。
- 使用线程使程序更脆弱;线程可能会崩溃,需要小心处理线程中引发的异常。如果线程阻塞或崩溃或挂起,则很难恢复。
- 停止循环变得更加困难,因为 KeyboardInterrupt 不再是一个选项(相反,我需要与线程“合作”,向它发送一些它应该自我终止的信号)。
方法 3:从循环内进入 REPL
可以配合循环掉入repl:
import os
import code
if __name__ == "__main__":
...
for n in itertools.count():
... # business logic
if os.stat("use_repl.txt").st_size > 0: # file nonempty
code.interact(local=locals())
这里我们使用一个文件在我们想要修改状态时向循环发出信号。如果“use_repl.txt”文件是非空的,那么循环然后调用 code.interact
来创建一个交互式控制台。这有一个额外的好处,即使循环被包裹在一个函数中也能正常工作;局部变量被加载到交互式控制台中。
缺点:
- 每次通过循环读取“use_repl.txt”文件感觉有点笨拙。但是,还有什么方法可以(合作地)向循环发出信号,表明它应该创建一个 REPL?
- 调用
code.interact
时循环暂停。这不同于基于 threading
的解决方案,其中循环 运行 是连续的。这可能是优势也可能是劣势,具体取决于您的用例。
code.interact
创建的 REPL 不如 python 的本机交互模式完善。例如,制表符完成功能不再有效。
调用 code.interact
的替代方法是调用 breakpoint
内置函数。另一种选择是调用 break
关键字,退出循环。然后可以修改状态并重新启动循环(如方法 1 中所示),而不必担心 KeyboardInterrupt 会创建不一致的状态。
问题:
有没有我错过的方法?所考虑的每种方法都有明显的缺点......是否有关于实现预期结果的最佳实践?
调试程序,当你想要inspect/modify状态时暂停它,然后恢复。
受 xaa 的启发 :
import pdb
import signal
signal.signal(signal.SIGINT, pdb.Pdb().sigint_handler)
这会设置一个信号处理程序来捕获 SIGINT
(否则会被翻译成 KeyboardInterrupt
),并在按下 CTRL-C
时掉落到 pdb
。因此 CTRL-C
可用于中断循环并在任何时候检查或修改状态,并且可以通过在 pdb
提示符下键入 continue
来恢复执行。
我的程序涉及阻塞循环。我想在程序处于 运行ning.
时使用 python REPL 修改程序状态上下文:
我的 python 代码中经常有一个阻塞循环:
# main.py
import itertools
import time
global_state = 123
if __name__ == "__main__":
print("Starting main loop")
m = 0
for n in itertools.count():
time.sleep(1) # "computation"
global_state += 1
m += 10
print(f"{n=}, {m=}, {global_state=}")
当我在命令行 运行 这个程序时,我得到这样的东西:
$ python -i main.py
Starting main loop
n=0, m=10, global_state=124
n=1, m=20, global_state=125
n=2, m=30, global_state=126
...
循环将 运行 数小时或数天。有时,在循环 运行ning 期间,我希望以交互方式修改某些程序状态。例如,我想设置 global_state = -1000
或 m = 75
。但是 loop
函数正在阻塞...
交互式修改程序状态的方法
方法 1:停止并重新启动循环
我可以使用 KeyboardInterrupt
来停止循环。由于我已经使用 -i
交互式标志调用了 python
,因此我可以在交互式 REPL 中修改全局状态。然后我重新启动循环。这是一个简单的方法,但它有缺点:
- 如果循环修改了全局状态(或有任何副作用),则在引发 KeyboardInterrupt 时全局状态可能会处于不一致的状态;异常可能在循环控制流中的任何一点引发,正确处理此异常可能很棘手,尤其是在业务逻辑复杂的情况下。
- 当重新开始循环时,计数器
n
将被重置为零。循环使用的其他状态变量(例如m
)也可能被重置或处于不一致状态。
方法 2:使用线程
我可以将循环包装在一个函数中,然后使用 threading
到 运行 另一个线程中的函数:
def loop():
print("Starting main loop")
m = 0
for n in itertools.count():
time.sleep(1) # "computation"
global_state += 1
m += 10
print(f"{n=}, {m=}, {global_state=}")
import threading
thread = threading.Thread(target=loop)
thread.start()
当我调用 python -i main.py
时,我可以使用 REPL 与全局状态交互,作为另一个线程中的循环 运行。 loop
函数仍然像以前一样从 print
到 stdout
。这种方法的缺点是:
- 我无法再与循环的局部状态交互(例如,我无法修改
m
),因为局部状态被包裹在一个函数中。这是一个主要缺点。 - 使用线程使程序更脆弱;线程可能会崩溃,需要小心处理线程中引发的异常。如果线程阻塞或崩溃或挂起,则很难恢复。
- 停止循环变得更加困难,因为 KeyboardInterrupt 不再是一个选项(相反,我需要与线程“合作”,向它发送一些它应该自我终止的信号)。
方法 3:从循环内进入 REPL
可以配合循环掉入repl:
import os
import code
if __name__ == "__main__":
...
for n in itertools.count():
... # business logic
if os.stat("use_repl.txt").st_size > 0: # file nonempty
code.interact(local=locals())
这里我们使用一个文件在我们想要修改状态时向循环发出信号。如果“use_repl.txt”文件是非空的,那么循环然后调用 code.interact
来创建一个交互式控制台。这有一个额外的好处,即使循环被包裹在一个函数中也能正常工作;局部变量被加载到交互式控制台中。
缺点:
- 每次通过循环读取“use_repl.txt”文件感觉有点笨拙。但是,还有什么方法可以(合作地)向循环发出信号,表明它应该创建一个 REPL?
- 调用
code.interact
时循环暂停。这不同于基于threading
的解决方案,其中循环 运行 是连续的。这可能是优势也可能是劣势,具体取决于您的用例。 code.interact
创建的 REPL 不如 python 的本机交互模式完善。例如,制表符完成功能不再有效。
调用 code.interact
的替代方法是调用 breakpoint
内置函数。另一种选择是调用 break
关键字,退出循环。然后可以修改状态并重新启动循环(如方法 1 中所示),而不必担心 KeyboardInterrupt 会创建不一致的状态。
问题:
有没有我错过的方法?所考虑的每种方法都有明显的缺点......是否有关于实现预期结果的最佳实践?
调试程序,当你想要inspect/modify状态时暂停它,然后恢复。
受 xaa 的启发
import pdb
import signal
signal.signal(signal.SIGINT, pdb.Pdb().sigint_handler)
这会设置一个信号处理程序来捕获 SIGINT
(否则会被翻译成 KeyboardInterrupt
),并在按下 CTRL-C
时掉落到 pdb
。因此 CTRL-C
可用于中断循环并在任何时候检查或修改状态,并且可以通过在 pdb
提示符下键入 continue
来恢复执行。