是否可以从列表理解中调用函数而无需调用函数的开销?
Is it possible to call a function from within a list comprehension without the overhead of calling the function?
在这个简单的示例中,我想将列表理解的 i < 5
条件分解到它自己的函数中。我也想吃我的蛋糕,也有它,避免 CALL_FUNCTION
bytecode/creating 在 python 虚拟机中的新框架的开销。
是否有任何方法可以将列表理解内的条件分解为新函数,但以某种方式获得反汇编结果,避免 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 解决方案,比如在 运行 时间以某种方式替换字节码。
换句话说,是否可以在运行时将a
的地址8、10、12替换为b
的8、10、12?
将评论中的所有优秀答案合并为一个。
正如 georg 所说,这听起来像是您正在寻找一种内联函数或表达式的方法,而在 CPython 中没有这样的尝试:https://bugs.python.org/issue10399
因此,按照 "metaprogramming",您可以构建 lambda 的内联和评估:
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 下给出:
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 表达式中可以做的事情更加有限,但仍然很危险,比如调用一个做坏事的函数。
但是,如果您想要更安全的相同效果,您可以修改 AST 而不是使用字符串。不过我觉得麻烦多了。
一种混合方法是调用 ast.parse()
并检查结果。例如:
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
这样更安全一些,但条件中仍然可能存在恶意函数,因此您可以继续进行。同样,我觉得这有点乏味。
从字节码开始的另一个方向,有我的跨版本汇编器;参见 https://pypi.org/project/xasm/
在这个简单的示例中,我想将列表理解的 i < 5
条件分解到它自己的函数中。我也想吃我的蛋糕,也有它,避免 CALL_FUNCTION
bytecode/creating 在 python 虚拟机中的新框架的开销。
是否有任何方法可以将列表理解内的条件分解为新函数,但以某种方式获得反汇编结果,避免 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 解决方案,比如在 运行 时间以某种方式替换字节码。
换句话说,是否可以在运行时将a
的地址8、10、12替换为b
的8、10、12?
将评论中的所有优秀答案合并为一个。
正如 georg 所说,这听起来像是您正在寻找一种内联函数或表达式的方法,而在 CPython 中没有这样的尝试:https://bugs.python.org/issue10399
因此,按照 "metaprogramming",您可以构建 lambda 的内联和评估:
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 下给出:
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 表达式中可以做的事情更加有限,但仍然很危险,比如调用一个做坏事的函数。
但是,如果您想要更安全的相同效果,您可以修改 AST 而不是使用字符串。不过我觉得麻烦多了。
一种混合方法是调用 ast.parse()
并检查结果。例如:
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
这样更安全一些,但条件中仍然可能存在恶意函数,因此您可以继续进行。同样,我觉得这有点乏味。
从字节码开始的另一个方向,有我的跨版本汇编器;参见 https://pypi.org/project/xasm/