代码之家  ›  专栏  ›  技术社区  ›  Jon-Eric

按程序创建python函数(特别是参数)

  •  1
  • Jon-Eric  · 技术社区  · 14 年前

    问题

    如何在python中按程序创建一个函数,该函数接受特定的命名参数,但允许这些参数名称是数据驱动的?

    例子

    比如,你想创建一个类装饰器, with_init ,这将添加 __init__ 具有特定命名参数的方法,以便以下两个类是等效的。

    class C1(object):
        def __init__(self, x, y, z):
            self.x = x
            self.y = y
            self.z = z
    
    @with_init('x y z')
    class C2(object):
        pass
    

    我的第一次尝试是做一个接受 *args 而不是特定的命名参数:

    class with_init(object):
        def __init__(self, params):
            self.params = params.split()
    
        def __call__(self, cls):
            def init(cls_self, *args):
                for param, value in zip(self.params, args):
                    setattr(cls_self, param, value)
            cls.__init__ = init
            return cls
    

    它在某些情况下工作:

    >>> C1(1,2,3)
    <__main__.C1 object at 0x100c410>
    >>> C2(1,2,3)
    <__main__.C2 object at 0x100ca70>
    

    但在其他人身上就没那么多了:

    >>> C2(1,2,3,4) # Should fail, but doesn't.
    <__main__.C2 object at 0x100cc90>
    
    >>> C2(x=1, y=2, z=3) # Should succeed, but doesn't.
    Traceback (most recent call last):
      File "<string>", line 1, in <fragment>
    TypeError: init() got an unexpected keyword argument 'y'
    

    当然,我可以将代码添加到嵌套 init 函数尝试检查每个可能的情况,但似乎应该有一个更简单的方法。

    我注意到了 collections.namedtuple 通过将字符串传递给 exec . 在我看来,这似乎很圆,但也许这就是解决办法。

    什么是正确的执行 with_init.__call__ ?

    注意:我想要一个Python2.x解决方案。

    2 回复  |  直到 14 年前
        1
  •  3
  •   John La Rooy    14 年前

    非常粗略。它接受kw args并检查args的数量是否正确

    def __call__(self, cls):
        def init(cls_self, *args, **kw):
            if len(args)+len(kw) != len(self.params):
                raise RuntimeError("Wrong number of arguments")
            for param, value in zip(self.params, args):
                setattr(cls_self, param, value)
            vars(cls_self).update(kw)
        cls.__init__ = init
        return cls
    

    这个版本有一些改进

    def __call__(self, cls):
        def init(cls_self, *args, **kw):
            for param, value in zip(self.params, args):
                if param in kw:
                    raise TypeError("Multiple values for %s"%param)
                kw[param]=value
            if len(args) > len(self.params) or set(kw) != set(self.params):
                raise TypeError("Wrong number of arguments")
            vars(cls_self).update(kw)
        cls.__init__ = init
        return cls
    

    此版本还告诉您意外的关键字参数

    def __call__(self, cls):
        def init(cls_self, *args, **kw):
            for param, value in zip(self.params, args):
                if param in kw:
                    raise TypeError("Multiple values for %s"%param)
                kw[param]=value
            unexpected_args = list(set(kw)-set(self.params))
            if unexpected_args:
                raise TypeError("Unexpected args %s"%unexpected_args)
            missing_args = list(set(self.params)-set(kw))
            if missing_args:
                raise TypeError("Expected args %s"%missing_args)
            vars(cls_self).update(kw)
        cls.__init__ = init
        return cls
    
        2
  •  0
  •   Jon-Eric    14 年前

    这是我的名字和灵感的答案。它目前对模板注入攻击开放,但您不必处理 任何 参数的错误。

    def __call__(self, cls):
        paramtxt = ', '.join(['self'] + self.params)
        bodytxt =  '\n\t'.join('self.%(param)s = %(param)s' % locals() for param in self.params)
        template = 'def __init__(%(paramtxt)s):\n\t%(bodytxt)s' % locals()
    
        namespace = dict()
        exec template in namespace
    
        cls.__init__ = namespace['__init__']
    
        return cls