2014-11-04 84 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