问题描述
我的程序涉及一个阻塞循环。我想在程序运行时使用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
...
循环将运行数小时或数天。有时,当循环运行时,我希望以交互方式修改某些程序状态。例如,我想设置global_state = -1000
或m = 75
。但是loop
函数正在阻止...
交互式修改程序状态的方法
方法1:停止并重新启动循环
我可以使用KeyboardInterrupt
来停止循环。由于我已经使用python
交互式标记调用了-i
,因此我可以在交互式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=},{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会创建不一致的状态。
问题:
我错过了什么方法吗?所考虑的每种方法都有明显的缺点...关于实现预期结果是否有最佳实践?
解决方法
调试程序,在要检查/修改状态时将其暂停,然后继续。
,受到xaa的answer的启发:
col1 = {
'abc': '#323233','xyz': '#C92735',}
def convert(value):
value /= 1000
return str(round(value,2))+”k”
fig = px.bar(df,x="id",text=[convert(_) for _ in df[‘x’].values.tolist()] y="Total",color="sys_type",barmode="group",color_discrete_map=col1)
fig.layout = go.Layout(
title=go.layout.Title(text="Total value for each id",x=0.5),xaxis_title="Id",yaxis_title="Total Value"
)
fig.show()
这将设置捕获import pdb
import signal
signal.signal(signal.SIGINT,pdb.Pdb().sigint_handler)
的信号处理程序(否则将转换为SIGINT
),并在按下KeyboardInterrupt
时放到pdb
中。因此,CTRL-C
可用于在任何时候中断循环并检查或修改状态,并且可以通过在CTRL-C
提示符下键入continue
来恢复执行。