博客 / 詳情

返回

Python面試必備二之 lambda 表達式、函數傳參 args 和 kwargs、垃圾回收機制和上下文管理器

本文首發於公眾號:Hunter後端

原文鏈接:Python面試必備二之 lambda 表達式、函數傳參 args 和 kwargs、垃圾回收機制和上下文管理器

本篇筆記主要介紹 Python 面試過程中關於 Lambda 表達式、函數傳參、垃圾回收機制等問題,大致如下:

  1. Python 中 Lambda 表達式是什麼,有什麼用
  2. 函數的參數 args 怎麼用,kwargs 怎麼用
  3. Python 怎麼進行垃圾回收,都有哪幾種類型
  4. Python 上下文是什麼,怎麼定義上下文

以下是本篇筆記目錄:

  1. Lambda 表達式
  2. 函數傳參 args 和 kwargs
  3. 垃圾回收機制
  4. 上下文管理器

1、Lambda 表達式

Lambda 表達式,即 Lambda 函數,是一個匿名函數,也就是説我們可以創建一個不需要定義函數名的函數。

1. Lambda 函數的定義和調用

比如對於下面的兩數相加的函數:

def add(x, y):
    return x + y

我們可以使用 lambda 函數表示如下:

add_lambda = lambda x, y: x + y

對於 add 函數和 add_lambda 匿名函數,這兩個函數的效果是一致的,都是對於輸入的兩個參數進行相加,然後返回:

print(add(1, 3))

print(add_lambda(1, 3))

Lambda 函數的定義方式其實很簡單:

lambda x, y: x + y

使用 lambda 修飾,表示定義一個函數,之後跟着的 x 和 y 表示輸入的參數,冒號 : 後跟着的即為需要 return 的函數邏輯,這裏是相加。

2. Lambda 函數的使用

除了前面直接調用的使用場景,Lambda 還有一個比較常用的場景,就是用在 Python 的內置函數中,比如 map,sorted 等。

下面以兩個示例來介紹 Lambda 的使用。

1) 獲取兩個列表對應位置之和

給定兩個列表,獲取這兩個列表對應索引位置的和,形成一個新列表返回,這裏使用 Python 的 map 函數和 Lambda:

a = [1, 2, 3]
b = [4, 5, 6]

c = list(map(lambda x, y: x + y, a, b))
print(c)
# [5, 7, 9]

這裏使用的 Lambda 函數接收兩個參數,其來源是 a 和 b 兩個列表。

2) 對字典列表根據指定 key 排序

我們有一個字典列表如下:

s = [{"a": 6}, {"a": 19}, {"a": 3}, {"a": 7}]

如果我們想要對其根據元素的 a 這個 key 的值進行從小到大進行排序,其操作如下:

sort_s = sorted(s, key=lambda x: x["a"])
print(sort_s)
# [{'a': 3}, {'a': 6}, {'a': 7}, {'a': 19}]

2、函數傳參 args 和 kwargs

當我們定義一個函數的時候,需要為其指定參數,比如一個兩數相加的函數示例如下:

def add(x, y):
    return x + y

而如果我們想要實現一個函數,可能會傳入兩個數,也可能傳入三個、四個,甚至七八個數字,然後返回其和,這個能實現嗎?

可以,這個就需要用到我們的可變參數了,也就是這裏的 args 和 kwargs。

1. args

在定義函數中,如果我們不確定會傳入多少個參數,我們可以使用 args 這個可變參數,它的作用是將不定量的參數按照順序以 tuple 元組傳入,在函數的定義中,前面加一個 * 號即可完成定義。

比如我們想實現一個不定量數字相加然後返回的函數:

def add_n(*args):
    print("參數 args 內容為:", args)
    total = 0
    for num in args:
        total += num
    return total

print(add_n(1, 2))
print(add_n(1, 2, 3, 4))
print(add_n(1, 2, 3, 4, 5, 6, 7))

當然,對於這個不定量參數,如果我們想要對其截取特定數量,也可以如下實現:

a, b, c, *_ = args

2. kwargs

kwargs 也是以可變參數的形式傳入,不過不一樣的點在於函數是將任意個關鍵字參數放入一個 dict 進行處理的,其使用方式是在 kwargs 前加兩個 **

比如我們想要實現一個函數用於計算,計算到底是加法還是減法需要根據傳入的符號來確定,我們可以實現如下:

def calculate(a, b, **kwargs):
    print("參數 kwargs 內容為:", kwargs)
    if kwargs.get("add") is True:
        return a + b
    elif kwargs.get("sub") is True:
        return a - b
    else:
        print("不合法的運算符")
        return None
        
print(calculate(10, 5, add=True))
print(calculate(10, 5, sub=True))
print(calculate(10, 5, times=True))

可以看到 kwargs 的輸出內容為:{'add': True}

3. args 和 kwargs 混合使用

這裏介紹一下如何將固定參數和可變參數 args 以及 kwargs 混合在一起使用的示例。

以下函數示例沒有實際意義,僅為了展示如何使用參數:

def test_arg(a, b, *args, **kwargs):
    print("f固定參數為 a: {a}, b: {b}")
    print(f"不定量參數為 args: {args}")
    print(f"不定關鍵字參數為 kwargs: {kwargs}")

arg_list = [1, 2, 3]
kwarg_dict = {"x": 5, "y": 4}

a = 1
b = 2
test_arg(a, b)
test_arg(a, b, *arg_list)
test_arg(a, b, **kwarg_dict)
test_arg(a, b, *arg_list, **kwarg_dict)
test_arg(a, b, *arg_list, z=3, **kwarg_dict)

注意:這裏需要注意的一點是,當我們傳入列表或者字典作為參數傳入時,如果不加上 *** 作為修飾,那麼函數會將作為一個整體輸入,而非不定量參數:

test_arg(a, b, arg_list)
# 這裏輸入的參數的值就是 a, b, [1, 2, 3]
# 而不是 a, b, 1, 2, 3

注意:函數裏的 argskwargs 只是作為常用的寫法,而不是並非固定的關鍵字,我們也可以寫成 func(*arg_list, **kwarg_dict),在函數內部調用的時候分別替換一下即可。

4. 用於裝飾器

不定量參數 args 和 kwargs 還有一個比較常見的場景就是用於裝飾器,比如下面的示例:

def decorator_func(func):
    def inner_func(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return inner_func

關於裝飾器更多詳細內容,參見:閉包與裝飾器

3、垃圾回收機制

Python 的垃圾回收機制有三種,一種是引用計數,一種是標記清除,一種是分代回收。

1. 引用計數

引用計數機制是通過記錄一個對象被引用的次數來管理內存的機制,當這個對象的引用次數為 0,那麼該對象則會被銷燬。

那麼引用計數的規則是怎麼樣的呢?

1) 引用計數規則

a. 引用計數增加規則

先來介紹一下引用計數的增加規則,當一個對象發生以下行為時,該對象的引用計數器 +1:

  1. 對象被創建
  2. 對象被引用
  3. 對象被作為參數傳給函數
  4. 對象作為一個元素,存儲在容器中(這裏的容器比如説有列表,字典等)

我們可以使用 sys.getrefcount() 函數來查看某個變量指向的對象被引用的次數。

注意:當使用這個函數查看被引用次數的時候,總是比實際的引用次數要 +1,因為作為參數傳給 sys.getrefcount() 的時候,引用次數也會 +1。

下面是示例:

import sys
# 987 這個對象被創建,次數 +1
a = 987
print(sys.getrefcount(a))  # 2

# 對象被引用,次數 +1
b = a
print(sys.getrefcount(a))  # 3

def test(a):
    print(sys.getrefcount(a))

# 對象被傳遞給函數,次數 +1
test(a)  # 4

# 對象作為元素存儲在元素中,次數 +1
c = [a]
print(sys.getrefcount(a))  # 4

這裏需要注意一下,雖然調用了函數 test(a) 增加了一次引用計數,但是在函數執行完之後,內部的變量被銷燬,所以其中增加的引用計數會消失。

b. 引用計數減少規則

前面介紹的是引用計數增加的規則,那麼與上述情況相對應的反向操作則會使得引用計數器 -1:

  1. 對象的別名被顯式銷燬, del a
  2. 對象的引用別名被賦予新的對象時, a = 99999
  3. 對象離開它的作用域時,比如函數執行完畢
  4. 將該元素從容器中刪除或者容器直接被銷燬時

以下是示例:

當對象的別名被銷燬時:

a = 99999
b = a
print(sys.getrefcount(a))  # 3

del b
print(sys.getrefcount(a)) # 2

當對象的別名被賦予新的對象時:

a = 99999
b = a
print(sys.getrefcount(a)) # 3

b = 100000
print(sys.getrefcount(a)) # 2

當元素被從容器中刪除或者容器銷燬時:

a = 99999
c = [a]
print(sys.getrefcount(a))  # 3

c.remove(a)
print(sys.getrefcount(a)) # 2


a = 99999
c = [a]
print(sys.getrefcount(a)) # 3

del c
print(sys.getrefcount(a)) # 2

還有一個當調用這個對象的函數執行完畢之後,它的引用次數也會 -1,這一點在前面增加規則裏也已經説明了。

在我們執行程序的過程中,引用計數根據上面的增減規則進行計數,當某個對象的引用次數變成了 0,比如我們執行了對某個變量進行了新的賦值,且原對象沒有其他引用,或者使用了 del 刪除了引用等方式造成了這個對象的引用計數變成了 0,那麼它的內存則會被回收釋放。

c. 對象池和常量緩存

這裏需要注意一點,前面的示例中,我對 a 進行賦值的時候,取的都是很大的整數,比如 99999,100000 這種,為什麼呢。

因為在 Python 中,對於整數、字符串這種不可變對象,會有一些常見的小整數,以及某些短字符串,比如 -5-256 之間的整數,Python 解釋器啓動的時候就對其進行了緩存,並且在解釋器的整個生命週期中重用,所以當我們執行 a=1 的操作的時候,並不是創建一個新的整數對象,而是引用的已經存在的整數 1 對象。

可以看下面這個示例:

a = 256
print(sys.getrefcount(a))

a = 257
print(sys.getrefcount(a))

2. 標記-清除

前面介紹的引用計數機制有一個無法解決的一種的問題,就是循環引用。

1) 循環引用

什麼叫循環引用呢?

就是對象間互相引用,這種情況下,對象的引用次數並不為 0,但是沒有任何外部的引用指向他們,這種情況下,即便是刪除了引用,也無法通過引用計數機制對其進行回收。

以下是一個示例:

首先,創建兩個循環引用的對象:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

a = Node(1)
b = Node(2)

a.next = b
b.next = a

然後查看未被垃圾回收的對象裏是否有這兩個對象:

import gc
target_list = [obj for obj in gc.get_objects() if isinstance(obj, Node)]
for target in target_list:
    print(target.value)
# 1
# 2

這裏可以看到還是有這兩個對象的。

然後我們對其進行刪除操作:

del a
del b

然後再次獲取未被垃圾回收的對象裏是否還有這兩個對象,這裏我們可以打印出其引用對象的 value值:

import gc
target_list = [obj for obj in gc.get_objects() if isinstance(obj, Node)]
for target in target_list:
    print(target.value, gc.get_referents(target)[1].value)
# 1 2
# 2 1

可以看到,即便是我們刪除了這兩個對象,但是因為其內部的互相引用,導致其並沒有被回收釋放。

2) 標記-清除

那麼如何解決上面這個循環引用的問題呢,這裏就引入了 Python 進行垃圾回收的另一個機制,標記-清除。

標記-清除分為兩個階段,一個是標記,一個是清除。

在標記階段,系統會從根節點對象出發,遍歷所有對象,所有可以訪問到的對象(也就是還有對象引用它)會被打上一個標記,表示這個對象是可達的。

這裏的根節點對象,指的是全局變量、調用棧、內存器。

在清除階段,遍歷所有對象,將上一步中未被標記的不可達對象進行回收。

3. 分代回收

在執行垃圾回收的過程中,程序會被暫停,為了減少程序暫停的時間,採用分帶回收的機制來降低垃圾回收的耗時。

所謂分代回收就是將程序裏的對象分為三個世代,每個世代裏的對象會有不同的時間間隔進行檢測。

在 Python 中,一共 3 種世代,G0,G1 和 G2,其中,G0 包含新創建的對象,最頻繁的進行垃圾回收,G1 則包含在 G0 中倖存下來,也就是沒有被回收的對象,G2 則包括在 G1 中沒有被回收的對象,這幾個世代進行垃圾回收的頻率逐個降低。

每一個世代執行垃圾回收的機制是當每個世代的對象數量達到一定的閾值則開始進行當前世代的垃圾回收操作,通過下面的命令我們可以獲取到每個世代執行垃圾回收的對象數量閾值:

import gc
gc.get_threshold()
(700, 10, 10)

表示 G0 世代的對象數量閾值是 700,達到 700 後,則會開啓垃圾回收操作,第二、三個結果則表示的是 G1、G2 世代的閾值。

這個閾值我們也可以手動設置:

gc.set_threshold(800, 20, 10)

gc.get_threshold()

4. 手動執行垃圾回收

手動執行垃圾回收的操作如下:

import gc
gc.collect()

如果想要指定某個世代進行回收:

gc.collect(0)
gc.collect(1)
gc.collect(2)

4、上下文管理器

上下文管理器是一個對象,它定義了進入和退出特定上下文的標準方式,它的主要作用是管理資源,確保在使用資源時對其進行正確地分配和釋放。

以常用的打開文件為示例,使用 with 來操作上下文管理器:

with open("./test.txt", "r") as f:
    data = f.read()

在這個過程中,open() 函數就返回了一個文件對象,這個對象實現了上下文管理器協議,因此我們不用手動對其進行 close() 關閉文件的操作。

這樣的操作可以使代碼簡潔,提高代碼的可讀性,以及減少因忘記釋放資源而可能導致的資源泄露問題。

1. 手動實現上下文管理器

在 Python 中,只要一個類實現了 __enter____exit__ 方法,它的實例就是一個上下文管理器,其中,__enter__ 方法定義了進入 with 前執行的邏輯,__exit__ 方法則定義了退出 with 塊時調用的邏輯。

我們可以手動來實現一個上下文管理器。

class ReadFile:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        print("before read file")
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_value, exc_tb):
        if self.file:
            self.file.close()
        print("exit read file")

        
with ReadFile("/path/to/file.txt", "r") as f:
    content = f.read()
    print(content)

為了展示上下文管理器的作用,我們可以對這個讀取文件內容的上下文管理進行一個加強操作,比如讀取文件內容的時候出錯,或者文件不存在的時候不報錯,而是以打印信息的形式輸出,並且在這種出錯的情況下,文件仍然可以正常關閉:

import os 

class ReadFile:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        if not os.path.exists(self.filename):
            self.file = None
        else:
            self.file = open(self.filename, self.mode)
        return self

    def read(self):
        1 / 0
        return self.file.read()            

    def __exit__(self, exc_type, exc_value, exc_tb):
        if exc_value:
            print(exc_value)
        
        if self.file:
            print("file closed")
            self.file.close()
        return True

with ReadFile("/path/to/file", "r") as f:
    content = f.read()

在上面這個示例中,雖然在讀取文件內容的時候報錯了,但是卻沒有將錯誤 raise 出來,而是以打印的方式將錯誤信息暴露出來,而且文件也被正常關閉了。

2. 裝飾器實現

Python 提供了一個裝飾器,可以讓我們不新建類而是以創建函數的方式來實現一個上下文管理器。

以下是一個示例:

from contextlib import contextmanager

@contextmanager
def open_file(filename, mode):
    f = open(filename, mode)
    
    yield f
    
    f.close()

with open_file("/path/to/file", "r") as f:
    content = f.read()
    print(content)

在上面這個示例中,使用 yield 將代碼分為兩部分,上半部分屬於在類裏 __enter__ 方法的操作,下半部分屬於類裏 __exit__ 方法的操作。

而如果想要對錯誤進行捕獲並正確地釋放資源,我們需要額外添加 try-except 操作:

from contextlib import contextmanager

@contextmanager
def open_file(filename, mode):
    f = open(filename, mode)
    try:
        yield f
    except:
        f.close()

如果想獲取更多後端相關文章,可掃碼關注閲讀:

user avatar zeran 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.