使用自定义名称创建Python动态函数

64

如果这个问题已经被提出并回答了,请接受我的道歉。我的需求在概念上非常简单,但不幸的是我无法在网上找到答案。

我需要在 Python(Python 2.7)中在运行时使用自定义名称创建动态函数。每个函数的主体也需要在运行时构建,但对于所有函数来说它几乎是相同的。

我从一个名称列表开始。

func_names = ["func1", "func2", "func3"]
注意,func_name列表可以容纳任意名称的列表,因此名称不会简单地是func1、func2、func3等等。
我希望结果是:
    def func1(*args):
        ...

    def func2(*args):
        ...

    def func3(*args):
        ...
我需要这样做的原因是,每个函数名对应一个测试用例,然后从外部调用该测试用例。

更新: 没有用户输入。我正在连接一个更大的模块的两端。一端确定测试用例是什么,除其他事项外,还填充了测试用例名称的列表。另一端是函数本身,它们必须与测试用例的名称具有1:1映射关系。所以我知道测试用例的名称,也知道要对每个测试用例执行什么操作,我只需要创建带有测试用例名称的函数即可。由于测试用例的名称是在运行时确定的,因此基于这些测试用例创建函数也必须在运行时进行。

更新: 如果有必要,我还可以将这些自定义命名函数包装在一个类中,以使事情变得更加容易。

我可以在字符串中硬编码函数的内容(因为它们几乎相同),也可以基于之前定义的基类来创建函数内容。只需要知道如何使用这些内容填充函数即可。例如:
    func_content = """
                   for arg in args:
                       print arg
                   """

谢谢您提前考虑,

Mahdi

7个回答

65

根据你所描述的,我认为你不需要使用eval或宏——通过闭包创建函数实例应该能够正常工作。例如:

def bindFunction1(name):
    def func1(*args):
        for arg in args:
            print arg
        return 42 # ...
    func1.__name__ = name
    return func1

def bindFunction2(name):
    def func2(*args):
        for arg in args:
            print arg
        return 2142 # ...
    func2.__name__ = name
    return func2

但是,你可能希望将这些函数按名称添加到某个作用域中,以便可以通过名称访问它们。

>>> print bindFunction1('neat')
<function neat at 0x00000000629099E8>
>>> print bindFunction2('keen')
<function keen at 0x0000000072C93DD8>

1
这个解决方案有点巧妙。这样做不会覆盖其他输出,其中函数名可以找到。例如,__qualname__ 仍将保留原始名称。 - Fusion
这个解决方案有点hacky。 这样做不会覆盖其他输出,其中函数名称可以找到。 例如__qualname__仍将保留原始名称。 - Fusion

22

在 Shane 的回答基础上进行扩展,因为当我在寻找类似问题的解决方案时,我刚刚发现了这个问题。注意变量的作用域范围。您可以通过使用生成器函数来定义作用域来避免作用域问题。以下是一个在类上定义方法的示例:

class A(object):
    pass

def make_method(name):
    def _method(self):
        print("method {0} in {1}".format(name, self))
    return _method

for name in ('one', 'two', 'three'):
    _method = make_method(name)
    setattr(A, name, _method)

使用中:

In [4]: o = A()

In [5]: o.one()
method one in <__main__.A object at 0x1c0ac90>

In [6]: o1 = A()

In [7]: o1.one()
method one in <__main__.A object at 0x1c0ad10>

In [8]: o.two()
method two in <__main__.A object at 0x1c0ac90>

In [9]: o1.two()
method two in <__main__.A object at 0x1c0ad10>

如何将此内容添加到类__init__中?而不是在类外面。 - Nairum
1
@AlexeiMarinichenko 把它添加到__init__中会使其成为实例特定的,这不是一个好主意。如果您需要实例特定的行为,我建议专门化__getattr__或使用类中定义的某些不同协议。除非您真的需要它,否则请避免使用这种奇怪的东西,如果您确实需要它,请额外小心,尽可能使其清晰明了,否则它将回来咬你。 - Paul Whipp
1
@AlexeiMarinichenko 话虽如此,将方法添加到类规范之后可能会显得有些“凌乱”,如果这是你所担心的问题,我建议将该类保留在其自己的模块中。如果你在应用程序中要为多个类执行此操作,我建议使用元类来专门化类实例化,以便将动态方法规范移动到类变量中。 - Paul Whipp

10

可能需要进行某种内省来完成这种操作,但我认为这不是解决问题的Pythonic方法。

我认为你应该利用Python中函数作为一级公民的特性。像Shane Holloway所说的那样使用闭包,根据需要自定义函数内容。然后对于动态名称绑定,使用一个字典,其键是动态定义的名称,值将是函数本身。

def function_builder(args):
    def function(more_args):
       #do stuff based on the values of args
    return function

my_dynamic_functions = {}
my_dynamic_functions[dynamic_name] = function_builder(some_dynamic_args)

#then use it somewhere else
my_dynamic_functions[dynamic_name](the_args)

希望这对您的使用情况有意义。


4
要真正动态创建函数,您可以使用我专门开发的makefun。它支持三种定义生成签名的方法:
  • 字符串表示形式,例如'foo(a, b=1)'
  • Signature对象,手工制作或使用inspect.signature从另一个函数派生
  • 引用函数。在这种情况下,公开的签名将与该函数的签名相同。
此外,您可以告诉它将所创建函数的引用作为第一个参数注入到您的实现中,以便根据调用来源(外观)进行较小的行为修改。例如:
# generic core implementation
def generic_impl(f, *args, **kwargs):
    print("This is generic impl called by %s" % f.__name__)
    # here you could use f.__name__ in a if statement to determine what to do
    if f.__name__ == "func1":
        print("called from func1 !")
    return args, kwargs

my_module = getmodule(generic_impl)

# generate 3 facade functions with various signatures
for f_name, f_params in [("func1", "b, *, a"),
                         ("func2", "*args, **kwargs"),
                         ("func3", "c, *, a, d=None")]:
    # the signature to generate
    f_sig = "%s(%s)" % (f_name, f_params)

    # create the function dynamically
    f = create_function(f_sig, generic_impl, inject_as_first_arg=True)

    # assign the symbol somewhere (local context, module...)
    setattr(my_module, f_name, f)

# grab each function and use it
func1 = getattr(my_module, 'func1')
assert func1(25, a=12) == ((), dict(b=25, a=12))

func2 = getattr(my_module, 'func2')
assert func2(25, a=12) == ((25,), dict(a=12))

func3 = getattr(my_module, 'func3')
assert func3(25, a=12) == ((), dict(c=25, a=12, d=None))

正如您在文档中所看到的那样,除非绝对不可能(例如在func2中的var-positional签名),否则参数始终会被重定向到kwargs

3
你可能希望使用eval;你需要构建包含每个函数Python定义的字符串(即以def func1开头的多行字符串),然后使用eval执行它。
但我建议为每个这样的函数生成一个唯一的名称(例如genfun345)。不要使用未经检查的用户输入来命名。因为如果名称与Python中的某些已知名称相同,你将陷入难以调试的灾难之中。
你信任制作这些函数的输入吗?你关心恶意软件或滥用吗?
阅读维基百科上的卫生宏

没有用户输入。我正在连接一个更大的模块的两个端点。一个端点确定测试用例是什么,除其他事项外,还填充了测试用例名称列表。另一个端点是函数本身,它们必须与测试用例的名称一一对应。所以我有测试用例的名称,我知道我想针对每个测试用例做什么,我只需要创建具有测试用例名称的函数。由于测试用例的名称在运行时确定,因此基于这些测试用例的函数创建也必须在运行时进行。 - mahdiolfat

1
如果我正确理解您的要求,那么您似乎只是想为现有函数动态分配新的或替代名称 - 在这种情况下,以下内容应该能够完成工作:
import sys

testcases = []
def testcase(f):
    """ testcase function decorator """
    testcases.append(f)
    return f

@testcase
def testcase0(*args):
    print 'testcase0 called, args:', args

@testcase
def testcase1(*args):
    print 'testcase1 called, args:', args

@testcase
def testcase2(*args):
    print 'testcase2 called, args:', args

def assign_function_names(func_names, namespace=None):
    if namespace is None:
        namespace = sys._getframe(1).f_globals  # default to caller's globals
    for name, func in zip(func_names, testcases):
        func.__name__ = name  # optional
        namespace[name] = func

assign_function_names(["funcA", "funcB", "funcC"])

funcA(1, 2, 3)
funcB(4, 5)
funcC(42)

感谢您的回复。但情况并非如此。它们不是现有的函数,而且要动态创建的函数数量也是未知的(只在运行时知道)。 - mahdiolfat
如果你可以“硬编码函数的内容”,那么你不妨在.py文件中使用def xxx(yyy):来预先定义该函数,并将其变成一个已存在的函数--你认为通过将其放入字符串并动态创建函数,你会获得什么好处呢? - martineau
我认为你误解了我在这里尝试做什么,并没有真正回答我的问题,而是一直告诉我去做其他事情。无论如何,还是谢谢。我已经解决了我的问题。 - mahdiolfat
@mahdiolfat:显然我不是唯一一个没有理解的人。你没有回答我的问题,即为什么在预先知道函数内容的情况下需要动态创建函数。动态更改它们的名称并使它们成为方法是一个更易于理解的需求,并且相对容易实现。 - martineau
我的问题是如何创建动态函数,而不是为什么需要创建动态函数。如果你知道如何做,请告诉我。如果不知道,请不要浪费时间。 - mahdiolfat
@mahdiolfat:根据你所写的内容,不清楚你需要做什么,也没有提供太多动力来帮助你完善或改进看似不必要的东西——因此,更详细地解释为什么你认为需要这样做可能是值得的。 - martineau

1

由于没有人给出一个合适的答案,我来补充一个(适用于Python 3)。

dynamic_func.py

from copy import deepcopy

__all__ = []  # For safer * imports
FUNC_NAMES = ["func1", "func2", "func3"]


def generate_func(func_name):
    def __dynamic_func():
        return func_name  # This is for testing
    return __dynamic_func

# Loop through, construct function body and add 
# it to the global scope of the module.
for __func_name in FUNC_NAMES:
    __func = generate_func(__func_name)
    globals()[__func_name] = deepcopy(__func)
    __all__.append(__func_name)


# Clean up
del __func_name
del __func
__all__ = tuple(__all__)

以下是一个测试。

test_dynamic_func.py

from unittest import TestCase
from dynamic_func import func1, func2, func3


class DynamicFuncTestCase(TestCase):
    def test_dynamic_func(self):
        self.assertEqual(func1(), "func1")
        self.assertEqual(func2(), "func2")
        self.assertEqual(func3(), "func3")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接