2014-11-04 56 views
7

让我先承认,我想要做的事情可能被认为是愚蠢到邪恶,但我想知道我是否可以用Python来做到这一点。装饰器如何将变量传递给函数而不更改其签名?

比方说,我有一个函数装饰器,它接受定义变量的关键字参数,并且我想在包装函数中访问这些变量。我可能会做这样的事情:

def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      return f(extras, *args, **kwargs) 
     return wrapped 
    return wrapper 

现在我可以这样做:

@more_vars(a='hello', b='world') 
def test(deco_vars, x, y): 
    print(deco_vars['a'], deco_vars['b']) 
    print(x, y) 

test(1, 2) 
# Output: 
# hello world 
# 1 2 

我不喜欢这个的事情是,当你使用这个装饰,你必须改变函数的调用签名,除了修饰装饰器外还增加额外的变量。另外,如果你看一下帮助功能,你看,你不希望在调用函数时使用一个额外的变量:

help(test) 
# Output: 
# Help on function test in module __main__: 
# 
# test(deco_vars, x, y) 

这使它看起来像预期用户调用的函数有3个参数,但显然这是行不通的。所以你还必须向docstring添加一条消息,指出第一个参数不是接口的一部分,它只是一个实现细节,应该被忽略。虽然这很糟糕。有没有办法做到这一点,而不必将这些变量挂在全局范围内的某些东西上?理想情况下,我希望它看起来像下面这样:

@more_vars(a='hello', b='world') 
def test(x, y): 
    print(a, b) 
    print(x, y) 

test(1, 2) 
# Output: 
# hello world 
# 1 2 
help(test) 
# Output: 
# Help on function test in module __main__: 
# 
# test(x, y) 

我满足于仅存在Python 3解决方案。

+1

也许你应该描述你的需要更多。你只会得到一个装饰函数的机会,那么把'a ='hello''放在装饰器中有什么好处,而不是把它作为函数的第一行呢? – 2014-11-04 22:50:05

+0

听起来好像你在问题已经被编译后如何在''test''中注入'extras',而不必以任何方式修改'test'的定义。如果是这样,我认为没有一些可怕的黑客攻击是不可能的。 – abarnert 2014-11-04 23:03:19

+0

但使用[MacroPy](https://github.com/lihaoyi/macropy)宏而不是装饰器,我猜这会很容易...这是一个可以接受的答案吗? – abarnert 2014-11-04 23:03:51

回答

0

我不喜欢将局部注入到命名空间的想法。虽然这可能是可能的,但处理新注入的本地人与已经存在于该函数中的名称之间的冲突将是令人讨厌的。

将它们作为函数对象的attrs存储是否可以接受?这样他们是命名空间。

from functools import wraps 

def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      return f(*args, **kwargs) 
     for k,v in extras.items(): 
      setattr(wrapped, k, v) 
     return wrapped 
    return wrapper 

@more_vars(a='hello', b='world') 
def test(x, y): 
    print(test.a, test.b) 
    print(x, y) 
+0

我首先考虑将它们存储为函数attrs,但是我的问题是它使得函数定义不再以某种方式“自包含”。如果复制并粘贴测试函数定义,将其重命名为“test2”,则还必须确保在定义中重命名“test”的所有实例。这可能会导致复制粘贴错误,我宁愿避免。 – user108471 2014-11-05 01:52:12

+0

这很容易解决..函数名可以通过inspect.currentframe(),f_code.co_name或inspect.stack动态获得,那么你可以得到一个函数对象的句柄,而不需要对函数名进行硬编码。 – wim 2014-11-05 11:32:13

+0

这不是一个坏主意,除了它仍然将该样板代码添加到每个想要使用装饰器的函数。如果所有相关的伪装都可以由装饰器自己处理,那么装饰器的用户在添加装饰器之后根本不需要改变函数。 – user108471 2014-11-05 14:33:47

2

你可以使用一些诡计是插入传递给装饰变量到函数的局部变量做到这一点:

import sys 
from functools import wraps 
from types import FunctionType 


def is_python3(): 
    return sys.version_info >= (3, 0) 


def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      fn_globals = {} 
      fn_globals.update(globals()) 
      fn_globals.update(extras) 
      if is_python3(): 
       func_code = '__code__' 
      else: 
       func_code = 'func_code' 
      call_fn = FunctionType(getattr(f, func_code), fn_globals) 
      return call_fn(*args, **kwargs) 
     return wrapped 
    return wrapper 


@more_vars(a="hello", b="world") 
def test(x, y): 
    print("locals: {}".format(locals())) 
    print("x: {}".format(x)) 
    print("y: {}".format(y)) 
    print("a: {}".format(a)) 
    print("b: {}".format(b)) 


if __name__ == "__main__": 
    test(1, 2) 

你这样做吗?当然! 应该你这样做?可能不会!

(代码可用here

+0

这看起来像一个体面的解决方案,但我认为我看到一个问题。如果您试图分配任何注入more_vars的变量,您将创建一个局部范围变量,其名称与注入的“假”全局相同,除非您还在该函数中添加全局声明。有没有办法解决? – user108471 2014-11-05 01:55:59

+0

这就是为什么你的设计首先是一个坏主意。你为什么要打破命名空间?对于什么可能的用例这可能是一个好主意?您是否考虑过使用可调用的类,并且轻松而明确地注入新对象,而不是用装饰器来装饰? – wim 2014-11-05 11:38:53

+0

如果难以实施,总会有一种倾向,即在不理解背后的动机的情况下,“不应该首先这样做”。我明白了,挑战性的问题当你将它们改变成不同的,更容易的问题时肯定会更容易解决。但我并没有要求其他策略来获得我所追求的功能,我期待着看到这个策略是否可以实施。如果你不知道用装饰器做这件事的方式,那很好,但是请不要要求一篇文章解释我所有的动机导致这一点。 – user108471 2014-11-05 14:53:02

0

这听起来像你唯一的问题是,help正显示出原始test作为包装的函数的签名的签名,你不希望它。

发生这种情况的唯一原因是wraps(或者说,update_wrapper,其中wraps调用)明确地将其从wrappee复制到包装。

你可以决定你做什么,不想复制。如果你想做的不同是很简单的,这只是一个过滤的东西出默认WRAPPER_ASSIGNMENTSWRAPPER_UPDATES的问题。如果您想要更改其他内容,则可能需要分叉update_wrapper并使用您自己的版本 - 但functools是其中一个模块,在文档顶部有一个链接到the source,因为它旨在用作可读样本码。

在你的情况下,它可能只是wraps(f, updated=[])的问题,或者您可能需要做一些花哨,像使用inspect.signature得到的f签名,并将其修改为除去第一个参数,并明确构建一个包装甚至愚弄inspect模块。

+0

这不是唯一的问题。我也在我对这个问题的初始描述中写道:“我不喜欢这件事的事情是,当你使用这个装饰器时,你必须改变函数的调用签名,除了添加额外的变量装饰“。 “帮助”的问题直接跟在这句话之后,但这是两个不同的问题。在最后的代码片段中,我展示了我想要的样子。 – user108471 2014-11-05 14:38:29

相关问题