是否可以从列表推导中调用函数而无需调用函数呢?

时间:2019-08-25 17:00:16

标签: python python-3.x python-3.6 bytecode

在这个简单的示例中,我想将列表理解的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?

1 个答案:

答案 0 :(得分: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

从安全角度来看,“ 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/