是否有可能在列表理解内调用函数而没有调用该函数的开销?

问题描述 投票:3回答:1

在这个简单的示例中,我想将列表理解的i < 5条件分解为它自己的函数。我也想吃蛋糕,也要避免蛋糕CALL_FUNCTION字节码/在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?

python python-3.x python-3.6 bytecode
1个回答
1
投票

将评论中的所有优秀答案合并为一个。

正如georg所说,这听起来像是您正在寻找一种内联函数或表达式的方法,并且在CPython中没有进行过这样的尝试:https://bugs.python.org/issue10399

因此,按照“元编程”的原则,您可以构建lambda的内联和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下运行时会给出:

 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

从安全的角度来看,“评估”是危险的,尽管在这里危险要小一些,因为您可以在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/

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.