Telegraf+Influxdb+Grafana自動化運維監控
今天寫一篇兒紙來解釋一下python下的閉包的概念,再說一下python裡非常有用的一個特性:裝飾器。我自己之前在學習這部分內容的時候很蒙,看了很多blog的解釋才理解。希望能解釋清楚一些,和大家一起討論。
一、引數作用域與閉包
任何一門語言都有引數作用域的概念,python也不例外。總體來說,在哪個作用域裡定義的變數一般就應該在哪個作用域裡使用,儘量不要跨作用域使用。當然python也提供了跨作用域使用變數的功能,但是處理起來要小心一些,否則就會出錯誤。
一般的,在某個作用域內定義的變數,在該作用域被銷燬之前,是可以被當前作用域
def scope_1():
a = 1
def scope_2():
print (a)
return
scope_2()
return
scope_1()
# 1
上述程式碼段,在scope_1的作用域內定義的區域性變數a=1,可以在scope_2內被使用。當我們執行scope_1時,在scope_1的return語句執行(scope_1作用域被銷燬)之前,呼叫了scope_2,此時變數a還未被銷燬,因此可以被scope_2使用。
python中如果在一個函式(如scope_1)的內部定義了另外一個函式(如scope_2),我們稱第一個函式(scope_1)為外函式,第二個函式(scope_2)為內函式。下面是閉包的概念:在一個外函式中定義了一個內函式,內函式裡使用了外函式作用域的區域性變數,並且外函式的返回值是內函式的引用。這樣就構成了一個閉包。
# 外函式scope_1
def scope_1():
# 外函式的區域性變數a=1
a = 1
# 內函式scope_2
def scope_2():
# 內函式中使用了外函式的區域性變數a
print (a)
return
# 外函式返回值為內函式的引用
return scope_2
func = scope_1()
func()
# 1
這段程式碼和前面那一段看起來很像,都是巢狀定義了兩個函式,都是內層函式使用了外層函式的區域性變數。但是有2個明顯的區別:
1、外函式的返回值是內函式的引用;
2、呼叫時先呼叫外函式,返回值為一個函式物件賦值給func,再呼叫func。
二、裝飾器
2.1 裝飾器概念
在介紹裝飾器晦澀難懂的定義之前,我們先通過一個例子來理解裝飾器的邏輯。比如小明買了一個毛坯房,小明把它裝修一下颳了大白貼了瓷磚。後來小明交了個女朋友,女朋友進家之後覺得家裡的裝修太素了,於是想在臥室裡增加一些可愛的元素點綴一下。那麼小明沒有必要把整個臥室的裝修全鏟了重新再裝一遍,他去買了一些牆紙、壁畫和花草,再原始裝修的基礎上裝飾了一下臥室,就完美的跟女朋友交差了。
python的裝飾器也是同樣的邏輯:我們本來已經有了完成業務邏輯的程式碼,比如一個函式,現在我們想對這個邏輯新增一些功能,這些功能跟已有的邏輯沒有很大的衝突,更多的是在原始功能上的一些補充,因此沒有必要去修改原始的功能模組,直接在原始的基礎上進行新增即可。考慮這樣一種情況,比如我們已經有了一個函式
t
a
s
k
task
task用來執行某項任務,現在我們需要新增以下功能,即在每次執行
t
a
s
k
task
task之前去列印一個log,代表當前正在執行任務,比如在執行task之前print(‘Doing work now.’)。我們考慮如何實現上述修改。
def task():
print (1)
return
task()
# 1
2.1.1 直接修改task函式
最直觀的想法就是直接去修改task函式,把新增的列印log功能加入到程式碼段裡,比如:
def task():
print ('Doing work now.')
print (1)
return
task()
# Doing work now.
# 1
這樣做至少有以下2個問題:
1、直接修改很麻煩。比如我們有100種不同的任務,我們分別為每種任務都寫了一個函式來實現邏輯,
t
a
s
k
1
,
t
a
s
k
2
.
.
.
t
a
s
k
100
task_1,task_2...task_{100}
task1,task2...task100。那麼我們需要去修改每一個函式,非常囉嗦。
2、不利於擴充套件。新增的業務邏輯可能是多種多樣的,比如有時候我們需要去列印log,有的時候我們需要去開啟計時,有的時候我們需要去報警。這樣修改起來還是很囉嗦。
2.1.2 把新增的功能和task分開
既然新增的功能僅僅是對原始功能的一個裝飾,即新增的功能不會破壞task函式的內容,那麼可以考慮把新增的功能單獨包裝成一個函式,比如:
def task():
print (1)
return
def log():
print ('Doing work now.')
return
log()
task()
# Doing work now.
# 1
但是這樣做也有問題,原來只需要執行task(),現在需要再task()之前加上一行log()來顯示的呼叫log函式先輸出,看起來似乎破壞了結構,也顯得不fancy。
2.1.3 把task通過wrapper打包起來
還可以通過一個wrapper把task打包起來,類似上面講的閉包的例子,比如:
def wrapper():
print ('Doing work now.')
def task():
print (1)
return
return task
task = wrapper()
task()
# Doing work now.
# 1
這樣做的問題類似於2.1.2小節,需要手動去新增task = wrapper()這行程式碼,而且看起來好像重新定義了一個新的task,破壞了原始結構不fancy!如何不手動修改每個task的程式碼,又不修改執行時的僅僅需要一行task()的程式碼邏輯呢?
2.2 最簡單的裝飾器
為了解決上述問題,我們可以把程式碼如下修改,執行的時候不需要修改,仍然是一行task():
# 下面是一個典型的閉包
# 外函式log
def log(func):
# 外函式作用域的變數func
print ('Doing work now.')
# 內函式wrapper
def wrapper():
# 內函式裡使用了外函式作用域的變數func
func()
return
# 外函式的返回值為內函式的引用
return wrapper
@log
def task():
print (1)
return
task()
# Doing work now.
# 1
通過@關鍵字可以將一個裝飾器函式裝飾到業務函式上,上述修改後的程式碼在執行時候的邏輯,相當於:
1、被裝飾函式=裝飾器符號@後面的所有符號(被裝飾函式);
2、呼叫被裝飾函式。即:
task = log(task)
# 執行log(func=task),外函式中新增的裝飾功能首先執行,然後返回wrapper物件,此時task=wrapper
task()
# 呼叫task相當於呼叫wrapper,而wrapper函式體內執行了func函式的功能,func函式正是外函式傳入的引數task(閉包發揮作用),完成了原始業務邏輯的執行
2.3 被裝飾函式需要帶引數執行
有些業務函式(被裝飾函式)執行時需要帶引數,可以通過wrapper函式傳入業務函式執行時需要的實參,例如:
def log(func):
print ('Doing work now.')
def wrapper(*args, **kwds):
func(*args, **kwds)
return
return wrapper
@log
def task(*args, **kwds):
for arg in args:
print (arg)
for key in kwds.keys():
print ('%s: %s.' % (key, kwds[key]))
return
task(1,name='Bob', sex='male')
# Doing work now.
# 1
# name: Bob.
# sex: male.
執行task()的過程相當於:
task = log(task)
task(1, name='Bob', ses='male')
2.4 帶引數的裝飾器
這部分需要和2.3節的內容區分開,這部分說的是有些裝飾器需要帶引數,可以引入更大的靈活性,幫助使用者設計更多有用的功能,例如:
# 外函式log
def log(level):
# 第一內函式decorator
def decorator(func):
# 第一內函式中使用了外函式log中的變數level,形成閉包
if level == 'warning':
print ('Warning.')
else:
print ('Doing work now.')
# 第二內函式wrapper
def wrapper():
# 第二內函式中使用了第一內函式decorator中的變數func,形成閉包
func()
return
# 返回第二內函式的引用
return wrapper
# 返回第一函式的引用
return decorator
@log('warning')
def task():
print (1)
return
task()
# Warning.
# 1
執行task()的過程相當於:
task = log('warning')(task)
task()
2.5 多個裝飾器
函式功能複雜時,可能需要多個裝飾器裝飾同一個函式,其裝飾順序為從裡到外,例如:
def log(func):
print ('Doing work now.')
def wrapper():
func()
return
return wrapper
def warning(func):
print ('Warning now.')
def wrapper():
func()
return
return wrapper
@warning
@log
def task():
print (1)
return
task()
# Doing work now.
# Warning now.
# 1
執行task()的過程相當於:
task = log(task)
task = warning(task)
task()
2.6 類裝飾器
類也可以作為裝飾器,相比函式裝飾器,類裝飾器靈活度更高並且具有繼承、封裝、多型等面向物件的優點,使用類裝飾器主要是呼叫類的自帶方法__call__,例如:
class decorator(object):
def __init__(self, func):
self.func = func
def __call__(self):
print ('This is a decorator.')
self.func()
@decorator
def task():
print (1)
return
task()
# This is a decorator.
# 1
執行task()的過程相當於:
task = decorator(task)
# 執行decorator(task),相當於以func=task實參例項化了一個decorator物件並賦值給task
task()
# 執行task相當於執行task.__call__()
水平有限,寫的不對請指正。