2012-12-27 107 views
3

在Python,有可能這樣的嵌套函數:嵌套函數的性能開銷是多少?

def do_something(): 
    def helper(): 
     .... 
    .... 

除非Python的更巧妙地處理這種情況下,helper必須重新創建每次使用do_something。實際上是否有這樣的性能衝擊,而不是在主體之外創建輔助函數,如果是,它有多好?

回答

4

是,宣告了主函數內部助手是慢於單獨宣佈他們:

### test_nested.py ### 
import timeit 
def foo(): 
    def bar(): 
     pass 
    pass 
print(timeit.timeit("foo()", setup="from __main__ import foo")) 

### test_flat.py ### 
import timeit 
def foo(): 
    pass 
def bar(): 
    pass 
print(timeit.timeit("foo()", setup="from __main__ import foo, bar")) 


### Shell ### 
✗ python3 ./test_flat.py 
0.42562198638916016 
✗ python3 ./test_nested.py 
0.5836758613586426 

這是約30%放緩。請記住,在這個微不足道的案例中,創建和調用函數都是解釋器所做的。在任何實際使用中,差異將會小得多。

+0

確實,因爲每次執行外部函數時都會重新創建內部嵌套函數對象;創建對象涉及調用函數構造函數並傳遞(已編譯的)代碼對象。 –

+0

我剛剛注意到,我甚至忘記調用'bar',所以在這個測試中,與現實生活相比,這種影響更加誇張。 – Erik

+1

根據[Raymond Hettinger的「在Python中嵌套函數時是否有開銷?」](http://stackoverflow.com/a/7839697/4958),代碼對象被重用,所以不管長度如何內部函數(例如),唯一的開銷來自O(1)創建函數對象。所以嵌套函數不是免費的(就像我猜想的那樣添加一個分配),但是當嵌套函數「太大」時也不用擔心:無論嵌套函數是平凡還是平淡,開銷都是一樣的。 – ShreevatsaR

2

性能損失肯定存在。如果在調用另一個函數內部創建函數,則每次調用外部函數時都會創建函數對象。但是這個懲罰很小,通常可能會被忽略。尤其要考慮到一個明顯的事實:在大多數情況下,只有在不能放置在外部時,才應該創建嵌套函數。

您需要嵌套函數的原因是需要訪問嵌套函數內的外部函數的作用域變量。通常這會導致直接或間接地從外部函數返回內部函數對象(就像在裝飾器中一樣),或者可能將內部函數作爲回調傳遞給某個地方。由嵌套函數訪問的變量將一直存在,直到嵌套函數對象被銷燬,並且對於嵌套函數的不同實例它們將會不同,因爲每個實例都可以看到來自不同範圍實例的變量。

在我看來,僅僅比較創建一個空的內部函數來使用外部使用的相同函數所需的時間幾乎是毫無意義的。性能差異純粹來自代碼行爲的差異。所需的代碼行爲是應該讓您選擇放置函數的位置。

只是一個小例證:

def outer(n): 
    v1 = "abc%d" % n 
    v2 = "def" 
    def inner(): 
     print locals().keys() 
     return v1 
    v1 = "_" + v1 
    return inner 
f1 = outer(1) 
f2 = outer(2) 
print f1() 
print f2() 

輸出是:

['v1'] 
_abc1 
['v1'] 
_abc2 

的關鍵時刻:

  1. 內部函數的當地人()只包括外部函數當地人說,它使用(v1,但不是v2)。

  2. 在創建函數對象後更改了v1。但是,即使v1的類型是不可變的(str),內部函數仍然可以看到這些更改。所以,內部函數看到的是外部函數本地的真正子集,而不僅僅是函數對象創建時存儲的引用。幸運的是,內部函數對象的存在不會阻止v1以外的範圍變量被銷燬。如果我將v2的值替換爲一個在被銷燬時打印某些東西的對象,它會在外部函數退出時立即打印該消息。

  3. inner()的不同實例不共享單個外部作用域實例:v1值不同。

如果不使用嵌套函數,就無法實現所有這些效果。這就是爲什麼應該使用嵌套函數的原因,實際上並沒有性能損失:額外的行爲需要額外的時間。如果你需要額外的行爲,你應該使用嵌套函數。如果你不需要它,你不應該這樣做。

+3

我不同意你的斷言,這是嵌套函數應該被使用的唯一情況。通常,我在(僅)使用它的人中放置了一個輔助函數,因爲它沒有必要將模塊範圍與它混淆在一起,b)因爲這樣輔助器所屬的位置就更加明顯了。 – Erik

+0

當然,也可能有例外。有時(很少)我也是這麼做的,只是爲了隱藏一個函數(儘管通常在它的名字前面加下劃線就足夠了)。但是當我關心表現的時候並非如此。 – Ellioh