深入理解閉包,裝飾器,深拷貝淺拷貝

❗ 可樂發布文章是為了分享程式語言 python 的魅力,沒有在網上發布群號以及廣告。
💚 如果感興趣的話,大家可以關注一下可樂的公眾號(結尾處二維碼),就是對可樂最大的支援。

本篇內容可樂不僅僅呈現閉包,裝飾器以及深拷貝、淺拷貝的用法,還會和大家一起來理解這幾個高級用法,以及使用場景。相信大家看完全篇之後不僅僅會用這些高級用法,還知道在哪些地方用,如何用。

閑話不多說,直接上乾貨吧!

一、閉包

定義:閉包指的是能夠讀取其他函數內部變數的函數。他的表現形式是定義在函數內部的函數。

看到定義,大家可能覺得一臉懵逼。那麼通俗的說就是,內部函數引用了外部函數的局部變數,那麼就可以說創建了一個閉包。

在理解閉包之前,先回想一下函數的作用域(如果忘記了,大家可以往回看一下Python基礎 – 下篇的函數部分)。

✔ 語法

def 外部函數名(*args,**kwargs):
  外部函數程式碼塊
  def 內部函數名(*args,**kwargs):
    內部函數程式碼塊(注意:這裡有使用到外部函數的變數)
  return 內部函數名

✔ 舉例1

def out_func():
    name = "可樂"
    def inner_func():
        print(name)
    return inner_func
inner_func = out_func()
inner_func()
​
# 輸出: 可樂

✔ 舉例2

def out_func():
    name = "可樂"
    print(f"外部函數變數名name的記憶體地址是{id(name)}")
    def inner_func():
        print(f"內部函數變數名name的記憶體地址是{id(name)}")
​
    return inner_func
​
inner_func = out_func()
inner_func()

結果如下圖:

✔ 閉包的作用(功能)

在描述閉包功能之前,先來看看函數的作用域函數的作用域是在該函數內部生效。當函數程式碼執行結束後,該函數內部的變數就會被解釋器的垃圾回收機制(gc 模組)回收。
所以閉包的作用就是:當內部函數使用了外部函數的局部變數後,python 解釋器在垃圾回收時會發現內部函數執行需要依賴外部函數的變數,那麼解釋器就不會回收該變數所佔用的資源。

✔ 使用場景

🙋‍♂️:閉包一般在什麼地方會用呢?
👨‍💻:根據定義可知,有一些場景需要去訪問其他函數的局部變數,那麼你可以使用閉包。一般來說閉包會搭配裝飾器來使用,單獨使用的場景比較少見。

二、裝飾器

解釋:在不改變原對象的情況下,動態的擴展對象功能,需要遵循開放封閉原則。

✔ 開放封閉原則

何謂開放封閉原則,開放指的是源程式碼新功能開放封閉指的是源程式碼修改是不允許的、是封閉的

✔ 無參數語法

@裝飾器名
def 函數名(*args,**kwargs):
  實際程式碼

✔ 有參數語法

@裝飾器名(參數)
def 函數名(*args,**kwargs):
  實際程式碼

✔ 思考

在開始實現裝飾器之前,可樂先拋出一個問題:我想列印一下函數 demo1 的執行時間,有什麼方法呢 ?

程式碼如下:

import time
​
def demo1():
    for i in range(100):
        pass
​
before_time = time.time()
demo1()
after_time = time.time()
print(after_time - before_time)

結果解釋:

time.time() 返回的是當前時刻的時間戳秒數,所以列印出來的就是該函數的執行時間。

如果可樂還想知道 demo2,demo3,demo4 函數的執行時間,那麼是不是要寫很多重複的程式碼呢?這個時候我們可以用裝飾器來解決這個問題。

2.1 無參數裝飾器

✔ 裝飾器列印函數運行時間

import time
​
def print_time(func):
    def wrapper():
        before_time = time.time()
        func()
        after_time = time.time()
        print(after_time - before_time)
​
    return wrapper
​
@print_time
def demo1():
    for i in range(100):
        pass
​
demo1()

✔ 詳細拆解

如果大家還沒有看懂是什麼個情況,可樂再給上述的函數拆解一下。

① 我們先定義一個如下函數:

import time
​
def print_time(func):
    def wrapper():
        before_time = time.time()
        func()
        after_time = time.time()
        print(after_time - before_time)
​
    return wrapper
​
def demo1():
    for i in range(10):
        print(1)

② 此時如果我們需要列印 demo1 的執行時間,那麼只需要這樣調用:

wrapper = print_time(demo1)
wrapper()

和上述裝飾器計算函數執行時間的具有一樣的效果,由此我們可以看出:@print_time 其實就和 print_time(demo1) 是等價的

2.2 有參數裝飾器

🙋‍♂️ 根據上面可樂拆解的步驟以及語法,大家思考一下如果是有參數的裝飾器那麼應該是咋樣的呢?以及拆分步驟是如何?

根據上面無參數的程式碼可知:
 → @print_time 實際等價於 print_time(demo1)
 → 而有參數的語法是:@print_time(參數)
所以我們可以推斷出:
 → @print_time(參數) 等價於 print_time(參數)(demo1)

下面我們來證明一下:

✔ 舉例:有參數裝飾器執行時間

該參數是調用者的名字。

import time
​
def caller_name(name):
    def print_time(func):
        def wrapper():
            before_time = time.time()
            func(name)
            after_time = time.time()
            print(after_time - before_time)
​
        return wrapper
​
    return print_time
​
@caller_name(name="可樂")
def demo1(name):
    print(f"調用者的名字是{name}")
    for i in range(100):
        pass
demo1()
​
# 輸出 可樂  4.81秒

✔ 詳細拆解

import time
​
def caller_name(name):
    def print_time(func):
        def wrapper():
            before_time = time.time()
            func(name)
            after_time = time.time()
            print(after_time - before_time)
​
        return wrapper
​
    return print_time
​
def demo1(name):
    print(f"調用者的名字是{name}")
    for i in range(100):
        pass
# 此時如果我們需要列印 demo1 的執行時間和調用者的名字,那麼只需要這樣調用
print_time = caller_name("kele")
wrapper = print_time(demo1)
wrapper()
# 由此得出上述我們的推斷是正確的。

❗ 在 python 中除了函數裝飾器,還有類裝飾器。由於篇幅的原因,可樂就先不在本篇說明(後續可樂時間寬裕時,會加篇補充)。

提示:在開始深拷貝和淺拷貝之前,需要先了解引用、可變類型和不可變類型。可樂在這裡就不再描述了,如果不清楚的,可以往回看Python基礎 – 列表和元祖部分,可樂有詳細解釋。

三、拷貝

解釋:拷貝就是將一個變數的值傳給另外一個變數。

3.1 深拷貝

解釋:深拷貝是指原對象和拷貝對象完全獨立,對其中任何一個對象的改動都不會對另外一個對象有影響。

看了上面的定義,有的同學可能還有一些疑惑。可樂就來舉一個🌰:
  假設工廠就是一塊記憶體,工廠先造出來一個箱子 A ,這個時候工廠又按照 A 的樣子造出來箱子 B ,那麼我們對箱子 A 放入一個蘋果,對箱子 B 毫無影響,給箱子 B 放入一隻貓,也對箱子A沒有影響。

✔ 舉例1:用一段程式碼來演示深拷貝

import copy
​
box_a = [1, ["可樂", 18]]
box_b = copy.deepcopy(box_a)  
# 解釋一下:可以通過 copy 包的 deepcopy 方法來實現深拷貝
​
print(f"box_a內層列表的記憶體地址是{id(box_a[1])}")
print(f"box_a內層列表的記憶體地址是{id(box_b[1])}")
​
box_a[1][1] = 20
print(f"改變後box_a的值是{box_a}")
print(f"改變後box_b的值是{box_b}")

結果如下圖:

✔ 舉例2

import copy
​
box_a = [1, ["可樂", 18]]
box_b = copy.deepcopy(box_a)  
# 解釋一下:可以通過 copy 包的 deepcopy 方法來實現深拷貝
​
print(f"box_a內層數字1的記憶體地址是{id(box_a[0])}")
print(f"box_b內層數字1記憶體地址是{id(box_b[0])}")
​
box_a[0] = 2
print(f"改變後box_a的值是{box_a}")
print(f"改變後box_b的值是{box_b}")

結果如下圖:

✔ 解釋

解釋上面深拷貝的情況以及注意點:
① 在深拷貝時如果拷貝的是不可變類型,那麼拷貝出來的仍然是其引用。
② 在深拷貝時如果拷貝對象時可變類型,那麼拷貝出來的是他的值而非引用。

3.2 淺拷貝

解釋:淺拷貝是指拷貝對象對原對象最外層拷貝,內部元素拷貝的是他的引用,當修改拷貝對象或者原對象時會相互影響。

還是用箱子來舉個🌰:
  假設工廠就是一塊記憶體,工廠先造出來一個箱子 A,這個時候工廠又按照 A 的樣子造出來箱子 B ,可是在造箱子 B 的時候使得箱子 A 和 B 有一部分連體了(類似於連體嬰兒),這個時候會發現當給箱子 A 放入一個蘋果時,會影響到箱子 B ;當給箱子 B 放入貓時也會影響到 A 。

✔ 舉例1

import copy
​
box_a = [1, ["可樂", 18]]
box_b = copy.copy(box_a)  
# 解釋一下:可以通過 copy 包的 copy 方法來實現淺拷貝
​
print(f"box_a內層數字1的記憶體地址是{id(box_a[0])}")
print(f"box_b內層數字1記憶體地址是{id(box_b[0])}")
​
box_a[0] = 2
print(f"改變後box_a的值是{box_a}")
print(f"改變後box_b的值是{box_b}")

結果如下圖:

✔ 舉例2

import copy
​
box_a = [1, ["可樂", 18]]
box_b = copy.copy(box_a)  
# 解釋一下:可以通過 copy 包的 copy 方法來實現淺拷貝
​
print(f"box_a內層列表的記憶體地址是{id(box_a[1])}")
print(f"box_b內層列表記憶體地址是{id(box_b[1])}")
​
box_a[1][1] = 20
print(f"改變後box_a的值是{box_a}")
print(f"改變後box_b的值是{box_b}")

結果如下圖:

✔ 舉例3

import copy
​
box_a = [1, ["可樂", 18]]
box_b = copy.copy(box_a)  
# 解釋一下:可以通過 copy 包的 copy 方法來實現淺拷貝
​
print(f"box_a內層列表的記憶體地址是{id(box_a[0])}")
print(f"box_b內層列表記憶體地址是{id(box_b[0])}")
​
box_a[0] = 20
print(f"改變後box_a的值是{box_a}")
print(f"改變後box_b的值是{box_b}")
print(f"box_a內層列表的記憶體地址是{id(box_a[0])}")
print(f"box_b內層列表記憶體地址是{id(box_b[0])}")

結果如下圖:

✔ 解釋

解釋上面淺拷貝的情況以及注意點:
① 在淺拷貝中,無論內層對象是可變類型還是不可變類型,拷貝出來的都是其引用。
② 無論是深拷貝還是淺拷貝,如果拷貝對象的不可變類型,那麼修改該可變類型之後,不會影響另外一個對象。
( 原因可樂就不在這篇描述了,可以在之前可樂的Python基礎 – 列表和元祖部分中查找 )

✔ 補充

無論是深拷貝還是淺拷貝都會對原對象拷貝,區別在於其內層對象拷貝的是引用還是值

如果大家感興趣的話,可以掃描下方二維碼關注一下可樂的公眾號,就是對可樂的最大支援;公眾號後面會時不時分享可樂平時遇到的問題、學習心得以及平常開發中的一些項目設計。還有最最最重要的就是關注可樂不迷路💚。

< END>

在這裡插入圖片描述