是否可以从列表推导中调用函数而无需调用函数呢? [英] Is it possible to call a function from within a list comprehension without the overhead of calling the function?

查看:122
本文介绍了是否可以从列表推导中调用函数而无需调用函数呢?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在这个简单的示例中,我想排除 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆