是否可以从列表推导中调用函数而无需调用函数呢? [英] Is it possible to call a function from within a list comprehension without the overhead of calling the function?
问题描述
在这个简单的示例中,我想排除 i< 5
列表理解其自身功能的条件。我也想吃蛋糕,也要避免蛋糕 CALL_FUNCTION
字节码/在python虚拟机中创建新框架的开销。
In this trivial example, I want to factor out the i < 5
condition of a list comprehension into it's own function. I also want to eat my cake and have it too, and avoid the overhead of the CALL_FUNCTION
bytecode/creating a new frame in the python virtual machine.
有什么方法可以将列表理解内的条件分解为新函数,但以某种方式获得反汇编结果,从而避免 CALL_FUNCTION
?
Is there any way to factor out the conditions inside of a list comprehension into a new function but somehow get a disassembled result that avoids the large overhead of CALL_FUNCTION
?
import dis
import sys
import timeit
def my_filter(n):
return n < 5
def a():
# list comprehension with function call
return [i for i in range(10) if my_filter(i)]
def b():
# list comprehension without function call
return [i for i in range(10) if i < 5]
assert a() == b()
>>> sys.version_info[:]
(3, 6, 5, 'final', 0)
>>> timeit.timeit(a)
1.2616060493517098
>>> timeit.timeit(b)
0.685117881097812
>>> dis.dis(a)
3 0 LOAD_CONST 1 (<code object <listcomp> at 0x0000020F4890B660, file "<stdin>", line 3>)
# ...
>>> dis.dis(b)
3 0 LOAD_CONST 1 (<code object <listcomp> at 0x0000020F48A42270, file "<stdin>", line 3>)
# ...
# list comprehension with function call
# big overhead with that CALL_FUNCTION at address 12
>>> dis.dis(a.__code__.co_consts[1])
3 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 16 (to 22)
6 STORE_FAST 1 (i)
8 LOAD_GLOBAL 0 (my_filter)
10 LOAD_FAST 1 (i)
12 CALL_FUNCTION 1
14 POP_JUMP_IF_FALSE 4
16 LOAD_FAST 1 (i)
18 LIST_APPEND 2
20 JUMP_ABSOLUTE 4
>> 22 RETURN_VALUE
# list comprehension without function call
>>> dis.dis(b.__code__.co_consts[1])
3 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 16 (to 22)
6 STORE_FAST 1 (i)
8 LOAD_FAST 1 (i)
10 LOAD_CONST 0 (5)
12 COMPARE_OP 0 (<)
14 POP_JUMP_IF_FALSE 4
16 LOAD_FAST 1 (i)
18 LIST_APPEND 2
20 JUMP_ABSOLUTE 4
>> 22 RETURN_VALUE
我愿意采用一种我从未在生产中使用过的hacky解决方案
I'm willing to take a hacky solution that I would never use in production, like somehow replacing the bytecode at run time.
换句话说,是否可以替换 a
的地址8, 10和12,而 b
分别是8、10和12?
In other words, is it possible to replace a
's addresses 8, 10, and 12 with b
's 8, 10, and 12 at runtime?
推荐答案
将注释中的所有出色答案合并为一个。
Consolidating all of the excellent answers in the comments into one.
正如georg所说,这听起来像是您正在寻找一种内联函数或表达式的方法,而在CPython中却没有这样的尝试:< a href = https://bugs.python.org/issue10399 rel = nofollow noreferrer> https://bugs.python.org/issue10399
As georg says, this sounds like you are looking for a way to inline a function or an expression, and there is no such thing in CPython attempts have been made: https://bugs.python.org/issue10399
因此,按照元编程的原则,您可以构建lambda的内联和eval:
Therefore, along the lines of "metaprogramming", you can build the lambda's inline and eval:
from typing import Callable
import dis
def b():
# list comprehension without function call
return [i for i in range(10) if i < 5]
def gen_list_comprehension(expr: str) -> Callable:
return eval(f"lambda: [i for i in range(10) if {expr}]")
a = gen_list_comprehension("i < 5")
dis.dis(a.__code__.co_consts[1])
print("=" * 10)
dis.dis(b.__code__.co_consts[1])
在3.7.6下运行时给出:
which when run under 3.7.6 gives:
6 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 16 (to 22)
6 STORE_FAST 1 (i)
8 LOAD_FAST 1 (i)
10 LOAD_CONST 0 (5)
12 COMPARE_OP 0 (<)
14 POP_JUMP_IF_FALSE 4
16 LOAD_FAST 1 (i)
18 LIST_APPEND 2
20 JUMP_ABSOLUTE 4
>> 22 RETURN_VALUE
==========
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 16 (to 22)
6 STORE_FAST 1 (i)
8 LOAD_FAST 1 (i)
10 LOAD_CONST 0 (5)
12 COMPARE_OP 0 (<)
14 POP_JUMP_IF_FALSE 4
16 LOAD_FAST 1 (i)
18 LIST_APPEND 2
20 JUMP_ABSOLUTE 4
>> 22 RETURN_VALUE
从安全角度来看, eval是危险的,尽管在此情况下它不是那么危险,因为您可以在lambda里面做。而且在IfExp表达式中可以执行的操作更加受限制,但是仍然很危险,例如调用执行恶意操作的函数。
From a security standpoint "eval" is dangerous, athough here it is less so because what you can do inside a lambda. And what can be done in an IfExp expression is even more limited, but still dangerous like call a function that does evil things.
但是,如果您希望获得更安全的效果,则可以使用AST来代替字符串。我发现麻烦很多。
However, if you want the same effect that is more secure, instead of working with strings you can modify AST's. I find that a lot more cumbersome though.
一种混合方法是调用 ast.parse()
,检查结果。例如:
A hybrid approach would be the call ast.parse()
and check the result. For example:
import ast
def is_cond_str(s: str) -> bool:
try:
mod_ast = ast.parse(s)
expr_ast = isinstance(mod_ast.body[0])
if not isinstance(expr_ast, ast.Expr):
return False
compare_ast = expr_ast.value
if not isinstance(compare_ast, ast.Compare):
return False
return True
except:
return False
这是更安全的方法,但在这种情况下仍可能存在恶意功能这样您就可以继续前进。再次,我觉得这有点乏味。
This is a little more secure, but there still may be evil functions in the condition so you could keep going. Again, I find this a little tedious.
从字节码开始的另一个方向来看,有一个我的跨版本汇编器。参见 https://pypi.org/project/xasm/
Coming from the other direction of starting off with bytecode, there is my cross-version assembler; see https://pypi.org/project/xasm/
这篇关于是否可以从列表推导中调用函数而无需调用函数呢?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!