Python面試必備一之迭代器、生成器、淺拷貝、深拷貝

来源:https://www.cnblogs.com/hunterxiong/p/18117026
-Advertisement-
Play Games

本文首發於公眾號:Hunter後端 原文鏈接:Python面試必備一之迭代器、生成器、淺拷貝、深拷貝 這一篇筆記主要介紹 Python 面試過程中常被問到的一些問題,比如: Python 中的迭代器和生成器是什麼,有什麼作用 Python 中不可變類型有哪些 在 Python 函數中,傳遞參數傳遞的 ...


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

原文鏈接:Python面試必備一之迭代器、生成器、淺拷貝、深拷貝

這一篇筆記主要介紹 Python 面試過程中常被問到的一些問題,比如:

  1. Python 中的迭代器和生成器是什麼,有什麼作用
  2. Python 中不可變類型有哪些
  3. 在 Python 函數中,傳遞參數傳遞的是什麼,值還是引用
  4. 將一個列表或者字典傳入函數,在函數內部對其進行修改,會影響函數外部的該變數嗎
  5. Python 中的深拷貝和淺拷貝是什麼,怎麼用,區別是什麼

針對以上問題,本篇筆記將詳細闡述其原理,並用示例來對其進行解釋,本篇筆記目錄如下:

  1. 迭代器
  2. 生成器
  3. Python 中的可變與不可變類型
  4. Python 的函數參數傳遞
  5. 淺拷貝、深拷貝

1、迭代器

1. 迭代

在 Python 中,對於列表(list)、元組(tuple)、集合(set)等對象,我們可以通過 for 迴圈的方式拿到其中的元素,這個過程就是迭代。

2. 可迭代對象

在 Python 里,所有的數據都是對象,其中,可以實現迭代操作的數據就稱其為可迭代對象。

比如前面介紹的列表,元組,集合,字元串,字典都是可迭代對象。

如果要判斷一個對象是否是可迭代對象,可以通過與 typing.Iterable 來進行比較:

from typing import Iterable

print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance((1, 2, 3), Iterable))  # True
print(isinstance({1, 2, 3}, Iterable))  # True
print(isinstance({"a": 1, "b": 2}, Iterable))  # True
print(isinstance("asdsad", Iterable))  # True

3. 迭代器

我們可以將一個可迭代對象轉換成迭代器,所謂迭代器,就是內部含有 __iter____next__ 方法的對象,它可以記住遍歷位置,不會像列表那樣一次性全部載入。

迭代器有什麼好處呢,正如前面所言,因為不用一次性全部載入對象,所以可以節約記憶體,我們可以通過 next() 方法來逐個訪問對象中的元素。

我們可以使用 iter() 方法來將一個可迭代對象轉換成迭代器。

1) 創建迭代器

我們可以通過 iter() 函數來將可迭代對象轉換成迭代器:

s = [1, 2, 3]
s_2 = iter(s)

2) 判斷對象是否是迭代器

迭代器的類型是 typing.Iterator,我們可以通過 isinstance() 函數來進行判斷。

註意: 這裡進行測試的 Python 版本是 3.11,所以需要從 typing 中載入 Iterator,如果是之前的某個版本,應該從 collections 模塊中載入。

from typing import Iterator
 
isinstance(s, Iterator)  # False
isinstance(s_2, Iterator)  # True

3) 訪問迭代器

我們可以通過 next() 函數來訪問迭代器:

s = [1, 2, 3]
s_2 = iter(s)
next(s_2)  # 1
next(s_2)  # 2
next(s_2)  # 3
next(s_2)  # raise StopIteration

訪問迭代器的時候需要註意下,如果使用 next() 函數訪問到對象的末尾還接著訪問的話,會引發 StopIteration 的異常。

我們可以通過 try-except 的方式來捕獲:

s = [1, 2, 3]
s_2 = iter(s)

while True:
    try:
        print(next(s_2))
    except StopIteration:
        print("訪問結束")
        break

2、生成器

生成器也是一種迭代器,它也可以使用 next() 方法逐個訪問生成器中的元素,並且能夠實現惰性計算,延遲執行以達到節省記憶體的目的。

1. 生成器的創建

可以使用兩種方式創建生成器,一種是使用小括弧 () 操作列表生成式,一種是使用 yield 來修飾。

1) 使用列表生成式創建生成器

x = (i for i in range(10))
print(type(x))  # generator

前面介紹了生成器也是一種迭代器,下麵可以進行驗證操作:

from typing import Iterator
print(isinstance(x, Iterator))  # True

而生成器本身的類型為 Generator,也可以通過 typing 模塊引入:

from typing import Generator
print(isinstance(x, Generator))  # True

2) 使用 yield 欄位創建生成器

如果要使用 yield 來創建生成器,則需要將其放置在函數內,以下是一個示例:

def test_yield(n):
    for i in range(n):
        yield i

x = test_yield(8)
print(type(x))  # <class 'generator'>

print(next(x))  # 0

在這裡,yield 相當於 return 一個值,並且記住這個位置,在下次迭代時,代碼從 yield 的下一條語句開始執行。

2. 生成器的使用

前面介紹了生成器就是一種迭代器,所以可以使用迭代器的方式來訪問生成器,比如 for 迴圈,next() 方法等。

3. 生成器的應用示例

下麵介紹兩個運用生成器的實例,一個是用於斐波那契數列,一個是按行讀取文件。

1) 斐波那契數列

使用生成器來操作斐波那契數列,其函數操作如下:

def fibonacci(max_number):
    n, a, b = 0, 0, 1
    while n < max_number:
        yield b
        a, b = b, a + b
        n += 1

for i in fibonacci(6):
    print(i)

2) 讀取文件

如果有一個大文件,我們也可以使用生成器的方式來逐行讀取文件:

def read_file(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line

path = "path/to/file"
for line in read_file(path):
    print(line.strip())

4. 迭代器與生成器的異同

首先,生成器本身就是一個迭代器,所以生成器具有迭代器的所有優點,比如不用一次性載入全部對象,節約記憶體。

不同點在於兩者的創建方式是不一樣的,而且使用 yield 構成生成器的應用程度是更廣泛的。

3、Python 中的可變與不可變類型

首先,Python 中數據類型的可變與不可變的定義為當我們修改了它的值後它對應的記憶體地址是否變化。

如果一個數據類型,它的值修更改後,它的記憶體地址發生了改變,那麼我們稱其為不可變類型。

相反,如果我們修改某個數據類型的值後,記憶體地址沒有發生變化,那麼則稱其為可變類型。

我們可以這樣理解,對於同一個記憶體地址而言,如果可以修改變數的值,那麼它就是可變類型,否則是不可變類型。

1. 不可變類型

Python 中不可變的數據類型有 int、string、tuple、bool 等,示例如下:

s = 1
print(id(s))  # 140713862796072

s = 2
print(id(s))  # 140713862796104

上面的兩次輸出可以看到 s 這個變數的記憶體地址在值修改後就變化了。

2. 可變類型

Python 中可變的數據類型有 list、set、dict,這些數據類型在修改原值後,其記憶體地址不變,因此屬於可變類型。

s = [1,2,3]
print(id(s))  # 2116182318592

s.append(4)
print(id(s))  # 2116182318592

4、Python 的函數參數傳遞

這裡的問題其實是在 Python 中,我們往函數里傳參數時,是值傳遞還是引用傳遞。

所謂的值傳遞,就是把參數的值做一個拷貝,把拷貝的值傳到函數內。

所謂的引用傳遞,就是把參數的記憶體地址直接傳到函數內。

那麼在 Python 里,函數的傳參到底是哪一種呢,我們可以來做個實驗:

def test(a):
    print(id(a))

a = 1
print(id(a))  # 140713862796072
test(a)  # 140713862796072

a = [1, 2, 3]
print(id(a))  # 2116183414208
test(a)  # 2116183414208

可以看到,不管是不可變類型還是可變類型,我們傳入函數內部的變數的記憶體地址和外部變數的記憶體地址都是一樣的,因此,在 Python 中,函數的傳參都是傳遞的變數的引用,即變數的記憶體地址

可變類型與不可變類型的區別

這裡需要註意的一點,對於可變類型和不可變類型,當我們在函數內對其修改後,其是否會影響到外部變數呢,我們還是可以接著做一個測試,這裡對於兩種類型分別進行測試。

先做不可變類型的測試:

def test_1(a):
    print(f"函數內部修改前,a 的地址為: {id(a)}")
    a = 2
    print(f"函數內部修改後,a 的地址為: {id(a)}")

a = 1
print(f"調用函數前,a 的地址為:{id(a)}")

test_1(a)
print(f"函數外 a 的值是:{a},地址為:{id(a)}")

這裡輸出的信息如下:

調用函數前,a 的地址為:140713862796072
函數內部修改前,a 的地址為: 140713862796072
函數內部修改後,a 的地址為: 140713862796104
函數外 a 的值是:1,地址為:140713862796072

在這裡可以看到,雖然函數傳參傳入的是變數的引用,即記憶體地址,但因為它是不可變類型,所以對其修改後,函數內部相當於是對其重新申請了一個記憶體地址進行操作,但是不會影響函數外部原有的記憶體地址。

接下來測試一下可變數據類型:

def test_2(l):
    print(f"函數內部修改前,l 的地址為: {id(l)}")
    l.append(3)
    print(f"函數內部修改後,l 的地址為: {id(l)}")

l = [1, 2]
print(f"調用函數前,l 的地址為:{id(l)}")

test_2(l)
print(f"函數外 l 的值是:{l},地址為:{id(l)}")

其輸出的信息如下:

調用函數前,l 的地址為:2116196122176
函數內部修改前,l 的地址為: 2116196122176
函數內部修改後,l 的地址為: 2116196122176
函數外 l 的值是:[1, 2, 3],地址為:2116196122176

這裡可以看到,函數內外 l 變數的地址都是不變的,但因為是可變類型,所以在函數內部修改了變數的值以後,並沒有重新分配記憶體,所以在函數外部 l 變數同步被影響。

那麼在函數內部對傳入的可變類型變數進行任何操作都會影響到函數外部嗎?

不一定,這裡提供一個示例:

def test_3(l):
    print(f"修改變數前,l 的地址為:{id(l)}")
    l = l + [3]
    print(f"修改變數後,l 的地址為:{id(l)}")
    
l = [1, 2]
print(f"調用函數前,l 的地址為:{id(l)}, 值為:{l}")
test_3(l)
print(f"調用函數後,l 的地址為:{id(l)},值為:{l}")

它的輸出的信息如下:

調用函數前,l 的地址為:2116183414208, 值為:[1, 2]
修改變數前,l 的地址為:2116183414208
修改變數後,l 的地址為:2116200373376
調用函數後,l 的地址為:2116183414208,值為:[1, 2]

可以看到,在函數內部,對可變類型進行了操作之後,它的記憶體地址有所變化,而且修改後不會影響到原始變數。

這是因為在函數內部執行的操作是 l = l + [3],這個操作的本質並不是直接對變數的值進行修改,而是新建一個記憶體地址,然後對這個變數進行重新賦值,所以這個操作的 l 與函數傳入的變數 l 已經不是同一個變數了,因此不會影響到外部的變數。

多說一句,可變類型變數的這個操作其實就跟不可變類型的變數的重新賦值是同一個意義:

a = 1
a = 2

這裡其實也是因為對 a 進行了新的記憶體空間申請,然後重新賦值。

5、淺拷貝、深拷貝

1. 概念

在 Python 中,如果是不可變對象,比如 string,int 等,變數間的拷貝效果都是一致的,都會重新獲取一個記憶體地址,重新賦值,拷貝前後兩個變數不再相關。

而如果是可變對象,比如 list,set,dict 等,就需要區分淺拷貝和深拷貝。

淺拷貝的操作過程:為新變數重新分配記憶體地址,新變數的元素與原始變數的元素地址還是一致的。

但是如果原始變數的元素是不可變類型,那麼修改原始變數或新變數的元素之後,不會引起兩個變數的同步變化。

如果修改的是變數元素的可變類型,而可變類型進行修改後,其記憶體地址不會變的,則會引起兩個變數的同步變化。

深拷貝的操作過程:為新變數重新分配記憶體地址,創建一個對象,如果原始變數的元素中有嵌套的可變類型,那麼則會遞歸的將其中的全部元素都拷貝到新變數,拷貝過程結束之後,新變數與原始變數沒有任何關聯,只是簡單的值相等而已。

上面這兩個概念可能聽起來比較繞,接下來我們用示例來對其進行展示。

2. 淺拷貝

1) 元素為不可變類型

淺拷貝的操作使用 copy 模塊,引入和使用如下:

import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)

這裡使用元素為不可變類型的 dict 進行示例展示:

d1 = {"a": 1, "b": 2}
d2 = copy.copy(d1)

print(f"d1 的地址為:{id(d1)}")
print(f"d2 的地址為:{id(d2)}")

print(f"d1 a 的地址為:{id(d1['a'])}")
print(f"d2 a 的地址為:{id(d2['a'])}")

它的信息輸出如下:

d1 的地址為:2116196027264
d2 的地址為:2116200318400
d1 a 的地址為:140713862796072
d2 a 的地址為:140713862796072

可以看到,進行淺拷貝後,兩個變數的記憶體地址是不一樣的,但是內部的元素的地址都還是一樣的。

而如果對其元素的值進行更改,因為元素是不可變類型,所以更改之後其內部元素的地址也會不一樣:

d2["a"] = "2"
print(f"d1 的值為:{d1}")
print(f"d2 的值為:{d2}")

print(f"d1 a 元素的地址為:{id(d1['a'])}")
print(f"d2 a 元素的地址為:{id(d2['a'])}")

其輸出的內容如下:

d1 的值為:{'a': 1, 'b': 2}
d2 的值為:{'a': '2', 'b': 2}
d1 a 元素的地址為:140713862796072
d2 a 元素的地址為:140713862839480

2) 元素為可變類型

當需要拷貝的可變對象的元素也是可變類型的時候,比如,列表內嵌套了列表或者字典,或者字典內嵌套了列表或者字典,以及集合的相關嵌套,對其進行淺拷貝後,因其嵌套的元素是可變類型的,所以在對內部元素進行修改後,元素的記憶體地址還是會指向同一個,所以對外展示的影響就是,原始變數和新變數會同步更新數據。

接下來我們以字典內嵌套列表為例進行示例展示:

d1 = {"a": 1, "b": [1, 2]}
d2 = copy.copy(d1)

print(f"d1 的地址為:{id(d1)}, d1 的 b 元素的地址為:{id(d1['b'])}")
print(f"d2 的地址為:{id(d2)}, d2 的 b 元素的地址為:{id(d2['b'])}")

其輸出內容如下:

d1 的地址為:2116201415808, d1 的 b 元素的地址為:2116195489024
d2 的地址為:2116183354816, d2 的 b 元素的地址為:2116195489024

這裡可以看到 d1 和 d2 的記憶體地址是不一樣的,但是內部的 b 元素的記憶體地址一致。

接下來我們對 d2 的 b 列表進行修改,再來看一看兩者的地址和 d1 以及 d2 的值:

d2["b"].append(3)

print(f"d1 的值為:{d1}, d1 的 b 元素的地址為:{id(d1['b'])}")
print(f"d2 的值為:{d2}, d2 的 b 元素的地址為:{id(d2['b'])}")

其輸出內容如下:

d1 的值為:{'a': 1, 'b': [1, 2, 3]}, d1 的 b 元素的地址為:2116195489024
d2 的值為:{'a': 1, 'b': [1, 2, 3]}, d2 的 b 元素的地址為:2116195489024

可以看到,對 d2 修改 b 元素的值後,也同步反映到了 d1 上。

總結: 綜上,可以看到,在淺拷貝中,如果元素是不可變對象,那麼修改原始變數或新變數後,不會引起兩者的同步變化,如果元素是可變對象,那麼修改原始變數或者新變數後,則會引起兩者的同步變化。

3. 深拷貝

相對於淺拷貝而言,深拷貝的操作要簡單許多,不管元素是可變對象還是不可變對象,進行深拷貝後,原始變數和新變數從外到內都是不一樣的記憶體空間,而且修改任意一個都不會引起同步變化。

代碼示例如下:

import copy

d1 = {"a": 1, "b": [1, 2]}
d2 = copy.deepcopy(d1)

d2["b"].append(3)

print(f"d1 的值為:{d1},d1 的 b 元素地址為:{id(d1['b'])}")
print(f"d2 的值為:{d2},d2 的 b 元素地址為:{id(d2['b'])}")

其輸出內容如下:

d1 的值為:{'a': 1, 'b': [1, 2]},d1 的 b 元素地址為:2116199853248
d2 的值為:{'a': 1, 'b': [1, 2, 3]},d2 的 b 元素地址為:2116199512896

根據輸出可以看到,它的內容是符合我們前面對其的解釋的。

4. 總結

一般來說,如果沒有特殊需求,不需要原始變數與新變數之間有所關聯的話,建議使用深拷貝,因為淺拷貝的內部元素的關聯性,在實際編程中很容易造成數據混亂。

以上就是本次 Python 面試知識的全部內容,下一篇將介紹 Python 中的 lambda 表達式、函數傳參 args 和 kwargs 以及垃圾回收機制等。

如果想獲取更多後端相關文章,可掃碼關註閱讀:
image


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 安裝react-native-fs npm npm install react-native-fs --save yarn yarn add react-native-fs 安卓配置 android/settings.gradle ... include ':react-native-fs' pro ...
  • nvm nvm(Node Version Manager)是一個Node.js的版本管理器。 安裝nvm windows安裝nvm 1. 下載nvm 下載地址:nvm-windows,下載 nvm-noinstall 或者 nvm-setup.exe 如果使用 nvm-noinstall 可以運行 ...
  • 大家好,我是 Java陳序員。 今天,給大家介紹一個基於 Vue3 實現的高仿抖音開源項目。 關註微信公眾號:【Java陳序員】,獲取開源項目分享、AI副業分享、超200本經典電腦電子書籍等。 項目介紹 douyin —— 一個基於 Vue、Vite 實現,模仿抖音的移動端短視頻項目。 這個項目的 ...
  • 一、簡單分析 簡單的分析,從輸入 URL到回車後發生的行為如下: URL解析 DNS 查詢 TCP 連接 HTTP 請求 響應請求 頁面渲染 二、詳細分析 URL解析 首先判斷你輸入的是一個合法的URL 還是一個待搜索的關鍵詞,並且根據你輸入的內容進行對應操作 URL的解析第過程中的第一步,一個ur ...
  • 本文介紹在瀏覽器中,獲取網頁中的某一個請求信息,並將其導入到Postman軟體,併進行API請求測試的方法。 Postman是一款流行的API開發和測試工具,它提供了一個用戶友好的界面,用於創建、測試、調試和文檔化API。本文就介紹一下這一工具的最基本用法——導入網頁請求,並配置相關的Headers ...
  • vue3 快速入門系列 - 基礎 前面我們已經用 vue2 和 react 做過開發了。 從 vue2 升級到 vue3 成本較大,特別是較大的項目。所以許多公司對舊項目繼續使用vue2,新項目則使用 vue3。 有些UI框架,比如ant design vue1.x 使用的 vue2。但現在 ant ...
  • 需求 設置飛機的一些坐標位置(經緯度高度),插值得到更多的坐標位置,然後飛機按照這些坐標集合形成的航線飛行,飛機的朝向、俯仰角以及飛機轉彎時的翻轉角根據坐標集合計算得出,而不需要手動設置heading、pitch、roll。 坐標插值 不知道為什麼,可能是飛行速度變化太大,我用Cesium自帶的插值 ...
  • 參考:https://fecify.com/doc/cn-1.0/fecify-shop-helper-cloudflare-r2.html#%E4%BA%91%E5%AD%98%E5%82%A8-%E4%BD%BF%E7%94%A8cloudflare-r2 https://zhuanlan.zh ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...