如何解决如何在代码更改最少的情况下遍历完整的Python脚本?
免责声明:我是科学家,而不是开发人员。我更喜欢可读性和可维护性的代码,但是我编码是为了产生结果,而不是代码。
我经常发现自己经常要运行一个简短的脚本来测试一个或多个参数的影响。通常,要事先知道要更改哪些参数并不容易。
假设我有此伪代码:
INPUT_FILE = "data.csv"
N_COMP = 7
MIN_SIZE = 35
MAX_SIZE = 70
OUTPUT_FILE = f"plot_{N_COMP}.pdf"
data = read(INPUT_FILE)
results = process(data,N_COMP)
figure = plot(results,MIN_SIZE,MAX_SIZE)
store(figure,OUTPUT_FILE)
现在,我想尝试各种N_COMP
值的影响。我可以在大多数脚本上添加循环:
INPUT_FILE = "data.csv"
# N_COMP = 7
MIN_SIZE = 35
MAX_SIZE = 70
for N_COMP in (3,5,7,9,11):
OUTPUT_FILE = f"plot_{N_COMP}.pdf"
data = read(INPUT_FILE)
results = process(data,N_COMP)
figure = plot(results,MAX_SIZE)
store(figure,OUTPUT_FILE)
但是,一旦我想遍历几个变量(甚至可能一次),就变得一团糟,甚至每次我缩进时,甚至都没有提到black
对我接近88个字符的行会做些什么再上一层。
我还可以将循环体包装在一个函数中
INPUT_FILE = "data.csv"
MIN_SIZE = 35
MAX_SIZE = 70
def pipeline(N_COMP):
OUTPUT_FILE = f"plot_{N_COMP}.pdf"
data = read(INPUT_FILE)
results = process(data,OUTPUT_FILE)
for N_COMP in (3,11):
pipeline(N_COMP)
但是,缩进问题仍然存在,现在每次想添加一个我可能想要循环的附加参数时,我都必须在三个位置而不是一个位置添加它。最后,参数的定义在代码中的不同位置。 (我可以通过在顶部定义元组并在循环中重用它来使它变成四个而不是三个。)
所以我正在寻找的是这样的解决方案:
from autoloop import looptuple
INPUT_FILE = "data.csv"
N_COMP = looptuple(3,11)
MIN_SIZE = 35
MAX_SIZE = 70
OUTPUT_FILE = f"plot_{N_COMP}.pdf"
data = read(INPUT_FILE)
results = process(data,OUTPUT_FILE)
这应该执行上面的代码示例所做的工作,每个参数更改一行,并且没有其他缩进。我可以使用不同的命令行来调用此脚本,但是那应该是与参数或参数值无关的通用方法。
(怎么可能)?
解决方法
没有这样的内部“自动循环”方法,尤其是当您的代码未包装到函数中并使用全局变量时。
使用环境变量
影响最小的更改可能是对要更改的参数使用环境变量,例如
import os
INPUT_FILE = "data.csv"
N_COMP = int(os.environ.get("N_COMP",7))
MIN_SIZE = 35
MAX_SIZE = 70
OUTPUT_FILE = f"plot_{N_COMP}.pdf"
# ...
然后,您可以使用以下命令运行脚本(假设使用UNIX-y shell)
env N_COMP=16 python my_script.py
并可能实现自动化,例如
for n_comp in 3 5 7 9 11 42; do
env N_COMP=$n_comp python my_script.py
done
使用运行器功能
如果脚本的主体与上面类似,则要在内部对其进行自动化,必须将入口点包装在一个函数中,例如
def run_experiment():
data = read(INPUT_FILE)
results = process(data,N_COMP)
figure = plot(results,MIN_SIZE,MAX_SIZE)
store(figure,OUTPUT_FILE)
之后,您可以添加运行器功能;稍微滥用globals()
,我们可以使它变得更加动态:
import itertools
# Defaults (will be overwritten)
N_COMP = 3
N_KITTENS = 8
def run_experiment():
print("N_COMP * N_KITTENS:",N_COMP * N_KITTENS)
def run_experiments():
experiments = {
"N_COMP": (1,3,5,7),"N_KITTENS": (3,9,42,64),}
keys,values = zip(*experiments.items())
for value_combo in itertools.product(*values):
experiment_values = dict(zip(keys,value_combo))
# You should add dependent values such as OUTPUT_FILE here
print("Running:",experiment_values)
globals().update(experiment_values)
run_experiment()
if __name__ == "__main__":
run_experiments()
,
用很多话来说,任何您想重用的东西都应该模块化。第一步是创建一个函数。 (在更复杂的情况下,您可能需要将其放在单独的文件中,和/或使用几种方法创建一个类。)
您要封装内部结构(无论函数内部是什么),但要公开调用方应控制的参数。具体来说,您的函数应接受任何应由外部变量或常量控制的参数作为参数。
(如果您想创建用于对一组参数进行硬编码的其他包装器函数,那很好,但是很方便。)
INPUT_FILE = "data.csv"
MIN_SIZE = 35
MAX_SIZE = 70
def pipeline(n_comp,min_size,max_size,input_file):
output_file = f"plot_{n_comp}.pdf"
data = read(input_file)
results = process(data,n_comp)
figure = plot(results,max_size)
store(figure,output_file)
for N_COMP in (3,7,11):
pipeline(N_COMP,MAX_SIZE,INPUT_FILE)
(可选)您可以为某些参数声明默认值。
def pipeline(n_comp,input_file,min_size=MIN_SIZE,max_size=MAX_SIZE):
您可以使用pipeline(3,"data.csv")
来调用它,并且该函数将退回到默认值(对于您未提供的参数)。可选参数必须位于最后,因此我不得不在此处重新排序。
这个autoloop.py
很好地满足了我的目的:
"""
Upon import,read and execute initial script for autoloop parameter combinations.
Example code:
```
import autoloop
X = autoloop.looptuple(1,2)
Y = autoloop.looprange(10,12)
print(X,Y)
```
"""
import itertools
import os
import re
import sys
def noloop():
"""Make pylint happy."""
def loopdummy(*args):
"""Return first element in case we are not looping."""
return args[0] if args else None
looptuple = loopdummy
looprange = loopdummy
def find_vars(script,looptype):
"""Parse var = autoloop.looptype(value1,value2,...) lines."""
pattern = fr"(?P<var>[^\W0-9]\w*)\s*=\s*autoloop\.loop{looptype}\((?P<values>.+)\)"
matches = [match for line in script if (match := re.fullmatch(pattern,line))]
variables = [match.group("var") for match in matches]
value_lists = [re.split(r"\s*,\s*",match.group("values")) for match in matches]
if looptype == "range":
value_lists = [range(*map(int,values)) for values in value_lists]
return (variables,value_lists)
def loop():
"""See module docstring."""
if sys.argv[0] == __file__:
return
with open(sys.argv[0]) as script_file:
script = script_file.read().splitlines()
if "autoloop.noloop()" in script:
return
(tuple_vars,tuple_value_lists) = find_vars(script,"tuple")
(range_vars,range_value_lists) = find_vars(script,"range")
variables = tuple_vars + range_vars
value_lists = tuple_value_lists + range_value_lists
# if not variables:
# return
script = [line for line in script if "autoloop" not in line]
for values in itertools.product(*value_lists):
preamble = [f"{var} = {value}" for (var,value) in zip(variables,values)]
print("Exec'ing with",",".join(preamble),"...")
new_script = "\n".join(itertools.chain(preamble,script))
exec(new_script,locals(),locals()) # pylint: disable=exec-used
print("Done.")
os._exit(0) # pylint: disable=protected-access
loop()
要使用它,
- 添加
import autoloop
(尚未执行任何操作)
转换至少一个常数,例如
X = 1
至X = autoloop.looptuple(1,2,3)
要暂停,
- 在代码中的任意位置添加
autoloop.noloop()
(它将使用looptuple
/looprange
中的第一个值)
示例代码显示:
Exec'ing with X = 1,Y = 10 ...
1 10
Exec'ing with X = 1,Y = 11 ...
1 11
Exec'ing with X = 2,Y = 10 ...
2 10
Exec'ing with X = 2,Y = 11 ...
2 11
Done.
这缺少数十亿张支票。当前,它会将变量添加到顶部,而不是在变量的定义位置,因此依赖项将无法正常工作。当您将autoloop
导入到本身已导入的文件中时,它将不起作用。解析最多是最少的。但是对于我的一些小例子来说,它运行良好。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。