一份Python面试宝典
Python面試寶典
文章目錄
- Python面試寶典
- 題目001: 在Python中如何實(shí)現(xiàn)單例模式。
- 題目002:不使用中間變量,交換兩個(gè)變量`a`和`b`的值。
- 題目003:寫(xiě)一個(gè)刪除列表中重復(fù)元素的函數(shù),要求去重后元素相對(duì)位置保持不變。
- 題目004:假設(shè)你使用的是官方的CPython,說(shuō)出下面代碼的運(yùn)行結(jié)果。
- 題目005:Lambda函數(shù)是什么,舉例說(shuō)明的它的應(yīng)用場(chǎng)景。
- 題目006:說(shuō)說(shuō)Python中的淺拷貝和深拷貝。
- 題目007:Python是如何實(shí)現(xiàn)內(nèi)存管理的?
- 題目008:說(shuō)一下你對(duì)Python中迭代器和生成器的理解。
- 題目009:正則表達(dá)式的match方法和search方法有什么區(qū)別?
- 題目010:下面這段代碼的執(zhí)行結(jié)果是什么。
- 題目011:Python中為什么沒(méi)有函數(shù)重載?
- 題目012:用Python代碼實(shí)現(xiàn)Python內(nèi)置函數(shù)max。
- 題目013:寫(xiě)一個(gè)函數(shù)統(tǒng)計(jì)傳入的列表中每個(gè)數(shù)字出現(xiàn)的次數(shù)并返回對(duì)應(yīng)的字典。
- 題目014:使用Python代碼實(shí)現(xiàn)遍歷一個(gè)文件夾的操作。
- 題目015:現(xiàn)有2元、3元、5元共三種面額的貨幣,如果需要找零99元,一共有多少種找零的方式?
- 題目016:寫(xiě)一個(gè)函數(shù),給定矩陣的階數(shù)`n`,輸出一個(gè)螺旋式數(shù)字矩陣。
- 題目017:閱讀下面的代碼,寫(xiě)出程序的運(yùn)行結(jié)果。
- 題目018:說(shuō)出下面代碼的運(yùn)行結(jié)果。
- 題目19:說(shuō)說(shuō)你用過(guò)Python標(biāo)準(zhǔn)庫(kù)中的哪些模塊。
- 題目20:`__init__`和`__new__`方法有什么區(qū)別?
- 題目21:輸入年月日,判斷這個(gè)日期是這一年的第幾天。
- 題目22:平常工作中用什么工具進(jìn)行靜態(tài)代碼分析。
- 題目23:說(shuō)一下你知道的Python中的魔術(shù)方法。
- 題目24:函數(shù)參數(shù)`*arg`和`**kwargs`分別代表什么?
- 題目25:寫(xiě)一個(gè)記錄函數(shù)執(zhí)行時(shí)間的裝飾器。
- 題目26:什么是鴨子類(lèi)型(duck typing)?
- 題目27:說(shuō)一下Python中變量的作用域。
- 題目28:說(shuō)一下你對(duì)閉包的理解。
- 題目29:說(shuō)一下Python中的多線(xiàn)程和多進(jìn)程的應(yīng)用場(chǎng)景和優(yōu)缺點(diǎn)。
- 題目30:說(shuō)一下Python 2和Python 3的區(qū)別。
- 題目31:談?wù)勀銓?duì)“猴子補(bǔ)丁”(monkey patching)的理解。
- 題目32:閱讀下面的代碼說(shuō)出運(yùn)行結(jié)果。
- 題目33:編寫(xiě)一個(gè)函數(shù)實(shí)現(xiàn)對(duì)逆波蘭表達(dá)式求值,不能使用Python的內(nèi)置函數(shù)。
- 題目34:Python中如何實(shí)現(xiàn)字符串替換操作?
- 題目35:如何剖析Python代碼的執(zhí)行性能?
- 題目36:如何使用`random`模塊生成隨機(jī)數(shù)、實(shí)現(xiàn)隨機(jī)亂序和隨機(jī)抽樣?
- 題目37:解釋一下線(xiàn)程池的工作原理。
- 題目38:舉例說(shuō)明什么情況下會(huì)出現(xiàn)`KeyError`、`TypeError`、`ValueError`。
- 題目39:說(shuō)出下面代碼的運(yùn)行結(jié)果。
- 題目40:如何讀取大文件,例如內(nèi)存只有4G,如何讀取一個(gè)大小為8G的文件?
- 題目41:說(shuō)一下你對(duì)Python中模塊和包的理解。
- 題目42:說(shuō)一下你知道的Python編碼規(guī)范。
- 題目43:運(yùn)行下面的代碼是否會(huì)報(bào)錯(cuò),如果報(bào)錯(cuò)請(qǐng)說(shuō)明哪里有什么樣的錯(cuò),如果不報(bào)錯(cuò)請(qǐng)說(shuō)出代碼的執(zhí)行結(jié)果。
- 題目44:對(duì)下面給出的字典按值從大到小對(duì)鍵進(jìn)行排序。
- 題目45:說(shuō)一下`namedtuple`的用法和作用。
- 題目46:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
- 題目47:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
- 題目48:按照題目要求寫(xiě)出對(duì)應(yīng)的裝飾器。
- 題目49:寫(xiě)一個(gè)函數(shù)實(shí)現(xiàn)字符串反轉(zhuǎn),盡可能寫(xiě)出你知道的所有方法。
- 題目50:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
題目001: 在Python中如何實(shí)現(xiàn)單例模式。
點(diǎn)評(píng):單例模式是指讓一個(gè)類(lèi)只能創(chuàng)建出唯一的實(shí)例,這個(gè)題目在面試中出現(xiàn)的頻率極高,因?yàn)樗疾斓牟粌H僅是單例模式,更是對(duì)Python語(yǔ)言到底掌握到何種程度,建議大家用裝飾器和元類(lèi)這兩種方式來(lái)實(shí)現(xiàn)單例模式,因?yàn)檫@兩種方式的通用性最強(qiáng),而且也可以順便展示自己對(duì)裝飾器和元類(lèi)中兩個(gè)關(guān)鍵知識(shí)點(diǎn)的理解。
方法一:使用裝飾器實(shí)現(xiàn)單例模式。
from functools import wrapsdef singleton(cls):"""單例類(lèi)裝飾器"""instances = {}@wraps(cls)def wrapper(*args, **kwargs):if cls not in instances:instances[cls] = cls(*args, **kwargs)return instances[cls]return wrapper@singleton class President:pass擴(kuò)展:裝飾器是Python中非常有特色的語(yǔ)法,用一個(gè)函數(shù)去裝飾另一個(gè)函數(shù)或類(lèi),為其添加額外的能力。通常通過(guò)裝飾來(lái)實(shí)現(xiàn)的功能都屬橫切關(guān)注功能,也就是跟正常的業(yè)務(wù)邏輯沒(méi)有必然聯(lián)系,可以動(dòng)態(tài)添加或移除的功能。裝飾器可以為代碼提供緩存、代理、上下文環(huán)境等服務(wù),它是對(duì)設(shè)計(jì)模式中代理模式的踐行。在寫(xiě)裝飾器的時(shí)候,帶裝飾功能的函數(shù)(上面代碼中的wrapper函數(shù))通常都會(huì)用functools模塊中的wraps再加以裝飾,這個(gè)裝飾器最重要的作用是給被裝飾的類(lèi)或函數(shù)動(dòng)態(tài)添加一個(gè)__wrapped__屬性,這個(gè)屬性會(huì)將被裝飾之前的類(lèi)或函數(shù)保留下來(lái),這樣在我們不需要裝飾功能的時(shí)候,可以通過(guò)它來(lái)取消裝飾器,例如可以使用President = President.__wrapped__來(lái)取消對(duì)President類(lèi)做的單例處理。需要提醒大家的是:上面的單例并不是線(xiàn)程安全的,如果要做到線(xiàn)程安全,需要對(duì)創(chuàng)建對(duì)象的代碼進(jìn)行加鎖的處理。在Python中可以使用threading模塊的RLock對(duì)象來(lái)提供鎖,可以使用鎖對(duì)象的acquire和release方法來(lái)實(shí)現(xiàn)加鎖和解鎖的操作。當(dāng)然,更為簡(jiǎn)便的做法是使用鎖對(duì)象的with上下文語(yǔ)法來(lái)進(jìn)行隱式的加鎖和解鎖操作。
方法二:使用元類(lèi)實(shí)現(xiàn)單例模式。
class SingletonMeta(type):"""自定義單例元類(lèi)"""def __init__(cls, *args, **kwargs):cls.__instance = Nonesuper().__init__(*args, **kwargs)def __call__(cls, *args, **kwargs):if cls.__instance is None:cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass President(metaclass=SingletonMeta):pass擴(kuò)展:Python是面向?qū)ο蟮木幊陶Z(yǔ)言,在面向?qū)ο蟮氖澜缰?#xff0c;一切皆為對(duì)象。對(duì)象是通過(guò)類(lèi)來(lái)創(chuàng)建的,而類(lèi)本身也是對(duì)象,類(lèi)這樣的對(duì)象是通過(guò)元類(lèi)來(lái)創(chuàng)建的。我們?cè)诙x類(lèi)時(shí),如果沒(méi)有給一個(gè)類(lèi)指定父類(lèi),那么默認(rèn)的父類(lèi)是object,如果沒(méi)有給一個(gè)類(lèi)指定元類(lèi),那么默認(rèn)的元類(lèi)是type。通過(guò)自定義的元類(lèi),我們可以改變一個(gè)類(lèi)默認(rèn)的行為,就如同上面的代碼中,我們通過(guò)元類(lèi)的__call__魔術(shù)方法,改變了President類(lèi)的構(gòu)造器那樣。
補(bǔ)充:關(guān)于單例模式,在面試中還有可能被問(wèn)到它的應(yīng)用場(chǎng)景。通常一個(gè)對(duì)象的狀態(tài)是被其他對(duì)象共享的,就可以將其設(shè)計(jì)為單例,例如項(xiàng)目中使用的數(shù)據(jù)庫(kù)連接池對(duì)象和配置對(duì)象通常都是單例,這樣才能保證所有地方獲取到的數(shù)據(jù)庫(kù)連接和配置信息是完全一致的;而且由于對(duì)象只有唯一的實(shí)例,因此從根本上避免了重復(fù)創(chuàng)建對(duì)象造成的時(shí)間和空間上的開(kāi)銷(xiāo),也避免了對(duì)資源的多重占用。再舉個(gè)例子,項(xiàng)目中的日志操作通常也會(huì)使用單例模式,這是因?yàn)楣蚕淼娜罩疚募恢碧幱诖蜷_(kāi)狀態(tài),只能有一個(gè)實(shí)例去操作它,否則在寫(xiě)入日志的時(shí)候會(huì)產(chǎn)生混亂。
題目002:不使用中間變量,交換兩個(gè)變量a和b的值。
點(diǎn)評(píng):典型的送人頭的題目,通常交換兩個(gè)變量需要借助一個(gè)中間變量,如果不允許使用中間變量,在其他編程語(yǔ)言中可以使用異或運(yùn)算的方式來(lái)實(shí)現(xiàn)交換兩個(gè)變量的值,但是Python中有更為簡(jiǎn)單明了的做法。
方法一:
a = a ^ b b = a ^ b a = a ^ b方法二:
a, b = b, a擴(kuò)展:需要注意,a, b = b, a這種做法其實(shí)并不是元組解包,雖然很多人都這樣認(rèn)為。Python字節(jié)碼指令中有ROT_TWO指令來(lái)支持這個(gè)操作,類(lèi)似的還有ROT_THREE,對(duì)于3個(gè)以上的元素,如a, b, c, d = b, c, d, a,才會(huì)用到創(chuàng)建元組和元組解包。想知道你的代碼對(duì)應(yīng)的字節(jié)碼指令,可以使用Python標(biāo)準(zhǔn)庫(kù)中dis模塊的dis函數(shù)來(lái)反匯編你的Python代碼。
題目003:寫(xiě)一個(gè)刪除列表中重復(fù)元素的函數(shù),要求去重后元素相對(duì)位置保持不變。
點(diǎn)評(píng):這個(gè)題目在初中級(jí)Python崗位面試的時(shí)候經(jīng)常出現(xiàn),題目源于《Python Cookbook》這本書(shū)第一章的第10個(gè)問(wèn)題,有很多面試題其實(shí)都是這本書(shū)上的原題,所以建議大家有時(shí)間好好研讀一下這本書(shū)。
def dedup(items):no_dup_items = []seen = set()for item in items:if item not in seen:no_dup_items.append(item)seen.add(item)return no_dup_items如果愿意也可以把上面的函數(shù)改造成一個(gè)生成器,代碼如下所示。
def dedup(items):seen = set()for item in items:if item not in seen:yield itemseen.add(item)擴(kuò)展:由于Python中的集合底層使用哈希存儲(chǔ),所以集合的in和not in成員運(yùn)算在性能上遠(yuǎn)遠(yuǎn)優(yōu)于列表,所以上面的代碼我們使用了集合來(lái)保存已經(jīng)出現(xiàn)過(guò)的元素。集合中的元素必須是hashable對(duì)象,因此上面的代碼在列表元素不是hashable對(duì)象時(shí)會(huì)失效,要解決這個(gè)問(wèn)題可以給函數(shù)增加一個(gè)參數(shù),該參數(shù)可以設(shè)計(jì)為返回哈希碼或hashable對(duì)象的函數(shù)。
題目004:假設(shè)你使用的是官方的CPython,說(shuō)出下面代碼的運(yùn)行結(jié)果。
點(diǎn)評(píng):下面的程序?qū)?shí)際開(kāi)發(fā)并沒(méi)有什么意義,但卻是CPython中的一個(gè)大坑,這道題旨在考察面試者對(duì)官方的Python解釋器到底了解到什么程度。
a, b, c, d = 1, 1, 1000, 1000 print(a is b, c is d)def foo():e = 1000f = 1000print(e is f, e is d)g = 1print(g is a)foo()運(yùn)行結(jié)果:
True False True False True上面代碼中a is b的結(jié)果是True但c is d的結(jié)果是False,這一點(diǎn)的確讓人費(fèi)解。CPython解釋器出于性能優(yōu)化的考慮,把頻繁使用的整數(shù)對(duì)象用一個(gè)叫small_ints的對(duì)象池緩存起來(lái)造成的。small_ints緩存的整數(shù)值被設(shè)定為[-5, 256]這個(gè)區(qū)間,也就是說(shuō),在任何引用這些整數(shù)的地方,都不需要重新創(chuàng)建int對(duì)象,而是直接引用緩存池中的對(duì)象。如果整數(shù)不在該范圍內(nèi),那么即便兩個(gè)整數(shù)的值相同,它們也是不同的對(duì)象。
CPython底層為了進(jìn)一步提升性能還做了另一個(gè)設(shè)定,對(duì)于同一個(gè)代碼塊中值不在small_ints緩存范圍內(nèi)的整數(shù),如果同一個(gè)代碼塊中已經(jīng)存在一個(gè)值與其相同的整數(shù)對(duì)象,那么就直接引用該對(duì)象,否則創(chuàng)建新的int對(duì)象。需要大家注意的是,這條規(guī)則對(duì)數(shù)值型適用,但對(duì)字符串則需要考慮字符串的長(zhǎng)度,這一點(diǎn)大家可以自行證明。
擴(kuò)展:如果你用PyPy(另一種Python解釋器實(shí)現(xiàn),支持JIT,對(duì)CPython的缺點(diǎn)進(jìn)行了改良,在性能上優(yōu)于CPython,但對(duì)三方庫(kù)的支持略差)來(lái)運(yùn)行上面的代碼,你會(huì)發(fā)現(xiàn)所有的輸出都是True。
題目005:Lambda函數(shù)是什么,舉例說(shuō)明的它的應(yīng)用場(chǎng)景。
點(diǎn)評(píng):這個(gè)題目主要想考察的是Lambda函數(shù)的應(yīng)用場(chǎng)景,潛臺(tái)詞是問(wèn)你在項(xiàng)目中有沒(méi)有使用過(guò)Lambda函數(shù),具體在什么場(chǎng)景下會(huì)用到Lambda函數(shù),借此來(lái)判斷你寫(xiě)代碼的能力。因?yàn)長(zhǎng)ambda函數(shù)通常用在高階函數(shù)中,主要的作用是通過(guò)向函數(shù)傳入函數(shù)或讓函數(shù)返回函數(shù)最終實(shí)現(xiàn)代碼的解耦合。
Lambda函數(shù)也叫匿名函數(shù),它是功能簡(jiǎn)單用一行代碼就能實(shí)現(xiàn)的小型函數(shù)。Python中的Lambda函數(shù)只能寫(xiě)一個(gè)表達(dá)式,這個(gè)表達(dá)式的執(zhí)行結(jié)果就是函數(shù)的返回值,不用寫(xiě)return關(guān)鍵字。Lambda函數(shù)因?yàn)闆](méi)有名字,所以也不會(huì)跟其他函數(shù)發(fā)生命名沖突的問(wèn)題。
擴(kuò)展:面試的時(shí)候有可能還會(huì)考你用Lambda函數(shù)來(lái)實(shí)現(xiàn)一些功能,也就是用一行代碼來(lái)實(shí)現(xiàn)題目要求的功能,例如:用一行代碼實(shí)現(xiàn)求階乘的函數(shù),用一行代碼實(shí)現(xiàn)求最大公約數(shù)的函數(shù)等。
fac = lambda x: __import__('functools').reduce(int.__mul__, range(1, x + 1), 1) gcd = lambda x, y: y % x and gcd(y % x, x) or xLambda函數(shù)其實(shí)最為主要的用途是把一個(gè)函數(shù)傳入另一個(gè)高階函數(shù)(如Python內(nèi)置的filter、map等)中來(lái)為函數(shù)做解耦合,增強(qiáng)函數(shù)的靈活性和通用性。下面的例子通過(guò)使用filter和map函數(shù),實(shí)現(xiàn)了從列表中篩選出奇數(shù)并求平方構(gòu)成新列表的操作,因?yàn)橛玫搅烁唠A函數(shù),過(guò)濾和映射數(shù)據(jù)的規(guī)則都是函數(shù)的調(diào)用者通過(guò)另外一個(gè)函數(shù)傳入的,因此這filter和map函數(shù)沒(méi)有跟特定的過(guò)濾和映射數(shù)據(jù)的規(guī)則耦合在一起。
items = [12, 5, 7, 10, 8, 19] items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items))) print(items) # [25, 49, 361]擴(kuò)展:用列表的生成式來(lái)實(shí)現(xiàn)上面的代碼會(huì)更加簡(jiǎn)單明了,代碼如下所示。
items = [12, 5, 7, 10, 8, 19] items = [x ** 2 for x in items if x % 2] print(items) # [25, 49, 361]題目006:說(shuō)說(shuō)Python中的淺拷貝和深拷貝。
點(diǎn)評(píng):這個(gè)題目本身出現(xiàn)的頻率非常高,但是就題論題而言沒(méi)有什么技術(shù)含量。對(duì)于這種面試題,在回答的時(shí)候一定要讓你的答案能夠超出面試官的預(yù)期,這樣才能獲得更好的印象分。所以回答這個(gè)題目的要點(diǎn)不僅僅是能夠說(shuō)出淺拷貝和深拷貝的區(qū)別,深拷貝的時(shí)候可能遇到的兩大問(wèn)題,還要說(shuō)出Python標(biāo)準(zhǔn)庫(kù)對(duì)淺拷貝和深拷貝的支持,然后可以說(shuō)說(shuō)列表、字典如何實(shí)現(xiàn)拷貝操作以及如何通過(guò)序列化和反序列的方式實(shí)現(xiàn)深拷貝,最后還可以提到設(shè)計(jì)模式中的原型模式以及它在項(xiàng)目中的應(yīng)用。
淺拷貝通常只復(fù)制對(duì)象本身,而深拷貝不僅會(huì)復(fù)制對(duì)象,還會(huì)遞歸的復(fù)制對(duì)象所關(guān)聯(lián)的對(duì)象。深拷貝可能會(huì)遇到兩個(gè)問(wèn)題:一是一個(gè)對(duì)象如果直接或間接的引用了自身,會(huì)導(dǎo)致無(wú)休止的遞歸拷貝;二是深拷貝可能對(duì)原本設(shè)計(jì)為多個(gè)對(duì)象共享的數(shù)據(jù)也進(jìn)行拷貝。Python通過(guò)copy模塊中的copy和deepcopy函數(shù)來(lái)實(shí)現(xiàn)淺拷貝和深拷貝操作,其中deepcopy可以通過(guò)memo字典來(lái)保存已經(jīng)拷貝過(guò)的對(duì)象,從而避免剛才所說(shuō)的自引用遞歸問(wèn)題;此外,可以通過(guò)copyreg模塊的pickle函數(shù)來(lái)定制指定類(lèi)型對(duì)象的拷貝行為。
deepcopy函數(shù)的本質(zhì)其實(shí)就是對(duì)象的一次序列化和一次返回序列化,面試題中還考過(guò)用自定義函數(shù)實(shí)現(xiàn)對(duì)象的深拷貝操作,顯然我們可以使用pickle模塊的dumps和loads來(lái)做到,代碼如下所示。
import picklemy_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))列表的切片操作[:]相當(dāng)于實(shí)現(xiàn)了列表對(duì)象的淺拷貝,而字典的copy方法可以實(shí)現(xiàn)字典對(duì)象的淺拷貝。對(duì)象拷貝其實(shí)是更為快捷的創(chuàng)建對(duì)象的方式。在Python中,通過(guò)構(gòu)造器創(chuàng)建對(duì)象屬于兩階段構(gòu)造,首先是分配內(nèi)存空間,然后是初始化。在創(chuàng)建對(duì)象時(shí),我們也可以基于“原型”對(duì)象來(lái)創(chuàng)建新對(duì)象,通過(guò)對(duì)原型對(duì)象的拷貝(復(fù)制內(nèi)存)就完成了對(duì)象的創(chuàng)建和初始化,這種做法更加高效,這也就是設(shè)計(jì)模式中的原型模式。在Python中,我們可以通過(guò)元類(lèi)的方式來(lái)實(shí)現(xiàn)原型模式,代碼如下所示。
import copyclass PrototypeMeta(type):"""實(shí)現(xiàn)原型模式的元類(lèi)"""def __init__(cls, *args, **kwargs):super().__init__(*args, **kwargs)# 為對(duì)象綁定clone方法來(lái)實(shí)現(xiàn)對(duì)象拷貝cls.clone = lambda self, is_deep=True: \copy.deepcopy(self) if is_deep else copy.copy(self)class Person(metaclass=PrototypeMeta):passp1 = Person() p2 = p1.clone() # 深拷貝 p3 = p1.clone(is_deep=False) # 淺拷貝題目007:Python是如何實(shí)現(xiàn)內(nèi)存管理的?
點(diǎn)評(píng):當(dāng)面試官問(wèn)到這個(gè)問(wèn)題的時(shí)候,一個(gè)展示自己的機(jī)會(huì)就擺在面前了。你要先反問(wèn)面試官:“你說(shuō)的是官方的CPython解釋器嗎?”。這個(gè)反問(wèn)可以展示出你了解過(guò)Python解釋器的不同的實(shí)現(xiàn)版本,而且你也知道面試官想問(wèn)的是CPython。當(dāng)然,很多面試官對(duì)不同的Python解釋器底層實(shí)現(xiàn)到底有什么差別也沒(méi)有概念。所以,千萬(wàn)不要覺(jué)得面試官一定比你強(qiáng),懷揣著這份自信可以讓你更好的完成面試。
Python提供了自動(dòng)化的內(nèi)存管理,也就是說(shuō)內(nèi)存空間的分配與釋放都是由Python解釋器在運(yùn)行時(shí)自動(dòng)進(jìn)行的,自動(dòng)管理內(nèi)存功能極大的減輕程序員的工作負(fù)擔(dān),也能夠幫助程序員在一定程度上解決內(nèi)存泄露的問(wèn)題。以CPython解釋器為例,它的內(nèi)存管理有三個(gè)關(guān)鍵點(diǎn):引用計(jì)數(shù)、標(biāo)記清理、分代收集。
引用計(jì)數(shù):對(duì)于CPython解釋器來(lái)說(shuō),Python中的每一個(gè)對(duì)象其實(shí)就是PyObject結(jié)構(gòu)體,它的內(nèi)部有一個(gè)名為ob_refcnt 的引用計(jì)數(shù)器成員變量。程序在運(yùn)行的過(guò)程中ob_refcnt的值會(huì)被更新并藉此來(lái)反映引用有多少個(gè)變量引用到該對(duì)象。當(dāng)對(duì)象的引用計(jì)數(shù)值為0時(shí),它的內(nèi)存就會(huì)被釋放掉。
typedef struct _object {_PyObject_HEAD_EXTRAPy_ssize_t ob_refcnt;struct _typeobject *ob_type; } PyObject;以下情況會(huì)導(dǎo)致引用計(jì)數(shù)加1:
- 對(duì)象被創(chuàng)建
- 對(duì)象被引用
- 對(duì)象作為參數(shù)傳入到一個(gè)函數(shù)中
- 對(duì)象作為元素存儲(chǔ)到一個(gè)容器中
以下情況會(huì)導(dǎo)致引用計(jì)數(shù)減1:
- 用del語(yǔ)句顯示刪除對(duì)象引用
- 對(duì)象引用被重新賦值其他對(duì)象
- 一個(gè)對(duì)象離開(kāi)它所在的作用域
- 持有該對(duì)象的容器自身被銷(xiāo)毀
- 持有該對(duì)象的容器刪除該對(duì)象
可以通過(guò)sys模塊的getrefcount函數(shù)來(lái)獲得對(duì)象的引用計(jì)數(shù)。引用計(jì)數(shù)的內(nèi)存管理方式在遇到循環(huán)引用的時(shí)候就會(huì)出現(xiàn)致命傷,因此需要其他的垃圾回收算法對(duì)其進(jìn)行補(bǔ)充。
標(biāo)記清理:CPython使用了“標(biāo)記-清理”(Mark and Sweep)算法解決容器類(lèi)型可能產(chǎn)生的循環(huán)引用問(wèn)題。該算法在垃圾回收時(shí)分為兩個(gè)階段:標(biāo)記階段,遍歷所有的對(duì)象,如果對(duì)象是可達(dá)的(被其他對(duì)象引用),那么就標(biāo)記該對(duì)象為可達(dá);清除階段,再次遍歷對(duì)象,如果發(fā)現(xiàn)某個(gè)對(duì)象沒(méi)有標(biāo)記為可達(dá),則就將其回收。CPython底層維護(hù)了兩個(gè)雙端鏈表,一個(gè)鏈表存放著需要被掃描的容器對(duì)象(姑且稱(chēng)之為鏈表A),另一個(gè)鏈表存放著臨時(shí)不可達(dá)對(duì)象(姑且稱(chēng)之為鏈表B)。為了實(shí)現(xiàn)“標(biāo)記-清理”算法,鏈表中的每個(gè)節(jié)點(diǎn)除了有記錄當(dāng)前引用計(jì)數(shù)的ref_count變量外,還有一個(gè)gc_ref變量,這個(gè)gc_ref是ref_count的一個(gè)副本,所以初始值為ref_count的大小。執(zhí)行垃圾回收時(shí),首先遍歷鏈表A中的節(jié)點(diǎn),并且將當(dāng)前對(duì)象所引用的所有對(duì)象的gc_ref減1,這一步主要作用是解除循環(huán)引用對(duì)引用計(jì)數(shù)的影響。再次遍歷鏈表A中的節(jié)點(diǎn),如果節(jié)點(diǎn)的gc_ref值為0,那么這個(gè)對(duì)象就被標(biāo)記為“暫時(shí)不可達(dá)”(GC_TENTATIVELY_UNREACHABLE)并被移動(dòng)到鏈表B中;如果節(jié)點(diǎn)的gc_ref不為0,那么這個(gè)對(duì)象就會(huì)被標(biāo)記為“可達(dá)“(GC_REACHABLE),對(duì)于”可達(dá)“對(duì)象,還要遞歸的將該節(jié)點(diǎn)可以到達(dá)的節(jié)點(diǎn)標(biāo)記為”可達(dá)“;鏈表B中被標(biāo)記為”可達(dá)“的節(jié)點(diǎn)要重新放回到鏈表A中。在兩次遍歷之后,鏈表B中的節(jié)點(diǎn)就是需要釋放內(nèi)存的節(jié)點(diǎn)。
分代回收:在循環(huán)引用對(duì)象的回收中,整個(gè)應(yīng)用程序會(huì)被暫停,為了減少應(yīng)用程序暫停的時(shí)間,Python 通過(guò)分代回收(空間換時(shí)間)的方法提高垃圾回收效率。分代回收的基本思想是:對(duì)象存在的時(shí)間越長(zhǎng),是垃圾的可能性就越小,應(yīng)該盡量不對(duì)這樣的對(duì)象進(jìn)行垃圾回收。CPython將對(duì)象分為三種世代分別記為0、1、2,每一個(gè)新生對(duì)象都在第0代中,如果該對(duì)象在一輪垃圾回收掃描中存活下來(lái),那么它將被移到第1代中,存在于第1代的對(duì)象將較少的被垃圾回收掃描到;如果在對(duì)第1代進(jìn)行垃圾回收掃描時(shí),這個(gè)對(duì)象又存活下來(lái),那么它將被移至第2代中,在那里它被垃圾回收掃描的次數(shù)將會(huì)更少。分代回收掃描的門(mén)限值可以通過(guò)gc模塊的get_threshold函數(shù)來(lái)獲得,該函數(shù)返回一個(gè)三元組,分別表示多少次內(nèi)存分配操作后會(huì)執(zhí)行0代垃圾回收,多少次0代垃圾回收后會(huì)執(zhí)行1代垃圾回收,多少次1代垃圾回收后會(huì)執(zhí)行2代垃圾回收。需要說(shuō)明的是,如果執(zhí)行一次2代垃圾回收,那么比它年輕的代都要執(zhí)行垃圾回收。如果想修改這幾個(gè)門(mén)限值,可以通過(guò)gc模塊的set_threshold函數(shù)來(lái)做到。
題目008:說(shuō)一下你對(duì)Python中迭代器和生成器的理解。
點(diǎn)評(píng):很多人面試者都會(huì)寫(xiě)迭代器和生成器,但是卻無(wú)法準(zhǔn)確的解釋什么是迭代器和生成器。如果你也有同樣的困惑,可以參考下面的回答。
迭代器是實(shí)現(xiàn)了迭代器協(xié)議的對(duì)象。跟其他編程語(yǔ)言不通,Python中沒(méi)有用于定義協(xié)議或表示約定的關(guān)鍵字,像interface、protocol這些單詞并不在Python語(yǔ)言的關(guān)鍵字列表中。Python語(yǔ)言通過(guò)魔法方法來(lái)表示約定,也就是我們所說(shuō)的協(xié)議,而__next__和__iter__這兩個(gè)魔法方法就代表了迭代器協(xié)議。可以通過(guò)for-in循環(huán)從迭代器對(duì)象中取出值,也可以使用next函數(shù)取出迭代器對(duì)象中的下一個(gè)值。生成器是迭代器的語(yǔ)法升級(jí)版本,可以用更為簡(jiǎn)單的代碼來(lái)實(shí)現(xiàn)一個(gè)迭代器。
擴(kuò)展:面試中經(jīng)常讓寫(xiě)生成斐波那契數(shù)列的迭代器,大家可以參考下面的代碼。
class Fib(object):def __init__(self, num):self.num = numself.a, self.b = 0, 1self.idx = 0def __iter__(self):return selfdef __next__(self):if self.idx < self.num:self.a, self.b = self.b, self.a + self.bself.idx += 1return self.araise StopIteration()如果用生成器的語(yǔ)法來(lái)改寫(xiě)上面的代碼,代碼會(huì)簡(jiǎn)單優(yōu)雅很多。
def fib(num):a, b = 0, 1for _ in range(num):a, b = b, a + byield a題目009:正則表達(dá)式的match方法和search方法有什么區(qū)別?
點(diǎn)評(píng):正則表達(dá)式是字符串處理的重要工具,所以也是面試中經(jīng)常考察的知識(shí)點(diǎn)。在Python中,使用正則表達(dá)式有兩種方式,一種是直接調(diào)用re模塊中的函數(shù),傳入正則表達(dá)式和需要處理的字符串;一種是先通過(guò)re模塊的compile函數(shù)創(chuàng)建正則表達(dá)式對(duì)象,然后再通過(guò)對(duì)象調(diào)用方法并傳入需要處理的字符串。如果一個(gè)正則表達(dá)式被頻繁的使用,我們推薦用re.compile函數(shù)創(chuàng)建正則表達(dá)式對(duì)象,這樣會(huì)減少頻繁編譯同一個(gè)正則表達(dá)式所造成的開(kāi)銷(xiāo)。
match方法是從字符串的起始位置進(jìn)行正則表達(dá)式匹配,返回Match對(duì)象或None。search方法會(huì)掃描整個(gè)字符串來(lái)找尋匹配的模式,同樣也是返回Match對(duì)象或None。
題目010:下面這段代碼的執(zhí)行結(jié)果是什么。
def multiply():return [lambda x: i * x for i in range(4)]print([m(100) for m in multiply()])運(yùn)行結(jié)果:
[300, 300, 300, 300]上面代碼的運(yùn)行結(jié)果很容易被誤判為[0, 100, 200, 300]。首先需要注意的是multiply函數(shù)用生成式語(yǔ)法返回了一個(gè)列表,列表中保存了4個(gè)Lambda函數(shù),這4個(gè)Lambda函數(shù)會(huì)返回傳入的參數(shù)乘以i的結(jié)果。需要注意的是這里有閉包(closure)現(xiàn)象,multiply函數(shù)中的局部變量i的生命周期被延展了,由于i最終的值是3,所以通過(guò)m(100)調(diào)列表中的Lambda函數(shù)時(shí)會(huì)返回300,而且4個(gè)調(diào)用都是如此。
如果想得到[0, 100, 200, 300]這個(gè)結(jié)果,可以按照下面幾種方式來(lái)修改multiply函數(shù)。
方法一:使用生成器,讓函數(shù)獲得i的當(dāng)前值。
def multiply():return (lambda x: i * x for i in range(4))print([m(100) for m in multiply()])或者
def multiply():for i in range(4):yield lambda x: x * iprint([m(100) for m in multiply()])方法二:使用偏函數(shù),徹底避開(kāi)閉包。
from functools import partial from operator import __mul__def multiply():return [partial(__mul__, i) for i in range(4)]print([m(100) for m in multiply()])題目011:Python中為什么沒(méi)有函數(shù)重載?
點(diǎn)評(píng):C++、Java、C#等諸多編程語(yǔ)言都支持函數(shù)重載,所謂函數(shù)重載指的是在同一個(gè)作用域中有多個(gè)同名函數(shù),它們擁有不同的參數(shù)列表(參數(shù)個(gè)數(shù)不同或參數(shù)類(lèi)型不同或二者皆不同),可以相互區(qū)分。重載也是一種多態(tài)性,因?yàn)橥ǔJ窃诰幾g時(shí)通過(guò)參數(shù)的個(gè)數(shù)和類(lèi)型來(lái)確定到底調(diào)用哪個(gè)重載函數(shù),所以也被稱(chēng)為編譯時(shí)多態(tài)性或者叫前綁定。這個(gè)問(wèn)題的潛臺(tái)詞其實(shí)是問(wèn)面試者是否有其他編程語(yǔ)言的經(jīng)驗(yàn),是否理解Python是動(dòng)態(tài)類(lèi)型語(yǔ)言,是否知道Python中函數(shù)的可變參數(shù)、關(guān)鍵字參數(shù)這些概念。
首先Python是解釋型語(yǔ)言,函數(shù)重載現(xiàn)象通常出現(xiàn)在編譯型語(yǔ)言中。其次Python是動(dòng)態(tài)類(lèi)型語(yǔ)言,函數(shù)的參數(shù)沒(méi)有類(lèi)型約束,也就無(wú)法根據(jù)參數(shù)類(lèi)型來(lái)區(qū)分重載。再者Python中函數(shù)的參數(shù)可以有默認(rèn)值,可以使用可變參數(shù)和關(guān)鍵字參數(shù),因此即便沒(méi)有函數(shù)重載,也要可以讓一個(gè)函數(shù)根據(jù)調(diào)用者傳入的參數(shù)產(chǎn)生不同的行為。
題目012:用Python代碼實(shí)現(xiàn)Python內(nèi)置函數(shù)max。
點(diǎn)評(píng):這個(gè)題目看似簡(jiǎn)單,但實(shí)際上還是比較考察面試者的功底。因?yàn)镻ython內(nèi)置的max函數(shù)既可以傳入可迭代對(duì)象找出最大,又可以傳入兩個(gè)或多個(gè)參數(shù)找出最大;最為關(guān)鍵的是還可以通過(guò)命名關(guān)鍵字參數(shù)key來(lái)指定一個(gè)用于元素比較的函數(shù),還可以通過(guò)default命名關(guān)鍵字參數(shù)來(lái)指定當(dāng)可迭代對(duì)象為空時(shí)返回的默認(rèn)值。
下面的代碼僅供參考:
def my_max(*args, key=None, default=None):"""獲取可迭代對(duì)象中最大的元素或兩個(gè)及以上實(shí)參中最大的元素:param args: 一個(gè)可迭代對(duì)象或多個(gè)元素:param key: 提取用于元素比較的特征值的函數(shù),默認(rèn)為None:param default: 如果可迭代對(duì)象為空則返回該默認(rèn)值,如果沒(méi)有給默認(rèn)值則引發(fā)ValueError異常:return: 返回可迭代對(duì)象或多個(gè)元素中的最大元素"""if len(args) == 1 and len(args[0]) == 0:if default:return defaultelse:raise ValueError('max() arg is an empty sequence')items = args[0] if len(args) == 1 else argsmax_elem, max_value = items[0], items[0]if key:max_value = key(max_value)for item in items:value = itemif key:value = key(item)if value > max_value:max_elem, max_value = item, valuereturn max_elem題目013:寫(xiě)一個(gè)函數(shù)統(tǒng)計(jì)傳入的列表中每個(gè)數(shù)字出現(xiàn)的次數(shù)并返回對(duì)應(yīng)的字典。
點(diǎn)評(píng):送人頭的題目,不解釋。
def count_letters(items):result = {}for item in items:if isinstance(item, (int, float)):result[item] = result.get(item, 0) + 1return result也可以直接使用Python標(biāo)準(zhǔn)庫(kù)中collections模塊的Counter類(lèi)來(lái)解決這個(gè)問(wèn)題,Counter是dict的子類(lèi),它會(huì)將傳入的序列中的每個(gè)元素作為鍵,元素出現(xiàn)的次數(shù)作為值來(lái)構(gòu)造字典。
from collections import Counterdef count_letters(items):counter = Counter(items)return {key: value for key, value in counter.items() \if isinstance(key, (int, float))}題目014:使用Python代碼實(shí)現(xiàn)遍歷一個(gè)文件夾的操作。
點(diǎn)評(píng):基本也是送人頭的題目,只要用過(guò)os模塊就應(yīng)該知道怎么做。
Python標(biāo)準(zhǔn)庫(kù)os模塊的walk函數(shù)提供了遍歷一個(gè)文件夾的功能,它返回一個(gè)生成器。
import osg = os.walk('/Users/Hao/Downloads/') for path, dir_list, file_list in g:for dir_name in dir_list:print(os.path.join(path, dir_name))for file_name in file_list:print(os.path.join(path, file_name))說(shuō)明:os.path模塊提供了很多進(jìn)行路徑操作的工具函數(shù),在項(xiàng)目開(kāi)發(fā)中也是經(jīng)常會(huì)用到的。如果題目明確要求不能使用os.walk函數(shù),那么可以使用os.listdir函數(shù)來(lái)獲取指定目錄下的文件和文件夾,然后再通過(guò)循環(huán)遍歷用os.isdir函數(shù)判斷哪些是文件夾,對(duì)于文件夾可以通過(guò)遞歸調(diào)用進(jìn)行遍歷,這樣也可以實(shí)現(xiàn)遍歷一個(gè)文件夾的操作。
題目015:現(xiàn)有2元、3元、5元共三種面額的貨幣,如果需要找零99元,一共有多少種找零的方式?
點(diǎn)評(píng):還有一個(gè)非常類(lèi)似的題目:“一個(gè)小朋友走樓梯,一次可以走1個(gè)臺(tái)階、2個(gè)臺(tái)階或3個(gè)臺(tái)階,問(wèn)走完10個(gè)臺(tái)階一共有多少種走法?”,這兩個(gè)題目的思路是一樣,如果用遞歸函數(shù)來(lái)寫(xiě)的話(huà)非常簡(jiǎn)單。
from functools import lru_cache@lru_cache() def change_money(total):if total == 0:return 1if total < 0:return 0return change_money(total - 2) + change_money(total - 3) + \change_money(total - 5)說(shuō)明:在上面的代碼中,我們用lru_cache裝飾器裝飾了遞歸函數(shù)change_money,如果不做這個(gè)優(yōu)化,上面代碼的漸近時(shí)間復(fù)雜度將會(huì)是O(3N)O(3^N)O(3N),而如果參數(shù)total的值是99,這個(gè)運(yùn)算量是非常巨大的。lru_cache裝飾器會(huì)緩存函數(shù)的執(zhí)行結(jié)果,這樣就可以減少重復(fù)運(yùn)算所造成的開(kāi)銷(xiāo),這是空間換時(shí)間的策略,也是動(dòng)態(tài)規(guī)劃的編程思想。
題目016:寫(xiě)一個(gè)函數(shù),給定矩陣的階數(shù)n,輸出一個(gè)螺旋式數(shù)字矩陣。
例如:n = 2,返回:
1 2 4 3例如:n = 3,返回:
1 2 3 8 9 4 7 6 5這個(gè)題目本身并不復(fù)雜,下面的代碼僅供參考。
def show_spiral_matrix(n):matrix = [[0] * n for _ in range(n)]row, col = 0, 0num, direction = 1, 0while num <= n ** 2:if matrix[row][col] == 0:matrix[row][col] = numnum += 1if direction == 0:if col < n - 1 and matrix[row][col + 1] == 0:col += 1else:direction += 1elif direction == 1:if row < n - 1 and matrix[row + 1][col] == 0:row += 1else:direction += 1elif direction == 2:if col > 0 and matrix[row][col - 1] == 0:col -= 1else:direction += 1else:if row > 0 and matrix[row - 1][col] == 0:row -= 1else:direction += 1direction %= 4for x in matrix:for y in x:print(y, end='\t')print()題目017:閱讀下面的代碼,寫(xiě)出程序的運(yùn)行結(jié)果。
items = [1, 2, 3, 4] print([i for i in items if i > 2]) print([i for i in items if i % 2]) print([(x, y) for x, y in zip('abcd', (1, 2, 3, 4, 5))]) print({x: f'item{x ** 2}' for x in (2, 4, 6)}) print(len({x for x in 'hello world' if x not in 'abcdefg'}))點(diǎn)評(píng):生成式(推導(dǎo)式)屬于Python的特色語(yǔ)法之一,幾乎是面試必考內(nèi)容。Python中通過(guò)生成式字面量語(yǔ)法,可以創(chuàng)建出列表、集合、字典。
[3, 4] [1, 3] [('a', 1), ('b', 2), ('c', 3), ('d', 4)] {2: 'item4', 4: 'item16', 6: 'item36'} 6題目018:說(shuō)出下面代碼的運(yùn)行結(jié)果。
class Parent:x = 1class Child1(Parent):passclass Child2(Parent):passprint(Parent.x, Child1.x, Child2.x) Child1.x = 2 print(Parent.x, Child1.x, Child2.x) Parent.x = 3 print(Parent.x, Child1.x, Child2.x)點(diǎn)評(píng):運(yùn)行上面的代碼首先輸出1 1 1,這一點(diǎn)大家應(yīng)該沒(méi)有什么疑問(wèn)。接下來(lái),通過(guò)Child1.x = 2給類(lèi)Child1重新綁定了屬性x并賦值為2,所以Child1.x會(huì)輸出2,而Parent和Child2并不受影響。執(zhí)行Parent.x = 3會(huì)重新給Parent類(lèi)的x屬性賦值為3,由于Child2的x屬性繼承自Parent,所以Child2.x的值也是3;而之前我們?yōu)镃hild1重新綁定了x屬性,那么它的x屬性值不會(huì)受到Parent.x = 3的影響,還是之前的值2。
1 1 1 1 2 1 3 2 3題目19:說(shuō)說(shuō)你用過(guò)Python標(biāo)準(zhǔn)庫(kù)中的哪些模塊。
點(diǎn)評(píng):Python標(biāo)準(zhǔn)庫(kù)中的模塊非常多,建議大家根據(jù)自己過(guò)往的項(xiàng)目經(jīng)歷來(lái)介紹你用過(guò)的標(biāo)準(zhǔn)庫(kù)和三方庫(kù),因?yàn)檫@些是你最為熟悉的,經(jīng)得起面試官深挖的。
| sys | 跟Python解釋器相關(guān)的變量和函數(shù),例如:sys.version、sys.exit() |
| os | 和操作系統(tǒng)相關(guān)的功能,例如:os.listdir()、os.remove() |
| re | 和正則表達(dá)式相關(guān)的功能,例如:re.compile()、re.search() |
| math | 和數(shù)學(xué)運(yùn)算相關(guān)的功能,例如:math.pi、math.e、math.cos |
| logging | 和日志系統(tǒng)相關(guān)的類(lèi)和函數(shù),例如:logging.Logger、logging.Handler |
| json / pickle | 實(shí)現(xiàn)對(duì)象序列化和反序列的模塊,例如:json.loads、json.dumps |
| hashlib | 封裝了多種哈希摘要算法的模塊,例如:hashlib.md5、hashlib.sha1 |
| urllib | 包含了和URL相關(guān)的子模塊,例如:urllib.request、urllib.parse |
| itertools | 提供各種迭代器的模塊,例如:itertools.cycle、itertools.product |
| functools | 函數(shù)相關(guān)工具模塊,例如:functools.partial、functools.lru_cache |
| collections / heapq | 封裝了常用數(shù)據(jù)結(jié)構(gòu)和算法的模塊,例如:collections.deque |
| threading / multiprocessing | 多線(xiàn)程/多進(jìn)程相關(guān)類(lèi)和函數(shù)的模塊,例如:threading.Thread |
| concurrent.futures / asyncio | 并發(fā)編程/異步編程相關(guān)的類(lèi)和函數(shù)的模塊,例如:ThreadPoolExecutor |
| base64 | 提供BASE-64編碼相關(guān)函數(shù)的模塊,例如:bas64.encode |
| csv | 和讀寫(xiě)CSV文件相關(guān)的模塊,例如:csv.reader、csv.writer |
| profile / cProfile / pstats | 和代碼性能剖析相關(guān)的模塊,例如:cProfile.run、pstats.Stats |
| unittest | 和單元測(cè)試相關(guān)的模塊,例如:unittest.TestCase |
題目20:__init__和__new__方法有什么區(qū)別?
Python中調(diào)用構(gòu)造器創(chuàng)建對(duì)象屬于兩階段構(gòu)造過(guò)程,首先執(zhí)行__new__方法獲得保存對(duì)象所需的內(nèi)存空間,再通過(guò)__init__執(zhí)行對(duì)內(nèi)存空間數(shù)據(jù)的填充(對(duì)象屬性的初始化)。__new__方法的返回值是創(chuàng)建好的Python對(duì)象(的引用),而__init__方法的第一個(gè)參數(shù)就是這個(gè)對(duì)象(的引用),所以在__init__中可以完成對(duì)對(duì)象的初始化操作。__new__是類(lèi)方法,它的第一個(gè)參數(shù)是類(lèi),__init__是對(duì)象方法,它的第一個(gè)參數(shù)是對(duì)象。
題目21:輸入年月日,判斷這個(gè)日期是這一年的第幾天。
方法一:不使用標(biāo)準(zhǔn)庫(kù)中的模塊和函數(shù)。
def is_leap_year(year):"""判斷指定的年份是不是閏年,平年返回False,閏年返回True"""return year % 4 == 0 and year % 100 != 0 or year % 400 == 0def which_day(year, month, date):"""計(jì)算傳入的日期是這一年的第幾天"""# 用嵌套的列表保存平年和閏年每個(gè)月的天數(shù)days_of_month = [[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]]days = days_of_month[is_leap_year(year)][:month - 1]return sum(days) + date方法二:使用標(biāo)準(zhǔn)庫(kù)中的datetime模塊。
import datetimedef which_day(year, month, date):end = datetime.date(year, month, date)start = datetime.date(year, 1, 1)return (end - start).days + 1題目22:平常工作中用什么工具進(jìn)行靜態(tài)代碼分析。
點(diǎn)評(píng):靜態(tài)代碼分析工具可以從代碼中提煉出各種靜態(tài)屬性,這使得開(kāi)發(fā)者可以對(duì)代碼的復(fù)雜性、可維護(hù)性和可讀性有更好的了解,這里所說(shuō)的靜態(tài)屬性包括:
工作中靜態(tài)代碼分析主要用到的是Pylint和Flake8。Pylint可以檢查出代碼錯(cuò)誤、壞味道、不規(guī)范的代碼等問(wèn)題,較新的版本中還提供了代碼復(fù)雜度統(tǒng)計(jì)數(shù)據(jù),可以生成檢查報(bào)告。Flake8封裝了Pyflakes(檢查代碼邏輯錯(cuò)誤)、McCabe(檢查代碼復(fù)雜性)和Pycodestyle(檢查代碼是否符合PEP-8規(guī)范)工具,它可以執(zhí)行這三個(gè)工具提供的檢查。
題目23:說(shuō)一下你知道的Python中的魔術(shù)方法。
點(diǎn)評(píng):魔術(shù)方法也稱(chēng)為魔法方法,是Python中的特色語(yǔ)法,也是面試中的高頻問(wèn)題。
| __new__、__init__、__del__ | 創(chuàng)建和銷(xiāo)毀對(duì)象相關(guān) |
| __add__、__sub__、__mul__、__div__、__floordiv__、__mod__ | 算術(shù)運(yùn)算符相關(guān) |
| __eq__、__ne__、__lt__、__gt__、__le__、__ge__ | 關(guān)系運(yùn)算符相關(guān) |
| __pos__、__neg__、__invert__ | 一元運(yùn)算符相關(guān) |
| __lshift__、__rshift__、__and__、__or__、__xor__ | 位運(yùn)算相關(guān) |
| __enter__、__exit__ | 上下文管理器協(xié)議 |
| __iter__、__next__、__reversed__ | 迭代器協(xié)議 |
| __int__、__long__、__float__、__oct__、__hex__ | 類(lèi)型/進(jìn)制轉(zhuǎn)換相關(guān) |
| __str__、__repr__、__hash__、__dir__ | 對(duì)象表述相關(guān) |
| __len__、__getitem__、__setitem__、__contains__、__missing__ | 序列相關(guān) |
| __copy__、__deepcopy__ | 對(duì)象拷貝相關(guān) |
| __call__、__setattr__、__getattr__、__delattr__ | 其他魔術(shù)方法 |
題目24:函數(shù)參數(shù)*arg和**kwargs分別代表什么?
Python中,函數(shù)的參數(shù)分為位置參數(shù)、可變參數(shù)、關(guān)鍵字參數(shù)、命名關(guān)鍵字參數(shù)。*args代表可變參數(shù),可以接收0個(gè)或任意多個(gè)參數(shù),當(dāng)不確定調(diào)用者會(huì)傳入多少個(gè)位置參數(shù)時(shí),就可以使用可變參數(shù),它會(huì)將傳入的參數(shù)打包成一個(gè)元組。**kwargs代表關(guān)鍵字參數(shù),可以接收用參數(shù)名=參數(shù)值的方式傳入的參數(shù),傳入的參數(shù)的會(huì)打包成一個(gè)字典。定義函數(shù)時(shí)如果同時(shí)使用*args和**kwargs,那么函數(shù)可以接收任意參數(shù)。
題目25:寫(xiě)一個(gè)記錄函數(shù)執(zhí)行時(shí)間的裝飾器。
點(diǎn)評(píng):高頻面試題,也是最簡(jiǎn)單的裝飾器,面試者必須要掌握的內(nèi)容。
方法一:用函數(shù)實(shí)現(xiàn)裝飾器。
from functools import wraps from time import timedef record_time(func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)print(f'{func.__name__}執(zhí)行時(shí)間: {time() - start}秒')return resultreturn wrapper方法二:用類(lèi)實(shí)現(xiàn)裝飾器。類(lèi)有__call__魔術(shù)方法,該類(lèi)對(duì)象就是可調(diào)用對(duì)象,可以當(dāng)做裝飾器來(lái)使用。
from functools import wraps from time import timeclass Record:def __call__(self, func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)print(f'{func.__name__}執(zhí)行時(shí)間: {time() - start}秒')return resultreturn wrapper說(shuō)明:裝飾器可以用來(lái)裝飾類(lèi)或函數(shù),為其提供額外的能力,屬于設(shè)計(jì)模式中的代理模式。
擴(kuò)展:裝飾器本身也可以參數(shù)化,例如上面的例子中,如果不希望在終端中顯示函數(shù)的執(zhí)行時(shí)間而是希望由調(diào)用者來(lái)決定如何輸出函數(shù)的執(zhí)行時(shí)間,可以通過(guò)參數(shù)化裝飾器的方式來(lái)做到,代碼如下所示。
from functools import wraps from time import timedef record_time(output):"""可以參數(shù)化的裝飾器"""def decorate(func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)output(func.__name__, time() - start)return resultreturn wrapperreturn decorate題目26:什么是鴨子類(lèi)型(duck typing)?
鴨子類(lèi)型是動(dòng)態(tài)類(lèi)型語(yǔ)言判斷一個(gè)對(duì)象是不是某種類(lèi)型時(shí)使用的方法,也叫做鴨子判定法。簡(jiǎn)單的說(shuō),鴨子類(lèi)型是指判斷一只鳥(niǎo)是不是鴨子,我們只關(guān)心它游泳像不像鴨子、叫起來(lái)像不像鴨子、走路像不像鴨子就足夠了。換言之,如果對(duì)象的行為跟我們的預(yù)期是一致的(能夠接受某些消息),我們就認(rèn)定它是某種類(lèi)型的對(duì)象。
在Python語(yǔ)言中,有很多bytes-like對(duì)象(如:bytes、bytearray、array.array、memoryview)、file-like對(duì)象(如:StringIO、BytesIO、GzipFile、socket)、path-like對(duì)象(如:str、bytes),其中file-like對(duì)象都能支持read和write操作,可以像文件一樣讀寫(xiě),這就是所謂的對(duì)象有鴨子的行為就可以判定為鴨子的判定方法。再比如Python中列表的extend方法,它需要的參數(shù)并不一定要是列表,只要是可迭代對(duì)象就沒(méi)有問(wèn)題。
說(shuō)明:動(dòng)態(tài)語(yǔ)言的鴨子類(lèi)型使得設(shè)計(jì)模式的應(yīng)用被大大簡(jiǎn)化。
題目27:說(shuō)一下Python中變量的作用域。
Python中有四種作用域,分別是局部作用域(Local)、嵌套作用域(Embedded)、全局作用域(Global)、內(nèi)置作用域(Built-in),搜索一個(gè)標(biāo)識(shí)符時(shí),會(huì)按照LEGB的順序進(jìn)行搜索,如果所有的作用域中都沒(méi)有找到這個(gè)標(biāo)識(shí)符,就會(huì)引發(fā)NameError異常。
題目28:說(shuō)一下你對(duì)閉包的理解。
閉包是支持一等函數(shù)的編程語(yǔ)言(Python、JavaScript等)中實(shí)現(xiàn)詞法綁定的一種技術(shù)。當(dāng)捕捉閉包的時(shí)候,它的自由變量(在函數(shù)外部定義但在函數(shù)內(nèi)部使用的變量)會(huì)在捕捉時(shí)被確定,這樣即便脫離了捕捉時(shí)的上下文,它也能照常運(yùn)行。簡(jiǎn)單的說(shuō),可以將閉包理解為能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。正在情況下,函數(shù)的局部變量在函數(shù)調(diào)用結(jié)束之后就結(jié)束了生命周期,但是閉包使得局部變量的生命周期得到了延展。使用閉包的時(shí)候需要注意,閉包會(huì)使得函數(shù)中創(chuàng)建的對(duì)象不會(huì)被垃圾回收,可能會(huì)導(dǎo)致很大的內(nèi)存開(kāi)銷(xiāo),所以閉包一定不能濫用。
題目29:說(shuō)一下Python中的多線(xiàn)程和多進(jìn)程的應(yīng)用場(chǎng)景和優(yōu)缺點(diǎn)。
線(xiàn)程是操作系統(tǒng)分配CPU的基本單位,進(jìn)程是操作系統(tǒng)分配內(nèi)存的基本單位。通常我們運(yùn)行的程序會(huì)包含一個(gè)或多個(gè)進(jìn)程,而每個(gè)進(jìn)程中又包含一個(gè)或多個(gè)線(xiàn)程。多線(xiàn)程的優(yōu)點(diǎn)在于多個(gè)線(xiàn)程可以共享進(jìn)程的內(nèi)存空間,所以進(jìn)程間的通信非常容易實(shí)現(xiàn);但是如果使用官方的CPython解釋器,多線(xiàn)程受制于GIL(全局解釋器鎖),并不能利用CPU的多核特性,這是一個(gè)很大的問(wèn)題。使用多進(jìn)程可以充分利用CPU的多核特性,但是進(jìn)程間通信相對(duì)比較麻煩,需要使用IPC機(jī)制(管道、套接字等)。
多線(xiàn)程適合那些會(huì)花費(fèi)大量時(shí)間在I/O操作上,但沒(méi)有太多并行計(jì)算需求且不需占用太多內(nèi)存的I/O密集型應(yīng)用。多進(jìn)程適合執(zhí)行計(jì)算密集型任務(wù)(如:視頻編碼解碼、數(shù)據(jù)處理、科學(xué)計(jì)算等)、可以分解為多個(gè)并行子任務(wù)并能合并子任務(wù)執(zhí)行結(jié)果的任務(wù)以及在內(nèi)存使用方面沒(méi)有任何限制且不強(qiáng)依賴(lài)于I/O操作的任務(wù)。
擴(kuò)展:Python中實(shí)現(xiàn)并發(fā)編程通常有多線(xiàn)程、多進(jìn)程和異步編程三種選擇。異步編程實(shí)現(xiàn)了協(xié)作式并發(fā),通過(guò)多個(gè)相互協(xié)作的子程序的用戶(hù)態(tài)切換,實(shí)現(xiàn)對(duì)CPU的高效利用,這種方式也是非常適合I/O密集型應(yīng)用的。
題目30:說(shuō)一下Python 2和Python 3的區(qū)別。
點(diǎn)評(píng):這種問(wèn)題千萬(wàn)不要背所謂的參考答案,說(shuō)一些自己最熟悉的就足夠了。
題目31:談?wù)勀銓?duì)“猴子補(bǔ)丁”(monkey patching)的理解。
“猴子補(bǔ)丁”是動(dòng)態(tài)類(lèi)型語(yǔ)言的一個(gè)特性,代碼運(yùn)行時(shí)在不修改源代碼的前提下改變代碼中的方法、屬性、函數(shù)等以達(dá)到熱補(bǔ)丁(hot patch)的效果。很多系統(tǒng)的安全補(bǔ)丁也是通過(guò)猴子補(bǔ)丁的方式來(lái)實(shí)現(xiàn)的,但實(shí)際開(kāi)發(fā)中應(yīng)該避免對(duì)猴子補(bǔ)丁的使用,以免造成代碼行為不一致的問(wèn)題。
在使用gevent庫(kù)的時(shí)候,我們會(huì)在代碼開(kāi)頭的地方執(zhí)行g(shù)event.monkey.patch_all(),這行代碼的作用是把標(biāo)準(zhǔn)庫(kù)中的socket模塊給替換掉,這樣我們?cè)谑褂胹ocket的時(shí)候,不用修改任何代碼就可以實(shí)現(xiàn)對(duì)代碼的協(xié)程化,達(dá)到提升性能的目的,這就是對(duì)猴子補(bǔ)丁的應(yīng)用。
另外,如果希望用ujson三方庫(kù)替換掉標(biāo)準(zhǔn)庫(kù)中的json,也可以使用猴子補(bǔ)丁的方式,代碼如下所示。
import json, ujsonjson.__name__ = 'ujson' json.dumps = ujson.dumps json.loads = ujson.loads單元測(cè)試中的Mock技術(shù)也是對(duì)猴子補(bǔ)丁的應(yīng)用,Python中的unittest.mock模塊就是解決單元測(cè)試中用Mock對(duì)象替代被測(cè)對(duì)象所依賴(lài)的對(duì)象的模塊。
題目32:閱讀下面的代碼說(shuō)出運(yùn)行結(jié)果。
class A:def who(self):print('A', end='')class B(A):def who(self):super(B, self).who()print('B', end='')class C(A):def who(self):super(C, self).who()print('C', end='')class D(B, C):def who(self):super(D, self).who()print('D', end='')item = D() item.who()點(diǎn)評(píng):這道題考查到了兩個(gè)知識(shí)點(diǎn):
題目33:編寫(xiě)一個(gè)函數(shù)實(shí)現(xiàn)對(duì)逆波蘭表達(dá)式求值,不能使用Python的內(nèi)置函數(shù)。
點(diǎn)評(píng):逆波蘭表達(dá)式也稱(chēng)為“后綴表達(dá)式”,相較于平常我們使用的“中綴表達(dá)式”,逆波蘭表達(dá)式不需要括號(hào)來(lái)確定運(yùn)算的優(yōu)先級(jí),例如5 * (2 + 3)對(duì)應(yīng)的逆波蘭表達(dá)式是5 2 3 + *。逆波蘭表達(dá)式求值需要借助棧結(jié)構(gòu),掃描表達(dá)式遇到運(yùn)算數(shù)就入棧,遇到運(yùn)算符就出棧兩個(gè)元素做運(yùn)算,將運(yùn)算結(jié)果入棧。表達(dá)式掃描結(jié)束后,棧中只有一個(gè)數(shù),這個(gè)數(shù)就是最終的運(yùn)算結(jié)果,直接出棧即可。
import operatorclass Stack:"""棧(FILO)"""def __init__(self):self.elems = []def push(self, elem):"""入棧"""self.elems.append(elem)def pop(self):"""出棧"""return self.elems.pop()@propertydef is_empty(self):"""檢查棧是否為空"""return len(self.elems) == 0def eval_suffix(expr):"""逆波蘭表達(dá)式求值"""operators = {'+': operator.add,'-': operator.sub,'*': operator.mul,'/': operator.truediv}stack = Stack()for item in expr.split():if item.isdigit():stack.push(float(item))else: num2 = stack.pop()num1 = stack.pop()stack.push(operators[item](num1, num2))return stack.pop()題目34:Python中如何實(shí)現(xiàn)字符串替換操作?
Python中實(shí)現(xiàn)字符串替換大致有兩類(lèi)方法:字符串的replace方法和正則表達(dá)式的sub方法。
方法一:使用字符串的replace方法。
message = 'hello, world!' print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE'))方法二:使用正則表達(dá)式的sub方法。
import remessage = 'hello, world!' pattern = re.compile('[aeiou]') print(pattern.sub('#', message))擴(kuò)展:還有一個(gè)相關(guān)的面試題,對(duì)保存文件名的列表排序,要求文件名按照字母表和數(shù)字大小進(jìn)行排序,例如對(duì)于列表filenames = ['a12.txt', 'a8.txt', 'b10.txt', 'b2.txt', 'b19.txt', 'a3.txt'],排序的結(jié)果是['a3.txt', 'a8.txt', 'a12.txt', 'b2.txt', 'b10.txt', 'b19.txt']。提示一下,可以通過(guò)字符串替換的方式為文件名補(bǔ)位,根據(jù)補(bǔ)位后的文件名用sorted函數(shù)來(lái)排序,大家可以思考下這個(gè)問(wèn)題如何解決。
題目35:如何剖析Python代碼的執(zhí)行性能?
剖析代碼性能可以使用Python標(biāo)準(zhǔn)庫(kù)中的cProfile和pstats模塊,cProfile的run函數(shù)可以執(zhí)行代碼并收集統(tǒng)計(jì)信息,創(chuàng)建出Stats對(duì)象并打印簡(jiǎn)單的剖析報(bào)告。Stats是pstats模塊中的類(lèi),它是一個(gè)統(tǒng)計(jì)對(duì)象。當(dāng)然,也可以使用三方工具line_profiler和memory_profiler來(lái)剖析每一行代碼耗費(fèi)的時(shí)間和內(nèi)存,這兩個(gè)三方工具都會(huì)用非常友好的方式輸出剖析結(jié)構(gòu)。如果使用PyCharm,可以利用“Run”菜單的“Profile”菜單項(xiàng)對(duì)代碼進(jìn)行性能分析,PyCharm中可以用表格或者調(diào)用圖(Call Graph)的方式來(lái)顯示性能剖析的結(jié)果。
下面是使用cProfile剖析代碼性能的例子。
example.py
import cProfiledef is_prime(num):for factor in range(2, int(num ** 0.5) + 1):if num % factor == 0:return Falsereturn Trueclass PrimeIter:def __init__(self, total):self.counter = 0self.current = 1self.total = totaldef __iter__(self):return selfdef __next__(self):if self.counter < self.total:self.current += 1while not is_prime(self.current):self.current += 1self.counter += 1return self.currentraise StopIteration()cProfile.run('list(PrimeIter(10000))')如果使用line_profiler三方工具,可以直接剖析is_prime函數(shù)每行代碼的性能,需要給is_prime函數(shù)添加一個(gè)profiler裝飾器,代碼如下所示。
@profiler def is_prime(num):for factor in range(2, int(num ** 0.5) + 1):if num % factor == 0:return Falsereturn True安裝line_profiler。
pip install line_profiler使用line_profiler。
kernprof -lv example.py運(yùn)行結(jié)果如下所示。
Line # Hits Time Per Hit % Time Line Contents ==============================================================1 @profile2 def is_prime(num):3 86624 48420.0 0.6 50.5 for factor in range(2, int(num ** 0.5) + 1):4 85624 44000.0 0.5 45.9 if num % factor == 0:5 6918 3080.0 0.4 3.2 return False6 1000 430.0 0.4 0.4 return True題目36:如何使用random模塊生成隨機(jī)數(shù)、實(shí)現(xiàn)隨機(jī)亂序和隨機(jī)抽樣?
點(diǎn)評(píng):送人頭的題目,因?yàn)镻ython標(biāo)準(zhǔn)庫(kù)中的常用模塊應(yīng)該是Python開(kāi)發(fā)者都比較熟悉的內(nèi)容,這個(gè)問(wèn)題回如果答不上來(lái),整個(gè)面試基本也就砸鍋了。
擴(kuò)展:random模塊提供的函數(shù)除了生成均勻分布的隨機(jī)數(shù)外,還可以生成其他分布的隨機(jī)數(shù),例如random.gauss(mu, sigma)函數(shù)可以生成高斯分布(正態(tài)分布)的隨機(jī)數(shù);random.paretovariate(alpha)函數(shù)會(huì)生成帕累托分布的隨機(jī)數(shù);random.gammavariate(alpha, beta)函數(shù)會(huì)生成伽馬分布的隨機(jī)數(shù)。
題目37:解釋一下線(xiàn)程池的工作原理。
點(diǎn)評(píng):池化技術(shù)就是一種典型空間換時(shí)間的策略,我們使用的數(shù)據(jù)庫(kù)連接池、線(xiàn)程池等都是池化技術(shù)的應(yīng)用,Python標(biāo)準(zhǔn)庫(kù)currrent.futures模塊的ThreadPoolExecutor就是線(xiàn)程池的實(shí)現(xiàn),如果要弄清楚它的工作原理,可以參考下面的內(nèi)容。
線(xiàn)程池是一種用于減少線(xiàn)程本身創(chuàng)建和銷(xiāo)毀造成的開(kāi)銷(xiāo)的技術(shù),屬于典型的空間換時(shí)間操作。如果應(yīng)用程序需要頻繁的將任務(wù)派發(fā)到線(xiàn)程中執(zhí)行,線(xiàn)程池就是必選項(xiàng),因?yàn)閯?chuàng)建和釋放線(xiàn)程涉及到大量的系統(tǒng)底層操作,開(kāi)銷(xiāo)較大,如果能夠在應(yīng)用程序工作期間,將創(chuàng)建和釋放線(xiàn)程的操作變成預(yù)創(chuàng)建和借還操作,將大大減少底層開(kāi)銷(xiāo)。線(xiàn)程池在應(yīng)用程序啟動(dòng)后,立即創(chuàng)建一定數(shù)量的線(xiàn)程,放入空閑隊(duì)列中。這些線(xiàn)程最開(kāi)始都處于阻塞狀態(tài),不會(huì)消耗CPU資源,但會(huì)占用少量的內(nèi)存空間。當(dāng)任務(wù)到來(lái)后,從隊(duì)列中取出一個(gè)空閑線(xiàn)程,把任務(wù)派發(fā)到這個(gè)線(xiàn)程中運(yùn)行,并將該線(xiàn)程標(biāo)記為已占用。當(dāng)線(xiàn)程池中所有的線(xiàn)程都被占用后,可以選擇自動(dòng)創(chuàng)建一定數(shù)量的新線(xiàn)程,用于處理更多的任務(wù),也可以選擇讓任務(wù)排隊(duì)等待直到有空閑的線(xiàn)程可用。在任務(wù)執(zhí)行完畢后,線(xiàn)程并不退出結(jié)束,而是繼續(xù)保持在池中等待下一次的任務(wù)。當(dāng)系統(tǒng)比較空閑時(shí),大部分線(xiàn)程長(zhǎng)時(shí)間處于閑置狀態(tài)時(shí),線(xiàn)程池可以自動(dòng)銷(xiāo)毀一部分線(xiàn)程,回收系統(tǒng)資源。基于這種預(yù)創(chuàng)建技術(shù),線(xiàn)程池將線(xiàn)程創(chuàng)建和銷(xiāo)毀本身所帶來(lái)的開(kāi)銷(xiāo)分?jǐn)偟搅烁鱾€(gè)具體的任務(wù)上,執(zhí)行次數(shù)越多,每個(gè)任務(wù)所分擔(dān)到的線(xiàn)程本身開(kāi)銷(xiāo)則越小。
一般線(xiàn)程池都必須具備下面幾個(gè)組成部分:
題目38:舉例說(shuō)明什么情況下會(huì)出現(xiàn)KeyError、TypeError、ValueError。
舉一個(gè)簡(jiǎn)單的例子,變量a是一個(gè)字典,執(zhí)行int(a['x'])這個(gè)操作就有可能引發(fā)上述三種類(lèi)型的異常。如果字典中沒(méi)有鍵x,會(huì)引發(fā)KeyError;如果鍵x對(duì)應(yīng)的值不是str、float、int、bool以及bytes-like類(lèi)型,在調(diào)用int函數(shù)構(gòu)造int類(lèi)型的對(duì)象時(shí),會(huì)引發(fā)TypeError;如果a[x]是一個(gè)字符串或者字節(jié)串,而對(duì)應(yīng)的內(nèi)容又無(wú)法處理成int時(shí),將引發(fā)ValueError。
題目39:說(shuō)出下面代碼的運(yùn)行結(jié)果。
def extend_list(val, items=[]):items.append(val)return itemslist1 = extend_list(10) list2 = extend_list(123, []) list3 = extend_list('a') print(list1) print(list2) print(list3)點(diǎn)評(píng):Python函數(shù)在定義的時(shí)候,默認(rèn)參數(shù)items的值就被計(jì)算出來(lái)了,即[]。因?yàn)槟J(rèn)參數(shù)items引用了對(duì)象[],每次調(diào)用該函數(shù),如果對(duì)items引用的列表進(jìn)行了操作,下次調(diào)用時(shí),默認(rèn)參數(shù)還是引用之前的那個(gè)列表而不是重新賦值為[],所以列表中會(huì)有之前添加的元素。如果通過(guò)傳參的方式為items重新賦值,那么items將引用到新的列表對(duì)象,而不再引用默認(rèn)的那個(gè)列表對(duì)象。這個(gè)題在面試中經(jīng)常被問(wèn)到,通常不建議使用容器類(lèi)型的默認(rèn)參數(shù),像PyLint這樣的代碼檢查工具也會(huì)對(duì)這種代碼提出質(zhì)疑和警告。
[10, 'a'] [123] [10, 'a']題目40:如何讀取大文件,例如內(nèi)存只有4G,如何讀取一個(gè)大小為8G的文件?
很顯然4G內(nèi)存要一次性的加載大小為8G的文件是不現(xiàn)實(shí)的,遇到這種情況必須要考慮多次讀取和分批次處理。在Python中讀取文件可以先通過(guò)open函數(shù)獲取文件對(duì)象,在讀取文件時(shí),可以通過(guò)read方法的size參數(shù)指定讀取的大小,也可以通過(guò)seek方法的offset參數(shù)指定讀取的位置,這樣就可以控制單次讀取數(shù)據(jù)的字節(jié)數(shù)和總字節(jié)數(shù)。除此之外,可以使用內(nèi)置函數(shù)iter將文件對(duì)象處理成迭代器對(duì)象,每次只讀取少量的數(shù)據(jù)進(jìn)行處理,代碼大致寫(xiě)法如下所示。
with open('...', 'rb') as file:for data in iter(lambda: file.read(2097152), b''):pass在Linux系統(tǒng)上,可以通過(guò)split命令將大文件切割為小片,然后通過(guò)讀取切割后的小文件對(duì)數(shù)據(jù)進(jìn)行處理。例如下面的命令將名為filename的大文件切割為大小為512M的多個(gè)文件。
split -b 512m filename如果愿意, 也可以將名為filename的文件切割為10個(gè)文件,命令如下所示。
split -n 10 filename擴(kuò)展:外部排序跟上述的情況非常類(lèi)似,由于處理的數(shù)據(jù)不能一次裝入內(nèi)存,只能放在讀寫(xiě)較慢的外存儲(chǔ)器(通常是硬盤(pán))上。“排序-歸并算法”就是一種常用的外部排序策略。在排序階段,先讀入能放在內(nèi)存中的數(shù)據(jù)量,將其排序輸出到一個(gè)臨時(shí)文件,依此進(jìn)行,將待排序數(shù)據(jù)組織為多個(gè)有序的臨時(shí)文件,然后在歸并階段將這些臨時(shí)文件組合為一個(gè)大的有序文件,這個(gè)大的有序文件就是排序的結(jié)果。
題目41:說(shuō)一下你對(duì)Python中模塊和包的理解。
每個(gè)Python文件就是一個(gè)模塊,而保存這些文件的文件夾就是一個(gè)包,但是這個(gè)作為Python包的文件夾必須要有一個(gè)名為_(kāi)_init__.py的文件,否則無(wú)法導(dǎo)入這個(gè)包。通常一個(gè)文件夾下還可以有子文件夾,這也就意味著一個(gè)包下還可以有子包,子包中的__init__.py并不是必須的。模塊和包解決了Python中命名沖突的問(wèn)題,不同的包下可以有同名的模塊,不同的模塊下可以有同名的變量、函數(shù)或類(lèi)。在Python中可以使用import或from ... import ...來(lái)導(dǎo)入包和模塊,在導(dǎo)入的時(shí)候還可以使用as關(guān)鍵字對(duì)包、模塊、類(lèi)、函數(shù)、變量等進(jìn)行別名,從而徹底解決編程中尤其是多人協(xié)作團(tuán)隊(duì)開(kāi)發(fā)時(shí)的命名沖突問(wèn)題。
題目42:說(shuō)一下你知道的Python編碼規(guī)范。
點(diǎn)評(píng):企業(yè)的Python編碼規(guī)范基本上是參照PEP-8或谷歌開(kāi)源項(xiàng)目風(fēng)格指南來(lái)制定的,后者還提到了可以使用Lint工具來(lái)檢查代碼的規(guī)范程度,面試的時(shí)候遇到這類(lèi)問(wèn)題,可以先說(shuō)下這兩個(gè)參照標(biāo)準(zhǔn),然后挑重點(diǎn)說(shuō)一下Python編碼的注意事項(xiàng)。
- 使用空格來(lái)表示縮進(jìn)而不要用制表符(Tab)。
- 和語(yǔ)法相關(guān)的每一層縮進(jìn)都用4個(gè)空格來(lái)表示。
- 每行的字符數(shù)不要超過(guò)79個(gè)字符,如果表達(dá)式因太長(zhǎng)而占據(jù)了多行,除了首行之外的其余各行都應(yīng)該在正常的縮進(jìn)寬度上再加上4個(gè)空格。
- 函數(shù)和類(lèi)的定義,代碼前后都要用兩個(gè)空行進(jìn)行分隔。
- 在同一個(gè)類(lèi)中,各個(gè)方法之間應(yīng)該用一個(gè)空行進(jìn)行分隔。
- 二元運(yùn)算符的左右兩側(cè)應(yīng)該保留一個(gè)空格,而且只要一個(gè)空格就好。
- 變量、函數(shù)和屬性應(yīng)該使用小寫(xiě)字母來(lái)拼寫(xiě),如果有多個(gè)單詞就使用下劃線(xiàn)進(jìn)行連接。
- 類(lèi)中受保護(hù)的實(shí)例屬性,應(yīng)該以一個(gè)下劃線(xiàn)開(kāi)頭。
- 類(lèi)中私有的實(shí)例屬性,應(yīng)該以?xún)蓚€(gè)下劃線(xiàn)開(kāi)頭。
- 類(lèi)和異常的命名,應(yīng)該每個(gè)單詞首字母大寫(xiě)。
- 模塊級(jí)別的常量,應(yīng)該采用全大寫(xiě)字母,如果有多個(gè)單詞就用下劃線(xiàn)進(jìn)行連接。
- 類(lèi)的實(shí)例方法,應(yīng)該把第一個(gè)參數(shù)命名為self以表示對(duì)象自身。
- 類(lèi)的類(lèi)方法,應(yīng)該把第一個(gè)參數(shù)命名為cls以表示該類(lèi)自身。
- 采用內(nèi)聯(lián)形式的否定詞,而不要把否定詞放在整個(gè)表達(dá)式的前面。例如:if a is not b就比if not a is b更容易讓人理解。
- 不要用檢查長(zhǎng)度的方式來(lái)判斷字符串、列表等是否為None或者沒(méi)有元素,應(yīng)該用if not x這樣的寫(xiě)法來(lái)檢查它。
- 就算if分支、for循環(huán)、except異常捕獲等中只有一行代碼,也不要將代碼和if、for、except等寫(xiě)在一起,分開(kāi)寫(xiě)才會(huì)讓代碼更清晰。
- import語(yǔ)句總是放在文件開(kāi)頭的地方。
- 引入模塊的時(shí)候,from math import sqrt比import math更好。
- 如果有多個(gè)import語(yǔ)句,應(yīng)該將其分為三部分,從上到下分別是Python標(biāo)準(zhǔn)模塊、第三方模塊和自定義模塊,每個(gè)部分內(nèi)部應(yīng)該按照模塊名稱(chēng)的字母表順序來(lái)排列。
題目43:運(yùn)行下面的代碼是否會(huì)報(bào)錯(cuò),如果報(bào)錯(cuò)請(qǐng)說(shuō)明哪里有什么樣的錯(cuò),如果不報(bào)錯(cuò)請(qǐng)說(shuō)出代碼的執(zhí)行結(jié)果。
class A: def __init__(self, value):self.__value = value@propertydef value(self):return self.__valueobj = A(1) obj.__value = 2 print(obj.value) print(obj.__value)點(diǎn)評(píng):這道題有兩個(gè)考察點(diǎn),一個(gè)考察點(diǎn)是對(duì)_和__開(kāi)頭的對(duì)象屬性訪(fǎng)問(wèn)權(quán)限以及@property裝飾器的了解,另外一個(gè)考察的點(diǎn)是對(duì)動(dòng)態(tài)語(yǔ)言的理解,不需要過(guò)多的解釋。
1 2擴(kuò)展:如果不希望代碼運(yùn)行時(shí)動(dòng)態(tài)的給對(duì)象添加新屬性,可以在定義類(lèi)時(shí)使用__slots__魔法。例如,我們可以在上面的A中添加一行__slots__ = ('__value', ),再次運(yùn)行上面的代碼,將會(huì)在原來(lái)的第10行處產(chǎn)生AttributeError錯(cuò)誤。
題目44:對(duì)下面給出的字典按值從大到小對(duì)鍵進(jìn)行排序。
prices = {'AAPL': 191.88,'GOOG': 1186.96,'IBM': 149.24,'ORCL': 48.44,'ACN': 166.89,'FB': 208.09,'SYMC': 21.29 }點(diǎn)評(píng):sorted函數(shù)的高階用法在面試的時(shí)候經(jīng)常出現(xiàn),key參數(shù)可以傳入一個(gè)函數(shù)名或一個(gè)Lambda函數(shù),該函數(shù)的返回值代表了在排序時(shí)比較元素的依據(jù)。
sorted(prices, key=lambda x: prices[x], reverse=True)題目45:說(shuō)一下namedtuple的用法和作用。
點(diǎn)評(píng):Python標(biāo)準(zhǔn)庫(kù)的collections模塊提供了很多有用的數(shù)據(jù)結(jié)構(gòu),這些內(nèi)容并不是每個(gè)開(kāi)發(fā)者都清楚,就比如題目問(wèn)到的namedtuple,在我參加過(guò)的面試中,90%的面試者都不能準(zhǔn)確的說(shuō)出它的作用和應(yīng)用場(chǎng)景。此外,deque也是一個(gè)非常有用但又經(jīng)常被忽視的類(lèi),還有Counter、OrderedDict 、defaultdict 、UserDict等類(lèi),大家清楚它們的用法嗎?
在使用面向?qū)ο缶幊陶Z(yǔ)言的時(shí)候,定義類(lèi)是最常見(jiàn)的一件事情,有的時(shí)候,我們會(huì)用到只有屬性沒(méi)有方法的類(lèi),這種類(lèi)的對(duì)象通常只用于組織數(shù)據(jù),并不能接收消息,所以我們把這種類(lèi)稱(chēng)為數(shù)據(jù)類(lèi)或者退化的類(lèi),就像C語(yǔ)言中的結(jié)構(gòu)體那樣。我們并不建議使用這種退化的類(lèi),在Python中可以用namedtuple(命名元組)來(lái)替代這種類(lèi)。
from collections import namedtupleCard = namedtuple('Card', ('suite', 'face')) card1 = Card('紅桃', 13) card2 = Card('草花', 5) print(f'{card1.suite}{card1.face}') print(f'{card2.suite}{card2.face}')命名元組與普通元組一樣是不可變?nèi)萜?#xff0c;一旦將數(shù)據(jù)存儲(chǔ)在namedtuple的頂層屬性中,數(shù)據(jù)就不能再修改了,也就意味著對(duì)象上的所有屬性都遵循“一次寫(xiě)入,多次讀取”的原則。和普通元組不同的是,命名元組中的數(shù)據(jù)有訪(fǎng)問(wèn)名稱(chēng),可以通過(guò)名稱(chēng)而不是索引來(lái)獲取保存的數(shù)據(jù),不僅在操作上更加簡(jiǎn)單,代碼的可讀性也會(huì)更好。
命名元組的本質(zhì)就是一個(gè)類(lèi),所以它還可以作為父類(lèi)創(chuàng)建子類(lèi)。除此之外,命名元組內(nèi)置了一系列的方法,例如,可以通過(guò)_asdict方法將命名元組處理成字典,也可以通過(guò)_replace方法創(chuàng)建命名元組對(duì)象的淺拷貝。
class MyCard(Card):def show(self):faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']return f'{self.suite}{faces[self.face]}'print(Card) # <class '__main__.Card'> card3 = MyCard('方塊', 12) print(card3.show()) # 方塊Q print(dict(card1._asdict())) # {'suite': '紅桃', 'face': 13} print(card2._replace(suite='方塊')) # Card(suite='方塊', face=5)總而言之,命名元組能更好的組織數(shù)據(jù)結(jié)構(gòu),讓代碼更加清晰和可讀,在很多場(chǎng)景下是元組、字典和數(shù)據(jù)類(lèi)的替代品。在需要?jiǎng)?chuàng)建占用空間更少的不可變類(lèi)時(shí),命名元組就是很好的選擇。
題目46:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
要求:寫(xiě)一個(gè)函數(shù),傳入一個(gè)有若干個(gè)整數(shù)的列表,該列表中某個(gè)元素出現(xiàn)的次數(shù)超過(guò)了50%,返回這個(gè)元素。
def more_than_half(items):temp, times = None, 0for item in items:if times == 0:temp = itemtimes += 1else:if item == temp:times += 1else:times -= 1return temp點(diǎn)評(píng):LeetCode上的題目,在Python面試中出現(xiàn)過(guò),利用元素出現(xiàn)次數(shù)超過(guò)了50%這一特征,出現(xiàn)和temp相同的元素就將計(jì)數(shù)值加1,出現(xiàn)和temp不同的元素就將計(jì)數(shù)值減1。如果計(jì)數(shù)值為0,說(shuō)明之前出現(xiàn)的元素已經(jīng)對(duì)最終的結(jié)果沒(méi)有影響,用temp記下當(dāng)前元素并將計(jì)數(shù)值置為1。最終,出現(xiàn)次數(shù)超過(guò)了50%的這個(gè)元素一定會(huì)被賦值給變量temp。
題目47:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
要求:寫(xiě)一個(gè)函數(shù),傳入的參數(shù)是一個(gè)列表(列表中的元素可能也是一個(gè)列表),返回該列表最大的嵌套深度。例如:列表[1, 2, 3]的嵌套深度為1,列表[[1], [2, [3]]]的嵌套深度為3。
def list_depth(items):if isinstance(items, list):max_depth = 1for item in items:max_depth = max(list_depth(item) + 1, max_depth)return max_depthreturn 0點(diǎn)評(píng):看到題目應(yīng)該能夠比較自然的想到使用遞歸的方式檢查列表中的每個(gè)元素。
題目48:按照題目要求寫(xiě)出對(duì)應(yīng)的裝飾器。
要求:有一個(gè)通過(guò)網(wǎng)絡(luò)獲取數(shù)據(jù)的函數(shù)(可能會(huì)因?yàn)榫W(wǎng)絡(luò)原因出現(xiàn)異常),寫(xiě)一個(gè)裝飾器讓這個(gè)函數(shù)在出現(xiàn)指定異常時(shí)可以重試指定的次數(shù),并在每次重試之前隨機(jī)延遲一段時(shí)間,最長(zhǎng)延遲時(shí)間可以通過(guò)參數(shù)進(jìn)行控制。
方法一:
from functools import wraps from random import random from time import sleepdef retry(*, retry_times=3, max_wait_secs=5, errors=(Exception, )):def decorate(func):@wraps(func)def wrapper(*args, **kwargs):for _ in range(retry_times):try:return func(*args, **kwargs)except errors:sleep(random() * max_wait_secs)return Nonereturn wrapperreturn decorate方法二:
from functools import wraps from random import random from time import sleepclass Retry(object):def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):self.retry_times = retry_timesself.max_wait_secs = max_wait_secsself.errors = errorsdef __call__(self, func):@wraps(func)def wrapper(*args, **kwargs):for _ in range(self.retry_times):try:return func(*args, **kwargs)except self.errors:sleep(random() * self.max_wait_secs)return Nonereturn wrapper點(diǎn)評(píng):我們不止一次強(qiáng)調(diào)過(guò),裝飾器幾乎是Python面試必問(wèn)內(nèi)容,這個(gè)題目比之前的題目稍微復(fù)雜一些,它需要的是一個(gè)參數(shù)化的裝飾器。
題目49:寫(xiě)一個(gè)函數(shù)實(shí)現(xiàn)字符串反轉(zhuǎn),盡可能寫(xiě)出你知道的所有方法。
點(diǎn)評(píng):爛大街的題目,基本上算是送人頭的題目。
方法一:反向切片
def reverse_string(content):return content[::-1]方法二:反轉(zhuǎn)拼接
def reverse_string(content):return ''.join(reversed(content))方法三:遞歸調(diào)用
def reverse_string(content):if len(content) <= 1:return contentreturn reverse_string(content[1:]) + content[0]方法四:雙端隊(duì)列
from collections import dequedef reverse_string(content):q = deque()q.extendleft(content)return ''.join(q)方法五:反向組裝
from io import StringIOdef reverse_string(content):buffer = StringIO()for i in range(len(content) - 1, -1, -1):buffer.write(content[i])return buffer.getvalue()方法六:反轉(zhuǎn)拼接
def reverse_string(content):return ''.join([content[i] for i in range(len(content) - 1, -1, -1)])方法七:半截交換
def reverse_string(content):length, content= len(content), list(content)for i in range(length // 2):content[i], content[length - 1 - i] = content[length - 1 - i], content[i]return ''.join(content)方法八:對(duì)位交換
def reverse_string(content):length, content= len(content), list(content)for i, j in zip(range(length // 2), range(length - 1, length // 2 - 1, -1)):content[i], content[j] = content[j], content[i]return ''.join(content)擴(kuò)展:這些方法其實(shí)都是大同小異的,面試的時(shí)候能夠給出幾種有代表性的就足夠了。給大家留一個(gè)思考題,上面這些方法,哪些做法的性能較好呢?我們之前提到過(guò)剖析代碼性能的方法,大家可以用這些方法來(lái)檢驗(yàn)下你給出的答案是否正確。
題目50:按照題目要求寫(xiě)出對(duì)應(yīng)的函數(shù)。
要求:列表中有1000000個(gè)元素,取值范圍是[1000, 10000),設(shè)計(jì)一個(gè)函數(shù)找出列表中的重復(fù)元素。
def find_dup(items: list):dups = [0] * 9000for item in items:dups[item - 1000] += 1for idx, val in enumerate(dups):if val > 1:yield idx + 1000點(diǎn)評(píng):這道題的解法和計(jì)數(shù)排序的原理一致,雖然元素的數(shù)量非常多,但是取值范圍[1000, 10000)并不是很大,只有9000個(gè)可能的取值,所以可以用一個(gè)能夠保存9000個(gè)元素的dups列表來(lái)記錄每個(gè)元素出現(xiàn)的次數(shù),dups列表所有元素的初始值都是0,通過(guò)對(duì)items列表中元素的遍歷,當(dāng)出現(xiàn)某個(gè)元素時(shí),將dups列表對(duì)應(yīng)位置的值加1,最后dups列表中值大于1的元素對(duì)應(yīng)的就是items列表中重復(fù)出現(xiàn)過(guò)的元素。
總結(jié)
以上是生活随笔為你收集整理的一份Python面试宝典的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 服务器主机是什么系统版本,服务器主机是什
- 下一篇: 假币问题python