本文首發(fā)于公眾號(hào):Hunter后端
原文鏈接:Python面試必備一之迭代器、生成器、淺拷貝、深拷貝
這一篇筆記主要介紹 Python 面試過程中常被問到的一些問題,比如:
- Python 中的迭代器和生成器是什么,有什么作用
- Python 中不可變類型有哪些
- 在 Python 函數(shù)中,傳遞參數(shù)傳遞的是什么,值還是引用
- 將一個(gè)列表或者字典傳入函數(shù),在函數(shù)內(nèi)部對(duì)其進(jìn)行修改,會(huì)影響函數(shù)外部的該變量嗎
- Python 中的深拷貝和淺拷貝是什么,怎么用,區(qū)別是什么
針對(duì)以上問題,本篇筆記將詳細(xì)闡述其原理,并用示例來對(duì)其進(jìn)行解釋,本篇筆記目錄如下:
- 迭代器
- 生成器
- Python 中的可變與不可變類型
- Python 的函數(shù)參數(shù)傳遞
- 淺拷貝、深拷貝
1、迭代器
1. 迭代
在 Python 中,對(duì)于列表(list)、元組(tuple)、集合(set)等對(duì)象,我們可以通過 for 循環(huán)的方式拿到其中的元素,這個(gè)過程就是迭代。
2. 可迭代對(duì)象
在 Python 里,所有的數(shù)據(jù)都是對(duì)象,其中,可以實(shí)現(xiàn)迭代操作的數(shù)據(jù)就稱其為可迭代對(duì)象。
比如前面介紹的列表,元組,集合,字符串,字典都是可迭代對(duì)象。
如果要判斷一個(gè)對(duì)象是否是可迭代對(duì)象,可以通過與 typing.Iterable 來進(jìn)行比較:
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. 迭代器
我們可以將一個(gè)可迭代對(duì)象轉(zhuǎn)換成迭代器,所謂迭代器,就是內(nèi)部含有 __iter__
和 __next__
方法的對(duì)象,它可以記住遍歷位置,不會(huì)像列表那樣一次性全部加載。
迭代器有什么好處呢,正如前面所言,因?yàn)椴挥靡淮涡匀考虞d對(duì)象,所以可以節(jié)約內(nèi)存,我們可以通過 next() 方法來逐個(gè)訪問對(duì)象中的元素。
我們可以使用 iter() 方法來將一個(gè)可迭代對(duì)象轉(zhuǎn)換成迭代器。
1) 創(chuàng)建迭代器
我們可以通過 iter() 函數(shù)來將可迭代對(duì)象轉(zhuǎn)換成迭代器:
s = [1, 2, 3]
s_2 = iter(s)
2) 判斷對(duì)象是否是迭代器
迭代器的類型是 typing.Iterator,我們可以通過 isinstance() 函數(shù)來進(jìn)行判斷。
注意: 這里進(jìn)行測(cè)試的 Python 版本是 3.11,所以需要從 typing 中加載 Iterator,如果是之前的某個(gè)版本,應(yīng)該從 collections 模塊中加載。
from typing import Iterator
isinstance(s, Iterator) # False
isinstance(s_2, Iterator) # True
3) 訪問迭代器
我們可以通過 next() 函數(shù)來訪問迭代器:
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
訪問迭代器的時(shí)候需要注意下,如果使用 next() 函數(shù)訪問到對(duì)象的末尾還接著訪問的話,會(huì)引發(fā) StopIteration 的異常。
我們可以通過 try-except 的方式來捕獲:
s = [1, 2, 3]
s_2 = iter(s)
while True:
try:
print(next(s_2))
except StopIteration:
print("訪問結(jié)束")
break
2、生成器
生成器也是一種迭代器,它也可以使用 next() 方法逐個(gè)訪問生成器中的元素,并且能夠?qū)崿F(xiàn)惰性計(jì)算,延遲執(zhí)行以達(dá)到節(jié)省內(nèi)存的目的。
1. 生成器的創(chuàng)建
可以使用兩種方式創(chuàng)建生成器,一種是使用小括號(hào) ()
操作列表生成式,一種是使用 yield
來修飾。
1) 使用列表生成式創(chuàng)建生成器
x = (i for i in range(10))
print(type(x)) # generator
前面介紹了生成器也是一種迭代器,下面可以進(jìn)行驗(yàn)證操作:
from typing import Iterator
print(isinstance(x, Iterator)) # True
而生成器本身的類型為 Generator,也可以通過 typing 模塊引入:
from typing import Generator
print(isinstance(x, Generator)) # True
2) 使用 yield 字段創(chuàng)建生成器
如果要使用 yield 來創(chuàng)建生成器,則需要將其放置在函數(shù)內(nèi),以下是一個(gè)示例:
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 相當(dāng)于 return 一個(gè)值,并且記住這個(gè)位置,在下次迭代時(shí),代碼從 yield 的下一條語句開始執(zhí)行。
2. 生成器的使用
前面介紹了生成器就是一種迭代器,所以可以使用迭代器的方式來訪問生成器,比如 for 循環(huán),next() 方法等。
3. 生成器的應(yīng)用示例
下面介紹兩個(gè)運(yùn)用生成器的實(shí)例,一個(gè)是用于斐波那契數(shù)列,一個(gè)是按行讀取文件。
1) 斐波那契數(shù)列
使用生成器來操作斐波那契數(shù)列,其函數(shù)操作如下:
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) 讀取文件
如果有一個(gè)大文件,我們也可以使用生成器的方式來逐行讀取文件:
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. 迭代器與生成器的異同
首先,生成器本身就是一個(gè)迭代器,所以生成器具有迭代器的所有優(yōu)點(diǎn),比如不用一次性加載全部對(duì)象,節(jié)約內(nèi)存。
不同點(diǎn)在于兩者的創(chuàng)建方式是不一樣的,而且使用 yield 構(gòu)成生成器的應(yīng)用程度是更廣泛的。
3、Python 中的可變與不可變類型
首先,Python 中數(shù)據(jù)類型的可變與不可變的定義為當(dāng)我們修改了它的值后它對(duì)應(yīng)的內(nèi)存地址是否變化。
如果一個(gè)數(shù)據(jù)類型,它的值修更改后,它的內(nèi)存地址發(fā)生了改變,那么我們稱其為不可變類型。
相反,如果我們修改某個(gè)數(shù)據(jù)類型的值后,內(nèi)存地址沒有發(fā)生變化,那么則稱其為可變類型。
我們可以這樣理解,對(duì)于同一個(gè)內(nèi)存地址而言,如果可以修改變量的值,那么它就是可變類型,否則是不可變類型。
1. 不可變類型
Python 中不可變的數(shù)據(jù)類型有 int、string、tuple、bool 等,示例如下:
s = 1
print(id(s)) # 140713862796072
s = 2
print(id(s)) # 140713862796104
上面的兩次輸出可以看到 s 這個(gè)變量的內(nèi)存地址在值修改后就變化了。
2. 可變類型
Python 中可變的數(shù)據(jù)類型有 list、set、dict,這些數(shù)據(jù)類型在修改原值后,其內(nèi)存地址不變,因此屬于可變類型。
s = [1,2,3]
print(id(s)) # 2116182318592
s.append(4)
print(id(s)) # 2116182318592
4、Python 的函數(shù)參數(shù)傳遞
這里的問題其實(shí)是在 Python 中,我們往函數(shù)里傳參數(shù)時(shí),是值傳遞還是引用傳遞。
所謂的值傳遞,就是把參數(shù)的值做一個(gè)拷貝,把拷貝的值傳到函數(shù)內(nèi)。
所謂的引用傳遞,就是把參數(shù)的內(nèi)存地址直接傳到函數(shù)內(nèi)。
那么在 Python 里,函數(shù)的傳參到底是哪一種呢,我們可以來做個(gè)實(shí)驗(yàn):
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
可以看到,不管是不可變類型還是可變類型,我們傳入函數(shù)內(nèi)部的變量的內(nèi)存地址和外部變量的內(nèi)存地址都是一樣的,因此,在 Python 中,函數(shù)的傳參都是傳遞的變量的引用,即變量的內(nèi)存地址。
可變類型與不可變類型的區(qū)別
這里需要注意的一點(diǎn),對(duì)于可變類型和不可變類型,當(dāng)我們?cè)诤瘮?shù)內(nèi)對(duì)其修改后,其是否會(huì)影響到外部變量呢,我們還是可以接著做一個(gè)測(cè)試,這里對(duì)于兩種類型分別進(jìn)行測(cè)試。
先做不可變類型的測(cè)試:
def test_1(a):
print(f"函數(shù)內(nèi)部修改前,a 的地址為: {id(a)}")
a = 2
print(f"函數(shù)內(nèi)部修改后,a 的地址為: {id(a)}")
a = 1
print(f"調(diào)用函數(shù)前,a 的地址為:{id(a)}")
test_1(a)
print(f"函數(shù)外 a 的值是:{a},地址為:{id(a)}")
這里輸出的信息如下:
調(diào)用函數(shù)前,a 的地址為:140713862796072
函數(shù)內(nèi)部修改前,a 的地址為: 140713862796072
函數(shù)內(nèi)部修改后,a 的地址為: 140713862796104
函數(shù)外 a 的值是:1,地址為:140713862796072
在這里可以看到,雖然函數(shù)傳參傳入的是變量的引用,即內(nèi)存地址,但因?yàn)樗遣豢勺冾愋?,所以?duì)其修改后,函數(shù)內(nèi)部相當(dāng)于是對(duì)其重新申請(qǐng)了一個(gè)內(nèi)存地址進(jìn)行操作,但是不會(huì)影響函數(shù)外部原有的內(nèi)存地址。
接下來測(cè)試一下可變數(shù)據(jù)類型:
def test_2(l):
print(f"函數(shù)內(nèi)部修改前,l 的地址為: {id(l)}")
l.append(3)
print(f"函數(shù)內(nèi)部修改后,l 的地址為: {id(l)}")
l = [1, 2]
print(f"調(diào)用函數(shù)前,l 的地址為:{id(l)}")
test_2(l)
print(f"函數(shù)外 l 的值是:{l},地址為:{id(l)}")
其輸出的信息如下:
調(diào)用函數(shù)前,l 的地址為:2116196122176
函數(shù)內(nèi)部修改前,l 的地址為: 2116196122176
函數(shù)內(nèi)部修改后,l 的地址為: 2116196122176
函數(shù)外 l 的值是:[1, 2, 3],地址為:2116196122176
這里可以看到,函數(shù)內(nèi)外 l 變量的地址都是不變的,但因?yàn)槭强勺冾愋?,所以在函?shù)內(nèi)部修改了變量的值以后,并沒有重新分配內(nèi)存,所以在函數(shù)外部 l 變量同步被影響。
那么在函數(shù)內(nèi)部對(duì)傳入的可變類型變量進(jìn)行任何操作都會(huì)影響到函數(shù)外部嗎?
不一定,這里提供一個(gè)示例:
def test_3(l):
print(f"修改變量前,l 的地址為:{id(l)}")
l = l + [3]
print(f"修改變量后,l 的地址為:{id(l)}")
l = [1, 2]
print(f"調(diào)用函數(shù)前,l 的地址為:{id(l)}, 值為:{l}")
test_3(l)
print(f"調(diào)用函數(shù)后,l 的地址為:{id(l)},值為:{l}")
它的輸出的信息如下:
調(diào)用函數(shù)前,l 的地址為:2116183414208, 值為:[1, 2]
修改變量前,l 的地址為:2116183414208
修改變量后,l 的地址為:2116200373376
調(diào)用函數(shù)后,l 的地址為:2116183414208,值為:[1, 2]
可以看到,在函數(shù)內(nèi)部,對(duì)可變類型進(jìn)行了操作之后,它的內(nèi)存地址有所變化,而且修改后不會(huì)影響到原始變量。
這是因?yàn)樵诤瘮?shù)內(nèi)部執(zhí)行的操作是 l = l + [3]
,這個(gè)操作的本質(zhì)并不是直接對(duì)變量的值進(jìn)行修改,而是新建一個(gè)內(nèi)存地址,然后對(duì)這個(gè)變量進(jìn)行重新賦值,所以這個(gè)操作的 l 與函數(shù)傳入的變量 l 已經(jīng)不是同一個(gè)變量了,因此不會(huì)影響到外部的變量。
多說一句,可變類型變量的這個(gè)操作其實(shí)就跟不可變類型的變量的重新賦值是同一個(gè)意義:
a = 1
a = 2
這里其實(shí)也是因?yàn)閷?duì) a 進(jìn)行了新的內(nèi)存空間申請(qǐng),然后重新賦值。
5、淺拷貝、深拷貝
1. 概念
在 Python 中,如果是不可變對(duì)象,比如 string,int 等,變量間的拷貝效果都是一致的,都會(huì)重新獲取一個(gè)內(nèi)存地址,重新賦值,拷貝前后兩個(gè)變量不再相關(guān)。
而如果是可變對(duì)象,比如 list,set,dict 等,就需要區(qū)分淺拷貝和深拷貝。
淺拷貝的操作過程:為新變量重新分配內(nèi)存地址,新變量的元素與原始變量的元素地址還是一致的。
但是如果原始變量的元素是不可變類型,那么修改原始變量或新變量的元素之后,不會(huì)引起兩個(gè)變量的同步變化。
如果修改的是變量元素的可變類型,而可變類型進(jìn)行修改后,其內(nèi)存地址不會(huì)變的,則會(huì)引起兩個(gè)變量的同步變化。
深拷貝的操作過程:為新變量重新分配內(nèi)存地址,創(chuàng)建一個(gè)對(duì)象,如果原始變量的元素中有嵌套的可變類型,那么則會(huì)遞歸的將其中的全部元素都拷貝到新變量,拷貝過程結(jié)束之后,新變量與原始變量沒有任何關(guān)聯(lián),只是簡(jiǎn)單的值相等而已。
上面這兩個(gè)概念可能聽起來比較繞,接下來我們用示例來對(duì)其進(jìn)行展示。
2. 淺拷貝
1) 元素為不可變類型
淺拷貝的操作使用 copy 模塊,引入和使用如下:
import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)
這里使用元素為不可變類型的 dict 進(jìn)行示例展示:
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
可以看到,進(jìn)行淺拷貝后,兩個(gè)變量的內(nèi)存地址是不一樣的,但是內(nèi)部的元素的地址都還是一樣的。
而如果對(duì)其元素的值進(jìn)行更改,因?yàn)樵厥遣豢勺冾愋?,所以更改之后其?nèi)部元素的地址也會(huì)不一樣:
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'])}")
其輸出的內(nèi)容如下:
d1 的值為:{'a': 1, 'b': 2}
d2 的值為:{'a': '2', 'b': 2}
d1 a 元素的地址為:140713862796072
d2 a 元素的地址為:140713862839480
2) 元素為可變類型
當(dāng)需要拷貝的可變對(duì)象的元素也是可變類型的時(shí)候,比如,列表內(nèi)嵌套了列表或者字典,或者字典內(nèi)嵌套了列表或者字典,以及集合的相關(guān)嵌套,對(duì)其進(jìn)行淺拷貝后,因其嵌套的元素是可變類型的,所以在對(duì)內(nèi)部元素進(jìn)行修改后,元素的內(nèi)存地址還是會(huì)指向同一個(gè),所以對(duì)外展示的影響就是,原始變量和新變量會(huì)同步更新數(shù)據(jù)。
接下來我們以字典內(nèi)嵌套列表為例進(jìn)行示例展示:
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'])}")
其輸出內(nèi)容如下:
d1 的地址為:2116201415808, d1 的 b 元素的地址為:2116195489024
d2 的地址為:2116183354816, d2 的 b 元素的地址為:2116195489024
這里可以看到 d1 和 d2 的內(nèi)存地址是不一樣的,但是內(nèi)部的 b 元素的內(nèi)存地址一致。
接下來我們對(duì) d2 的 b 列表進(jìn)行修改,再來看一看兩者的地址和 d1 以及 d2 的值:
d2["b"].append(3)
print(f"d1 的值為:{d1}, d1 的 b 元素的地址為:{id(d1['b'])}")
print(f"d2 的值為:{d2}, d2 的 b 元素的地址為:{id(d2['b'])}")
其輸出內(nèi)容如下:
d1 的值為:{'a': 1, 'b': [1, 2, 3]}, d1 的 b 元素的地址為:2116195489024
d2 的值為:{'a': 1, 'b': [1, 2, 3]}, d2 的 b 元素的地址為:2116195489024
可以看到,對(duì) d2 修改 b 元素的值后,也同步反映到了 d1 上。
總結(jié): 綜上,可以看到,在淺拷貝中,如果元素是不可變對(duì)象,那么修改原始變量或新變量后,不會(huì)引起兩者的同步變化,如果元素是可變對(duì)象,那么修改原始變量或者新變量后,則會(huì)引起兩者的同步變化。
3. 深拷貝
相對(duì)于淺拷貝而言,深拷貝的操作要簡(jiǎn)單許多,不管元素是可變對(duì)象還是不可變對(duì)象,進(jìn)行深拷貝后,原始變量和新變量從外到內(nèi)都是不一樣的內(nèi)存空間,而且修改任意一個(gè)都不會(huì)引起同步變化。
代碼示例如下:
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'])}")
其輸出內(nèi)容如下:
d1 的值為:{'a': 1, 'b': [1, 2]},d1 的 b 元素地址為:2116199853248
d2 的值為:{'a': 1, 'b': [1, 2, 3]},d2 的 b 元素地址為:2116199512896
根據(jù)輸出可以看到,它的內(nèi)容是符合我們前面對(duì)其的解釋的。
4. 總結(jié)
一般來說,如果沒有特殊需求,不需要原始變量與新變量之間有所關(guān)聯(lián)的話,建議使用深拷貝,因?yàn)闇\拷貝的內(nèi)部元素的關(guān)聯(lián)性,在實(shí)際編程中很容易造成數(shù)據(jù)混亂。
以上就是本次 Python 面試知識(shí)的全部?jī)?nèi)容,下一篇將介紹 Python 中的 lambda 表達(dá)式、函數(shù)傳參 args 和 kwargs 以及垃圾回收機(jī)制等。文章來源:http://www.zghlxwxcb.cn/news/detail-844151.html
如果想獲取更多后端相關(guān)文章,可掃碼關(guān)注閱讀:文章來源地址http://www.zghlxwxcb.cn/news/detail-844151.html
到了這里,關(guān)于Python面試必備一之迭代器、生成器、淺拷貝、深拷貝的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!