是否可以从列表理解中调用函数而无需调用函数的开销?

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/