您是否知道如何从模块化模板动态创建函数,模板代码是可读的,在一个地方收集,结果函数代码只包含所需内容并在回溯中正确显示?
在模拟框架的上下文中,我想动态创建一个在运行时期间经常调用的函数(比如说超过一百万次).该函数实现了一个要评估的数学表达式,并且该函数的多个实例可能存在,并且实际数学和函数代码都有变化.一个例子是表达a + b*c,与变型a和a + b,但也可能a**c相反.实际的等式更复杂,可能有更多的破坏性变化.
在初始化模拟时定义不同的函数实例,并且在每个时间步骤中调用所有这些函数实例.因此,我希望在运行时最小化每个函数中执行的代码,而不是携带不必要的行李.同时,我希望在一个模板中包含所有变体,而不是多次重复相同的代码并略有变化.如果我对代码进行更改,我不想检查每个重复项.
让我们假设为了简单起见,它们a, b, c在某些外部范围中定义,不需要显式传递.
1:始终评估最大等式
def full_equation():
return a + b*c
Run Code Online (Sandbox Code Playgroud)
使用这个解决方案,我必须在每个时间步骤中查找a,b和c并计算求和和乘积,即使根本不需要b和c(即分别设置为0和1).这是我想避免的额外计算.此外,这种方式a**c不包括替代方程,需要在不同的功能中实现.
2:明确地实现每个可能的变体
def variant_1():
return a + b*c
def variant_2():
return a + b
def variant_3():
return a + b*c
def variant_4():
return a**c
Run Code Online (Sandbox Code Playgroud)
接下来,我将实现一个选择器函数,该函数检查在哪些条件下需要使用哪个版本的函数.此解决方案最大限度地减少了运行时的计算工作量,但是对于更复杂的表达式和变量以及它们之间的条件依赖性,会大大增加代码库.如果我想对核心表达式进行微小的更改,我必须追踪每个变体并单独检查 - 这可能很容易出错.这就是为什么我想避免这个解决方案.
3:在运行时检查所有条件
def function_with_lots_of_ifs(cond_a, cond_b, cond_c):
if condition_a:
return a
else:
if condition_b:
return a + b
elif condition_c:
return a + b*c
else:
return a**c
Run Code Online (Sandbox Code Playgroud)
该解决方案在计算上效率低,因为需要在每个时间步骤中检查所有条件.我想避免if在初始化之外的模拟运行时中的任何内容.
我现在使用的是字符串执行:
def template_builder(cond_a, cond_b, cond_c):
second_part = ""
sum_snippet = ""
product_snippet = ""
if not cond_a:
if cond_b:
sum_snippet = " + b"
if cond_c:
product_snippet = "*c"
second_part = f"{sum_snippet}{product_snippet}"
else:
second_part = "**c"
template = f"""
def run_func():
a{second_part}"""
return template
print(template_builder(False, True, False))
Run Code Online (Sandbox Code Playgroud)
返回'\ndef run_func():\n a + b',可以使用它exec来定义函数run_func.到目前为止,所有代码都在一个地方,结果函数只包含必要的代码.代码可能会重新排列一点以提高可读性,但此解决方案的主要问题是调试它,例如:
a = "s"
b = 2
c = 3
run_func()
Run Code Online (Sandbox Code Playgroud)
回报
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-68-6a3db6ea9fbb> in <module>()
1 a = "s"
----> 2 run_func()
<string> in run_func()
TypeError: must be str, not int
Run Code Online (Sandbox Code Playgroud)
我可以看到一些string与int它不应该碰撞的地方相撞而且它发生在我的内部run_func.但我不知道函数的哪个变体导致问题以及错误发生的位置(再次,想象代码可能要复杂得多).有没有人有一个建议,如何正确地显示回溯中的代码,正如您在前三个解决方案中所期望的那样 - 没有各自的问题?另外,我在给一个评论已经阅读这个答案是
任何时候你认为"我可以使用执行官..."你几乎肯定做错了.
我愿意以不同的方式提出建议.我考虑过装饰器,但看不出解决问题的方法.另请注意,嵌套函数调用在计算上效率低下.
\n\n\n任何时候你认为“我可以使用 exec\xe2\x80\xa6”,你几乎肯定做错了。
\n
几乎可以确定。字符串元编程是适合使用evalor 的情况之一exec。即使标准库也这样做。(参见namedtuple实现。)
但是还有多种其他方法可以在 Python 中进行元编程。考虑到您关心的问题(性能、调试),您将需要使用ast 模块。
\n\n使用ast比使用字符串进行元编程更困难。您必须弄清楚许多附带的复杂性。所以我建议使用一个将其抽象化的库。
我所知道的最好的基于 ast 的 Python 元编程库之一是Hy。使用 Hy 宏,您可以使用基于s 表达式的相当简单的语法在编译时构建任意函数相当简单的语法在编译时构建任意函数,该语法非常自然地映射到抽象语法树。
\n\n这是使用 Hy 宏的示例。
\n\n=> (defmacro template-builder [func-name &rest args]\n... `(defn ~func-name[]\n... ~(.format "generated func named {}" func-name)\n... (-> a ~@args)))\nfrom hy import HyExpression, HyList, HySymbol\nimport hy\nhy.macros.macro(\'template-builder\')(lambda hyx_XampersandXname, func_name,\n *args: HyExpression([] + [HySymbol(\'defn\')] + [func_name] + [HyList([])\n ] + [\'generated func named {}\'.format(func_name)] + [HyExpression([] +\n [HySymbol(\'->\')] + [HySymbol(\'a\')] + list(args or []))]))\n\n<function <lambda> at 0x00000245B90B5400>\n=> (template-builder foo)\ndef foo():\n """generated func named foo"""\n return a\n\n\nNone\n\n=> (template-builder bar (+ b))\ndef bar():\n """generated func named bar"""\n return a + b\n\n\nNone\n\n=> (template-builder baz (+ b) (* c))\ndef baz():\n """generated func named baz"""\n return (a + b) * c\n\n\nNone\n\n=> (template-builder quux (+ b) (** c))\ndef quux():\n """generated func named quux"""\n return (a + b) ** c\n\n\nNone\n\n=>\nRun Code Online (Sandbox Code Playgroud)\n\n\n\n\n非常感谢您的回答!我浏览了一些文档并尝试了您的示例。看起来很有趣。我是否理解正确,Hy 应该与它自己的基于 lisp 的语法一起使用?
\n
是的,虽然这一切最终都会转换为 Python (ast),但它并不总是那么漂亮。您可以(原则上)使用 Python 语言中的 Hy\ 模型来实现宏,而根本不需要编写 s 表达式,但我并不推荐这样做。$ hy2py foo.hy将向您展示 Python 翻译,并将$ hy --spy以交互方式进行。
此外,只有ast-manipulation部分需要用Hy编写。Hy 编译为 Python ast,然后 CPython 将其编译为自己的字节码,因此它与 Python 具有透明的互操作性。您可以从 Python 代码导入和使用用 Hy 编写的模块,就像任何其他 Python 模块一样。最终用户甚至不需要知道它是用 Hy 编写的。
\n\n\n\n\n不管怎样,在我看来,用 Hy 生成代码本身就不太可读(在 Python 意义上)。这个观察正确吗?
\n
Lisp 与 Python 不同。与任何语言一样,您必须习惯它,并且可能会编写清晰或模糊的代码。
\n\n\n\n\n如果是的话,维护生成的库将变得更加困难。
\n
并非出于您可能想到的原因。最终用户可以通过 ast 模块访问 Python 的 ast,但它被认为是一个实现细节,可能会随着每个 Python 版本的变化而变化。如果您自己进行 ast 操作,则必须跟上这一点。这是仅使用的一个主要优点exec这是仅使用字符串
另一方面,Hy 同时保证 Hy 支持的所有 Python 版本的 ast 兼容性。但 Hy 本身一直在经历变化,还没有完全稳定的 API。如果您升级 Hy 版本(这可能是跟上 Python 所必需的),您可能还需要调整您的 Hy 代码。这可能仍然比自己编写 ast 操作更容易。
\n\n\n\n\n由于我在科学背景下工作,语法的简单性/可读性是一项相当重要的壮举。
\n
Lisp 的语法实际上比 Python 简单得多。这就是为什么用它来编写宏更容易。没有语句/表达式的区别。无运算符优先级。没有需要跟踪的缩进级别。一切都是一个广义的函数调用,只需一点语法糖即可扩展为这些(例如\'foo扩展为(quote foo),如果更容易的话,您可以在宏定义中使用后者。还有 quasiquote `,它允许取消引用~和拼接取消~@里面的引号,这是制作宏模板的简单方法。)
S 表达式绝对必须正确缩进以便人类可读 - 这样即使参数嵌套很深,您也知道哪些参数与哪个函数对应。(玩parinfer直到你掌握它。)剩下的只是基本熟悉。
\n\n您已经可以阅读 Python 的函数调用语法spam(eggs, ham, bacon)。在 Lisp 中,你可以去掉逗号(语法很简单,空格就足够了)spam(eggs ham bacon),然后将左括号向前移动一步(spam eggs ham bacon)。基本上就是这样。更简单,不是吗?
Hy 比大多数其他 Lisp 添加了更多的糖分,就像 Clojure 一样,对于其他数据结构类型([1 2 3]对于字典列表){"a" 1 "b" 2},使用与 Python 相同的括号类型。和#{1 2 3}为套装。
首先评估其参数的实际函数/方法与可能不评估其参数的宏/特殊形式之间存在语义区别。但它们的编写方式都是相同的,就像函数调用一样。剩下的只是词汇,与导入任何其他库相同。
\n\n话虽如此,Hy 并不是唯一的ast 操作库。尝试在 PyPI 中搜索“ast”来查找其他内容。
\n| 归档时间: |
|
| 查看次数: |
101 次 |
| 最近记录: |