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

kele是可樂呀發表於2021-03-16

❗ 可樂釋出文章是為了分享程式語言 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>

在這裡插入圖片描述

相關文章