一文掌握Python返回函數、閉包、裝飾器、偏函數

2022-06-30 18:01:26
本篇文章給大家帶來了關於Python 的相關知識,其中主要整理了高階程式設計的相關問題,包括了返回函數、閉包、裝飾器、偏函數等等內容,下面一起來看一下,希望對大家有幫助。

【相關推薦:Python3視訊教學

1.返回函數

高階函數除了可以接受函數作為引數外,還可以把函數作為結果值返回。我們在操作函數的時候,如果不需要立刻求和,而是在後面的程式碼中,根據需要再計算
例如下面

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/23 22:41def sum_fun_a(*args):
    a = 0
    for n in args:
        a = a + n      
    return a

這是我不需要立即計算我的結果sum_fun方法,不返回求和的結果,而是返回求和的函數,例如下方

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/23 22:41def sum_fun_b(*args):
    def sum_a():
        a = 0
        for n in args:
            a = a + n        return a    return sum_a

當我們呼叫 sum_fun_b() 時,返回的並不是求和結果,而是求和函數 sum_a , 當我們在調sum_fun_b函數時將他賦值給變數

f1 = sum_fun_b(1, 2, 3, 4, 5)#  此時f為一個物件範例化,並不會直接生成值print(f1())  # 15f2 = sum_fun_b(1, 2, 3, 4, 5)f3 = sum_fun_b(1, 2, 3, 4, 5)print(f2, f3)<function sum_fun_b.<locals>.sum_a at 0x0000016E1E1EFD30> <function sum_fun_b.<locals>.sum_a at 0x0000016E1E1EF700>print(id(f2), id(f3))1899067537152 1899067538880

此時我們直接拿到的值就是15,那可以想一想,此時 f = sum_a,那這裡存在一個疑問引數去哪裡了?
而且我們看到建立的兩個方法相互不影響的,地址及值是不相同的

在函數 sum_fun_b 中又定義了函數 sum_a ,並且,內部函數 sum_a 可以參照外部函數 sum_fun_b 的引數和區域性變數,當 sum_fun_b 返回函數 sum_a 時,而對應的引數和變數都儲存在返回的函數中,這裡稱為 閉包 。


2.閉包

什麼是閉包?
先看一段程式碼

# 定義一個函數def fun_a(num_a):# 在函數內部再定義⼀個函數# 並且這個內部函數⽤到了外部的變數,這個函數以及⽤到外部函數的變數及引數叫 閉包
    def fun_b(num_b):
        print('內嵌函數fun_b的引數是:%s,外部函數fun_a的引數是:%s' % (num_b, num_a))
        return num_a + num_b    
    # 這裡返回的就是閉包的結果
    return fun_b# 給fun_a函數賦值,這個10就是傳參給fun_aret = fun_a(10)# 注意這裡的10其實是賦值給fun_bprint(ret(10))# 注意這裡的90其實是賦值給fun_bprint(ret(90))

執行結果:

內嵌函數fun_b的引數是:10,外部函數fun_a的引數是:1020內嵌函數fun_b的引數是:90,外部函數fun_a的引數是:10100

此時,內部函數對外部函數作⽤域⾥變數的引⽤(⾮全域性變數),則稱內部函數為閉包。

這裡閉包需要有三個條件

"""
三個條件,缺一不可: 
1)必須有一個內嵌函數(函數裡定義的函數)——這對應函數之間的巢狀 
2)內嵌函數必須參照一個定義在閉合範圍內(外部函數裡)的變數——內部函數參照外部變數
3)外部函數必須返回內嵌函數——必須返回那個內部函數
"""

"python# python互動環境編輯器 >>> def counter(start=0): 
	count = [start]
	def incr(): 
		count[0] += 1 
		return count[0] 
		return incr 
		
>>> c1 = counter(5)>>> print(c1()) 6>>> print(c1()) 7>>> c2=counter(50) >>> print(c2()) 51>>> print(c2()) >52>>>

當一個函數在本地作用域找不到變數申明時會向外層函數尋找,這在函數閉包中很常見但是在本地作用域中使用的變數後,還想對此變數進行更改賦值就會報錯

def test(): 
	 count = 1 
	 def add(): 
		  print(count) 
		  count += 1 
	 return add 
a = test() a()

報錯資訊:

Traceback (most recent call last): ...... UnboundLocalError: local variable 'count' referenced before assignment

如果我在函數內加一行nonlocal count就可解決這個問題
程式碼

# -*- coding: UTF-8 -*- # def test(): 
	 # count不是區域性變數,介於全域性變數和區域性變數之間的一種變數,nonlocal標識
	 count = 1 
	 def add(): 
		  nonlocal count 
		  print(count) 
		  count += 1 
		  return count 	 return add 

a = test() a() # 1 a() # 2

nonlocal宣告的變數不是區域性變數,也不是全域性變數,而是外部巢狀函數內的變數。

如果從另一個角度來看我們給此函數增加了記錄函數狀態的功能。當然,這也可以通過申明全域性變數來實現增加函數狀態的功能。當這樣會出現以下問題:

1. 每次呼叫函數時,都得在全域性作用域申明變數。別人呼叫函數時還得檢視函數內部程式碼。 
3. 當函數在多個地方被呼叫並且同時記錄著很多狀態時,會造成非常地混亂。

使用nonlocal的好處是,在為函數新增狀態時不用額外地新增全域性變數,因此可以大量地呼叫此函數並同時記錄著多個函數狀態,每個函數都是獨立、獨特的。針對此項功能其實還個一個方法,就是使用類,通過定義__call__ 可實現在一個範例上直接像函數一樣呼叫

程式碼如下:

def line_conf(a, b): 
	def line(x): 
		return a * x + b 
		
	return line
	
line1 = line_conf(1, 1) line2 = line_conf(4, 5) print(line1(5)) 
	print(line2(5))

執行結果為

625

從這段程式碼中,函數line與變數a,b構成閉包。在建立閉包的時候,我們通過line_conf的引數a,b說明了這兩個變數的取值,這樣,我們就確定了函數的最終形式(y = x + 1和y = 4x + 5)。我們只需要變換引數a,b,就可以獲得不同的
直線表達函數。由此,我們可以看到,閉包也具有提⾼程式碼可復⽤性的作⽤。如果沒有閉包,我們需要每次建立函數的時候同時說明a,b,x。這樣,我們就需要更多的引數傳遞,也減少了程式碼的可移植性。

1.閉包似優化了變數,原來需要類物件完成的⼯作,閉包也可以完成 
2.由於閉包引⽤了外部函數的區域性變數,則外部函數的區域性變數沒有及時釋放,消耗記憶體

但是還沒有結束,我們知道,函數內部函數,參照外部函數引數或值,進行內部函數運算執行,並不是完全返回一個函數,也有可能是一個在外部函數的值,我們還需要知道返回的函數不會立刻執行,而是直到呼叫了函數才會執
行。

看程式碼:

def fun_a(): 
	fun_list = [] 
	for i in range(1, 4): 
		def fun_b(): 
			return i * i 
			
			fun_list.append(fun_b) 
		return fun_list 
		
f1, f2, f3 = fun_a() print(f1(), f2(), f3())# 結果:9,9,9

這裡建立了一個fun_a函數,外部函數的引數fun_list定義了一個列表,在進行遍歷,迴圈函數fun_b,參照外部變數i 計算返回結果,加入列表,每次迴圈,都建立了一個新的函數,然後,把建立的3個函數都返回了

但是實際結果並不是我們想要的1,4,9,而是9,9,9,這是為什麼呢?

這是因為,返回的函數參照了變數 i ,但不是立刻執行。等到3個函數都返回時,它們所參照的變數i已經變成了3,每一個獨立的函數參照的物件是相同的變數,但是返回的值時候,3個函數都返回時,此時值已經完整了運算,並儲存,當呼叫函數,產生值不會達成想要的,返回函數不要參照任何迴圈變數,或者將來會發生變化的變數,但是如果一定需要呢,如何修改這個函數呢?

我們把這裡的i賦值給_就可以解決

def test3():
    func_list = []
    for i in range(1, 4):

        def test4(i_= i):
            return i_**2

        func_list.append(test4)
    return func_list


f1, f2, f3 = test3()print(f1(), f2(), f3())

可以再建立一個函數,用該函數的引數繫結迴圈變數當前的值,無論該回圈變數後續如何更改,已係結到函數引數的值不變,那我們就可以完成下面的程式碼

# -*- coding: UTF-8 -*- # def fun_a(): 
	def fun_c(i): 
		def fun_b(): 
			return i * i 
			
		return fun_b 

	fun_list = [] 
	for i in range(1, 4): 
		# f(i)立刻被執行,因此i的當前值被傳入f() 
		fun_list.append(fun_c(i)) 
	return fun_list 


f1, f2, f3 = fun_a() print(f1(), f2(), f3()) # 1 4 9

3.裝飾器 wraps

什麼是裝飾器?

看一段程式碼:

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/24 17:03def eat():
    print('吃飯')def test1(func):
    def test2():
        print('做飯')
        
        func()
        
        print('洗碗')
    return test2


eat()  # 呼叫eat函數# 吃飯test1(eat)()# 做飯# 吃飯# 洗碗

由於函數也是一個物件,而且函數物件可以被賦值給變數,所以,通過變數也能呼叫該函數。也可以將函數賦值變數,做參傳入另一個函數。

1>什麼是裝飾器

"""
裝飾器本質上是一個Python函數,它可以讓其他函數在不需要做任何程式碼變動的前提下增加額外功能,裝飾器的返回值 也是一個函數物件。 
它經常用於有以下場景,比如:插入紀錄檔、效能測試、事務處理、快取、許可權校驗等場景。裝飾器是解決這類問題的絕 佳設計
"""

裝飾器的作用就是為已經存在的物件新增額外的功能
先看程式碼:

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/24 17:03def test1(func):
    def test2():
        print('做飯')
        func()
        print('洗碗')
    return test2@test1  # 裝飾器def eat():
    print('吃飯')eat()# 做飯# 吃飯# 洗碗

我們沒有直接將eat函數作為引數傳入test1中,只是將test1函數以@方式裝飾在eat函數上。
也就是說,被裝飾的函數,函數名作為引數,傳入到裝飾器函數上,不影響eat函數的功能,再此基礎上可以根據業務或者功能增加條件或者資訊。

(注意:@在裝飾器這裡是作為Python語法裡面的語法糖寫法,用來做修飾。)

但是我們這裡就存在一個問題這裡引入魔術方法 name 這是屬於 python 中的內建類屬性,就是它會天生就存在與一個 python 程式中,代表對應程式名稱,一般一段程式作為主線執行程式時其內建名稱就是 main ,當自己作為模組被呼叫時就是自己的名字

程式碼:

print(eat.__name__)# test2

這並不是我們想要的!輸出應該是" eat"。這裡的函數被test2替代了。它重寫了我們函數的名字和註釋檔案,那怎麼阻止變化呢,Python提供functools模組裡面的wraps函數解決了問題

程式碼:

 -*- coding: utf-8 -*-
 from functools import wrapsdef test1(func):
    @wraps(func)
    def test2():
        print('做飯')
        func()
        print('洗碗')
    return test2@test1  # 裝飾器def eat():
    print('吃飯')eat()# 做飯# 吃飯# 洗碗print(eat.__name__)# eat

我們在裝飾器函數內,作用eat的test2函數上也增加了一個裝飾器wraps還是帶引數的。
這個裝飾器的功能就是不改變使用裝飾器原有函數的結構。

我們熟悉了操作,拿來熟悉一下具體的功能實現,我們可以寫一個列印紀錄檔的功能

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/24 17:42import timefrom functools import wrapsdef logger(func):
    @wraps(func)
    def write_log():
        print('[info]--時間:%s' % time.strftime('%Y-%m-%d %H:%M:%S'))
        func()
    return write_log@loggerdef work():
    print('我在工作')work()# [info]--時間:2022-06-24 17:52:11# 我在工作print(work.__name__)#work

2>帶參裝飾器

我們也看到裝飾器wraps也是帶引數的,那我們是不是也可以定義帶引數的裝飾器呢,我們可以使用一個函數來包裹裝飾器,調入這個引數。

# -*- coding: utf-8 -*-# python 全棧# author : a wei # 開發時間: 2022/6/24 17:42import timefrom functools import wrapsdef logs(func):
    @wraps(func)
    def write_log(*args, **kwargs):
        print('[info]--時間:%s' % time.strftime('%Y-%m-%d %H:%M:%S'))
        func(*args, **kwargs)
    return write_log@logsdef work():
    print('我在工作')@logsdef work2(name1, name2):
    print('%s和%s在工作' % (name1, name2))work2('張三', '李四')# [info]--時間:2022-06-24 18:04:04# 張三和李四在工作

3>函數做裝飾器

把紀錄檔寫入檔案

# -*- coding: utf-8 -*-# python 全棧# author : a wei# 開發時間: 2022/6/20 0:06import timefrom functools import wrapsdef logger(file):
    def logs(fun):
        @wraps(fun)
        def write_log(*args, **kwargs):
            log = '[info] 時間是:%s' % time.strftime('%Y-%m-%d %H:%M:%S')
            print(log)
            with open(file, 'a+') as f:
                f.write(log)
            fun(*args, **kwargs)
        return write_log    return logs@logger('work.log')  # 使用裝飾器來給 work函數增加記錄紀錄檔的功能def work(name, name2):  # 1.當前 work可能有多個引數 2.自定義紀錄檔檔案的名字和位置,記錄紀錄檔級別
    print(f'{name}和{name2}在工作')work('張三', '李四')

終端輸出:
在這裡插入圖片描述

這裡生成裡work.log紀錄檔檔案
在這裡插入圖片描述
裡面記錄紀錄檔
在這裡插入圖片描述
這裡我們將帶引數的帶入進去根據程式碼流程執行生成了檔案並將檔案列印進去現在我們有了能用於正式環境的logs裝飾器,但當我們的應用的某些部分還比較脆弱時,異常也許是需要更緊急關注的事情。

比方說有時你只想打紀錄檔到一個檔案。而有時你想把引起你注意的問題傳送到一個email,同時也保留
紀錄檔,留個記錄。

這是一個使用繼承的場景,但目前為止我們只看到過用來構建裝飾器的函數。

4>類做裝飾器

# -*- coding: utf-8 -*-# python 全棧# author : a wei# 開發時間: 2022/6/20 0:06import timefrom functools import wraps# 不使用函數做裝飾器,使用類做裝飾器class Logs(object):
    def __init__(self, log_file='out.log', level='info'):
        # 初始化一個預設檔案和預設紀錄檔級別
        self.log_file = log_file
        self.level = level    def __call__(self, fun):  # 定義裝飾器,需要一個接受函數
        @wraps(fun)
        def write_log(name, name2):
            log = '[%s] 時間是:%s' % (self.level, time.strftime('%Y-%m-%d %H:%M:%S'))
            print(log)
            with open(self.log_file, 'a+') as f:
                f.write(log)
            fun(name, name2)
        return write_log@Logs()  # 使用裝飾器來給 work函數增加記錄紀錄檔的功能def work(name, name2):  # 1.當前 work可能有多個引數 2.自定義紀錄檔檔案的名字和位置,記錄紀錄檔級別
    print(f'{name}和{name2}在工作')work('張三', '李四')  # 呼叫work函數

這個實現有一個優勢,在於比巢狀函數的方式更加整潔,而且包裹一個函數還是使用跟以前一樣的語法

4.偏函數 partial

Python的 functools 模組提供了很多有用的功能,其中一個就是偏函(Partial function)。要注意,這裡的偏函數和數學意義上的偏函數不一樣。

在介紹函數引數的時候,我們講到,通過設定引數的預設值,可以降低函數呼叫的難度。而偏函數也可以做到這一點。

例如:int() 函數可以把字串轉換為整數,當僅傳入字串時, int() 函數預設按十進位制轉換

>>> int('123') 123

但 int() 函數還提供額外的 base 引數,預設值為 10 。如果傳入 base 引數,就可以做進位制的轉換

>>> int('12345', base=8) 5349 >>> int('12345', 16) 74565

如果要轉換大量的二進位制字串,每次都傳入 int(x, base=2) 非常麻煩,於是,我們想到,可以定義一個int2() 的函數,預設把 base=2 傳進去:

程式碼:

# 定一個轉換義函數 >>> def int_1(num, base=2): 
		return int(num, base) 
		>>> int_1('1000000') 64>>> int_1('1010101') 85

把一個函數的某些引數給固定住(也就是設定預設值),返回一個新的函數,呼叫這個新函數會更簡單
繼續優化,functools.partial 就是幫助我們建立一個偏函數的,不需要我們自己定義 int_1() ,可以直接使用下面的程式碼創 建一個新的函數 int_1

# 匯入 >>> import functools 

# 偏函數處理 >>> int_2 = functools.partial(int, base=2) >>> int_2('1000000') 64>>> int_2('1010101') 85

理清了 functools.partial 的作用就是,把一個函數的某些引數給固定住(也就是設定預設值),返回一個新的函數,呼叫這個新函數會更簡單。

注意到上面的新的 int_2 函數,僅僅是把 base 引數重新設定預設值為 2 ,但也可以在函數呼叫時傳入其他值實際上固定了int()函數的關鍵字引數 base

int2('10010')

相當於是:

kw = { base: 2 } int('10010', **kw)

當函數的引數個數太多,需要簡化時,使用 functools.partial 可以建立一個新的函數,這個新函數可以固定住原函數的部分引數,從而在呼叫時更簡單

【相關推薦:Python3視訊教學

以上就是一文掌握Python返回函數、閉包、裝飾器、偏函數的詳細內容,更多請關注TW511.COM其它相關文章!