給python入門者的幫助,關於函式和裝飾器的理解。

华腾海神發表於2024-03-11
有時候學習不能過於較真,至少在合適的時機之前,還是悶頭吞知識,等吃飽了,就有精力(足夠的能量儲備,足夠的經驗)來理解更深的理解,但是很多時候,包括我自己,都喜歡在吃飽之前就研究自己在吃什麼,為什麼這個東西能吃這種問題。

最近發現幾年前寫的一篇關於python函式return的一些理解,又被檢視了,而且評論中表示得到了幫助。但是慚愧的是,那篇理解是我剛學python沒多久的個人理解,由於儲備的認識不足,經驗不足,思考不足,許多理解是不恰當的,或者是錯的,所以今天重新寫一篇關於python函式的理解。
這篇理解主要是幫助剛入門的朋友儘快理解和接收函式,實際上也會有許多不那麼準確的比喻和描述,因為一旦涉及到完全準確的底層,就會變得晦澀難懂,就會在解決這個問題之前,遇到更多問題,這顯然對學習是不利的。

簡單理解函式和return

對初學者來說,首先遇到的一個迷惑的問題就是什麼是變數,什麼是函式,為什麼函式名加一個()就是執行,"函式名()"是什麼
首先,變數是一個名字,變數的本質是記憶體地址,但是為了方便記憶,方便閱讀,要用一個閱讀者可以快速識別的代號來指代。
函式也算是一個變數,只是通常說的變數指向的記憶體地址,是儲存數值的記憶體地址,函式名指向的記憶體地址,是可執行物件的記憶體地址。

函式,就是一個處理過程,是對某些已知變數或者資料的一系列處理、計算。一般時候,這些過程都是存在記憶體中的,只有啟用這個過程,CPU才會把這一系列處理過程從記憶體中拿到CPU快取裡一步步執行。
這個啟用的訊號,就是(),其實這個括號,是一個傳引數的動作,可以把定義好的函式理解為一條生產線上的一個機床,程式執行,就是產線通電,機床沒有上料之前,機床處於待命狀態,物料進來了,機床開始動作。
引數就是物料,有些時候可能()裡並沒有引數,這是因為大部分資料都是存在系統裡的,只有特殊時候,需要人工放引數進去。

def func():
    x = 1
    y = 2
    z = x + y
    return z

func()

這就是一個簡答的函式和呼叫,暫時也叫做函式啟用,return就是把函式處理的結果輸出出來。
問題來了,我們通常會有下面的疑惑:

def age():
    return 15


A = 12

B = 13

C = age()

A 和 B 賦值可以看懂,通俗理解就是 左邊的等於右邊的,那C是怎麼回事
在程式程式碼裡,func()除了代表啟用函式外,這個字元本身可以理解為是一個系統的臨時變數(實際上,這涉及到記憶體模型和堆疊),這個臨時變數用於儲存函式輸出的處理結果。而且這個臨時變數只保留一行程式碼的時間。
也就是在函式啟用,處理完畢之後,如果沒有一個變數來接收臨時變數,這個臨時變數就會被系統回收(這涉及到python的垃圾處理機制,這個值所在的記憶體沒有被任何引用,就會被清理)
簡單理解為產線機床調出來的東西,沒有任何箱子或盒子盛放,直接扔到地上了,搞5S的就把它當成垃圾掃走了。

那什麼也不return的函式時咋回事呢

def void():
    pass

empty = void()
print(empty)

void函式什麼也沒有return,但是執行上面的程式碼會發現,empty是None

如果執行下面的程式碼

def void():
    pass

N = None

print(id(N), id(void()))

會發現列印出來的地址是一樣的,首先在python裡,所有不可變的基礎資料都是指向的同一個地址,就是說,不管在哪裡定義的變數,A=123, B=123,A和B指向的是同一個地址
None也是如此,None是空,是無(並不是0)

其實python在函式執行上有個預設的行為,就是會在函式執行完之後return None,如果函式中有return語句,那麼就不會走預設的return None,如果沒有return,就會走預設的return None

理解裝飾器

首先什麼是裝飾器,裝飾是什麼意思,在主體的外面,加一些功能性的東西。
也就是裝飾器,是不觸及被裝飾目標(物件)的基礎上,新增一些其他的額外功能。

def dec(func):
    def inside(*args, **kwargs):
        result = func(*args, **kwargs)
        return result

    return inside

@dec
def func_nothing(x):
    y = x

@dec
def func_return(x):
    return x

上面是裝飾器一般形式,那麼裝飾器做了什麼

其實說白了,裝飾器就是修改,替換

def dec(func):
    print("01== FUNC IS ==%s"%func)
    def inside(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    print("02== INSIDE ==%s"%inside)
    return inside

@dec
def func_nothing(x):
    y = x

@dec
def func_return(x):
    return x
print("Start========")
print(func_nothing)
print(func_nothing("X"))
print(func_return)
print(func_return("Y"))

執行結果:

01== FUNC IS ==<function func_nothing at 0x0000020127B96550>
02== INSIDE ==<function dec.<locals>.inside at 0x00000201485F7820>
01== FUNC IS ==<function func_return at 0x00000201485F7790>
02== INSIDE ==<function dec.<locals>.inside at 0x00000201487115E0>
Start========
<function dec.<locals>.inside at 0x00000201485F7820>
None
<function dec.<locals>.inside at 0x00000201487115E0>
Y

我們來分析下
在定義完函式之後,列印第一行Start========之前,直譯器就已經執行了一堆東西,也就是說@dec實際上就是一個執行操作(這個後面再說)
關鍵在這裡:關注第一個函式 func_nothing,在裝飾器裡面的列印結果中看到,func_nothing的ID地址和我們後面主動列印 print(func_nothing) 輸出的不一樣
仔細再看會發現,print(func_nothing)輸出的居然和裝飾器內部列印的inside地址一樣,記憶體地址不會騙人,也就是說,裝飾後的func_nothing實際上就是inside函式

看下面的程式碼

tasks = []


def talk():
    print("Get out of my way!")


def got_method(method):
    tasks.append(method)
    return talk


@got_method
def speak():
    print("Hello..")


print(tasks)
speak()

執行結果如下:

[<function speak at 0x000001C22C267820>]
Get out of my way!

執行speak,卻實際執行了talk,我們沒有往tasks中放東西,但是tasks中有了一個元素。
唯一有嫌疑的就是@got_method,我貌似使用了一個裝飾器,但是got_method並不是裝飾器的格式,這就要涉及python中的@語法了。

@是做什麼的

從上面的程式碼來看@got_method做了這些事:

@got_method
def speak():
    print("Hello..")

實際等於

def got_method(method):
    tasks.append(method)
    return talk

def speak():
    print("Hello..")
    
speak = got_method(speak)

@實際上是執行了got_method,把被@施加的函式作為引數傳給got_method。
也就是說

@dec
def func():
    pass

等於

def func():
    pass

func = dec(func)

就是在@dec的地方,執行了裝飾器,替換了被裝飾的函式。

再回到最開始的裝飾器標準格式上

def dec(func):
    def inside(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return inside

@dec
def func_return(x):
    return x


print(func_return)
print(func_return("Y"))

現在裝飾器應該是解釋清楚了,裝飾器裡讓人不好理解的就是@符號,@是一個執行操作,相當於執行了裝飾器函式並把被裝飾的函式作為引數
裝飾器的意義在於利用閉包和變數的作用域來實現函式功能的擴充套件。裝飾器的使用主要就是為了保證函式的修改不影響其他地方的引用,目的和介面一致。

“專案中有五個登記的函式,在專案各處被大量引用,現在公司處於安全考慮,要對這個登記函式加認證,有兩個方案,一個是修改這五個函式,在五個函式中分別加入認證,
第二個是定義一個裝飾器,然後給五個函式分別裝飾上。第一個方案最簡單直接,但是後期維護就很麻煩,認證再次變動就要找出所有當時使用認證的地方,全都改一次,漏了任何一個都會出現
生產事故,第二個方案在維護上和可讀性上就優秀很多,我只用修改裝飾器,那麼所有新增認證的地方就都完成了修改。”

這就是裝飾器的意義。

裝飾器運作邏輯和函式return的邏輯講完了,接下來就是要解釋最後一個問題,裝飾器內部為什麼要把函式執行結果return出去。

分析上面的程式碼:

使用dec裝飾func_return之後,func_return實際上已經被替換成了inside函式,執行func_return實際就是執行inside,
但是我們要保證被裝飾的原func_return正常執行,功能不受影響,所以原func_return的輸出必須要做保留,至少在閉包內要保留,
那麼為什麼inside中 要把func的結果return出來,因為原先func_return就是輸出結果的,裝飾器內部是否return,要看該函式所在的程式碼中是否使用了輸出結果。
如果函式被呼叫的地方使用了輸出結果,那麼就要在裝飾器內部把func輸出結果接收,然後return出去。

在這裡千萬不要把return的作用理解錯誤了,return是傳遞的操作,函式只能透過return把自己輸出的資料傳遞給呼叫自己的一級,想象五個人傳麻袋,必須是每個人都把前一個人遞過來的
東西接過來,然後再遞給下一個,你不能接了不遞出去,更不能不接,這是return。

python裡能夠做到不用層層傳遞的只有raise,可以理解為扔出去,路徑上任何人都可以接住,或者不理睬,raise就是扔,或者說是冒泡。而return只能層層傳遞。

最後,透過上面的實驗,可以得出最後一個結論,裝飾器是不是一定要return什麼東西出來?
不是,要不要return,取決於要不要接收結果,如果需要從內部傳遞資料出去,那就必須層層遞傳,層層return,如果不需要,那就不用return。

本文首發於部落格園,轉載須註明出處

https://www.cnblogs.com/haiton/p/18065460

相關文章