1. 程式人生 > 實用技巧 >Telegraf+Influxdb+Grafana自動化運維監控

Telegraf+Influxdb+Grafana自動化運維監控

技術標籤:Pythonpython設計模式函式閉包

  今天寫一篇兒紙來解釋一下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。

那麼在執行的時候,func=scope_1()這一行執行結束之後,外函式scope_1已經退出,按理說下面再呼叫func時,a這個變數已經被銷燬不能被print出來,但是在實際執行時卻可以被正確呼叫。一般情況下,如果一個函式結束,函式的內部所有東西都會釋放掉,還給記憶體,區域性變數都會消失。但是閉包是一種特殊情況,如果外函式在結束的時候發現有自己的臨時變數將來會在內部函式中用到,就把這個臨時變數繫結給了內部函式,然後自己再結束。

二、裝飾器

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__()

  水平有限,寫的不對請指正。