Python:作用域、閉包、裝飾器
學到裝飾器,然後總結一下理解裝飾器需要的知識
一.python的作用域
當然,Python與大多數編程語言一樣,搜索變數值的時候,即命名空間的規則,會採用就近原則.
- 具體來說,由近及遠依次為: 本地作用域(Local) --> 外部嵌套函數作用域(Enclosing Local) --> 全局/模塊作用域(Global) --> 內置作用域(Built-in).
a = abs(-1) # abs:Built-in
b = 3 # b:Global
def f1():
print(b) # b:Global
a = 3 # a:Local
print(a)
def f2():
a = 3 # a:Enclosing
def f3():
b = 3 # b:Local
print(b)
- Local 與 Enclosing 是一個相對的概念. 在函數 f1 中, a 是一個 Local 變數, 而在 f2 中 a 是一個 Enclosing 變數.
- 只有模塊、類、函數才會引入新的作用域. 而 if for while 語句不會引入新的作用域.
- 全局作用域中的變數對於下層作用域比如函數來說, 是一個只讀變數.
a = 4
def foo():
a = 3
foo()
print(a) # 4
函數內部不是修改全局變數 a 的值,而是重新定義了一個本地變數 a.所以全局變數 a 的值沒有改變
a = 4
def foo():
a = a + 3
foo()
# local variable a referenced before assignment
在函數 foo 內部, a = a + 3 這個表達式的存在會讓 Python 編譯函數的定義體時,它判斷 a 是局部變數,因為在函數中給它賦值了。
- 內部作用域中要修改外部作用域變數的值時,要用 global、nonlocal 關鍵字聲明外部作用域變數
a = 3
def foo():
global a # 使用 global 聲明 a ,便可以在函數中修改 a 的值
a = 4
foo()
print(a)
# 4
def f1():
a = 3
def f2():
nonlocal a # 使用 nonlocal 聲明 a, 便可以在嵌套的函數內部修改 a 的值
a = a + 1
print(a)
f2()
二.閉包與自由變數
什麼是閉包?
閉包指延伸了作用域的函數,其中包含函數定義體中引用、但是不在定義體中定義的非全局變數。函數是不是匿名的沒有關係,關鍵是它能訪問定義體之外定義的非全局變數
總結:閉包是個函數,它能夠訪問函數體之外定義的非全局變數,而這個非全局變數指的就是自由變數.
下面舉一個例子, 定義一個 avg 函數,參數為一個值, 不斷累加的計算從開始到現在所接收的全部值的平均值
def make_average():
series = []
def average(value):
series.append(value)
total = sum(series)
return total / len(series)
return average
avg = make_average() # 1
avg(10) # 2
# 10
avg(20) # 3
# 15
說明:1調用 make_average 返回一個 average 函數對象.這就是一個閉包函數,因為 avg 可以訪問 average 函數定義體之外的 series .
注意:這裡嵌套函數 average 並沒有改變 series, 只是修改它的值, 因為 series 是一個可變的列表.所以並不會報錯.那麼如果 series 是一個不可變對象呢? 會發生什麼?
上面的例子效率比較低, 沒一次都得 sum.我們難道不可以保存每一步計算的 total 嗎?
def make_average():
count = 0
total = 0
def average(value):
count += 1
total += value
return total / count
return average
avg = make_average()
avg(10)
# local variable count referenced before assignment
說明: 在嵌套函數 average 內部有 count += 1 ,此表達式等價為 count = count + 1.在編譯階段, 會把內部的 count 解釋為一個本地變數, 所以如果沒有 nonlocal 聲明的話, 會報錯 local variable count referenced before assignment.
簡單的處理當然不行, 我們需要使用 nonlocal 將 count 和 total 變成自由變數
def make_average():
count = 0
total = 0
def average(value):
nonlocal count, total
count += 1
total += value
return total / count
return average
# 開始調用.返回一個函數對象,並且這個對象是一個閉包
avg = make_average()
avg(10) # 10
avg(20) # 15
三.裝飾器
裝飾器用來裝飾一個函數,為函數添加額外的功能,一般來說並不是核心功能.
裝飾器接收一個函數作為參數,裝飾器可能會處理被裝飾的函數,然後把它返回,或者將其替換成另一個函數或可調用對象。
舉例, 我們定義一個裝飾器,用來計算並且顯示每個函數運行的時間
import time
def decorate(func):
def wrapper(*args):
to = time.perf_counter()
result = func(*args)
t1 = time.perf_counter() - to
print(f運行時間為:{t1})
return result
return wrapper
讓我們裝飾一下別的函數
@decorate
def foo(n):
i = 0
while i < n*n:
i += 1
return i
print(foo(600))
# 程序的運行時間為:0.022877089999383315
# 360000
`@decorate `是一個語法糖, 等同於 func = decorate(func), 所以此時 func 是 wrapper 函數的引用.
如何證明?
foo.__name__
# wrapper
這是一個瑕疵啊,我們需要改進.由此我們需要使用一些標準庫裝飾器.
使用 functools.wraps 裝飾器把相關的屬性(func._name 和 func.doc_)從 func 複製到 wrapper 中
from functools import wraps
import time
def decorate(func):
@wraps(func) # 將func的一些屬性覆蓋到wrapper中去
def wrapper(*args):
to = time.perf_counter()
result = func(*args)
t1 = time.perf_counter() - to
print(f運行時間為:{t1})
return result
return wrapper
尾聲
腳踏實地的前進, 希望自己學得越來越多, 然後分享出來嘍.
推薦閱讀: