編程常見問題?
一般問題?
Python 有沒有提供帶有斷點、單步調試等功能的源碼級調試器??
有的。
以下介紹了一些 Python 的調試器,用內置函數(shù) breakpoint()
即可切入這些調試器中。
pdb 模塊是一個簡單但是夠用的控制臺模式 Python 調試器。 它是標準 Python 庫的一部分,并且 已收錄于庫參考手冊
。 你也可以通過使用 pdb 代碼作為樣例來編寫你自己的調試器。
作為標準 Python 發(fā)行版附帶組件的 IDLE 交互式環(huán)境(通常位于 Tools/scripts/idle)中包含一個圖形化的調試器。
PythonWin 是一種 Python IDE,其中包含了一個基于 pdb 的 GUI 調試器。PythonWin 的調試器會為斷點著色,并提供了相當多的超酷特性,例如調試非 PythonWin 程序等。PythonWin 是 pywin32 項目的組成部分,也是 ActivePython 發(fā)行版的組成部分。
Eric 是一個基于PyQt和Scintilla編輯組件構建的IDE。
trepan3k 是一個類似 gdb 的調試器。
Visual Studio Code 是包含了調試工具的 IDE,并集成了版本控制軟件。
有許多商業(yè) Python IDE 都包含了圖形化調試器。包括:
是否有能幫助尋找漏洞或執(zhí)行靜態(tài)分析的工具??
有的。
如何由 Python 腳本創(chuàng)建能獨立運行的二進制程序??
如果只是想要一個獨立的程序,以便用戶不必預先安裝 Python 即可下載和運行它,則不需要將 Python 編譯成 C 代碼。有許多工具可以檢測程序所需的模塊,并將這些模塊與 Python 二進制程序捆綁在一起生成單個可執(zhí)行文件。
One is to use the freeze tool, which is included in the Python source tree as
Tools/freeze
. It converts Python byte code to C arrays; with a C compiler you can
embed all your modules into a new program, which is then linked with the
standard Python modules.
它的工作原理是遞歸掃描源代碼,獲取兩種格式的 import 語句,并在標準 Python 路徑和源碼目錄(用于內置模塊)檢索這些模塊。然后,把這些模塊的 Python 字節(jié)碼轉換為 C 代碼(可以利用 marshal 模塊轉換為代碼對象的數(shù)組初始化器),并創(chuàng)建一個定制的配置文件,該文件僅包含程序實際用到的內置模塊。然后,編譯生成的 C 代碼并將其與 Python 解釋器的其余部分鏈接,形成一個自給自足的二進制文件,其功能與 Python 腳本代碼完全相同。
下列包可以用于幫助創(chuàng)建控制臺和 GUI 的可執(zhí)行文件:
Nuitka (跨平臺)
PyInstaller (跨平臺)
PyOxidizer (跨平臺)
cx_Freeze (跨平臺)
py2app (僅限 macOS)
py2exe (僅限 Windows)
是否有 Python 編碼標準或風格指南??
有的。 標準庫模塊所要求的編碼風格記錄于 PEP 8 之中。
語言核心內容?
變量明明有值,為什么還會出現(xiàn) UnboundLocalError??
因為在函數(shù)內部某處添加了一條賦值語句,導致之前正常工作的代碼報出 UnboundLocalError 錯誤,這可能是有點令人驚訝。
以下代碼:
>>> x = 10
>>> def bar():
... print(x)
>>> bar()
10
正常工作,但是以下代碼
>>> x = 10
>>> def foo():
... print(x)
... x += 1
會得到一個 UnboundLocalError :
>>> foo()
Traceback (most recent call last):
...
UnboundLocalError: local variable 'x' referenced before assignment
原因就是,當對某作用域內的變量進行賦值時,該變量將成為該作用域內的局部變量,并覆蓋外部作用域中的同名變量。由于 foo 的最后一條語句為 x
分配了一個新值,編譯器會將其識別為局部變量。因此,前面的 print(x)
試圖輸出未初始化的局部變量,就會引發(fā)錯誤。
在上面的示例中,可以將外部作用域的變量聲明為全局變量以便訪問:
>>> x = 10
>>> def foobar():
... global x
... print(x)
... x += 1
>>> foobar()
10
與類和實例變量貌似但不一樣,其實以上是在修改外部作用域的變量值,為了提示這一點,這里需要顯式聲明一下。
>>> print(x)
11
你可以使用 nonlocal
關鍵字在嵌套作用域中執(zhí)行類似的操作:
>>> def foo():
... x = 10
... def bar():
... nonlocal x
... print(x)
... x += 1
... bar()
... print(x)
>>> foo()
10
11
Python 的局部變量和全局變量有哪些規(guī)則??
函數(shù)內部只作引用的 Python 變量隱式視為全局變量。如果在函數(shù)內部任何位置為變量賦值,則除非明確聲明為全局變量,否則均將其視為局部變量。
起初盡管有點令人驚訝,不過考慮片刻即可釋然。一方面,已分配的變量要求加上 global
可以防止意外的副作用發(fā)生。另一方面,如果所有全局引用都要加上 global
,那處處都得用上 global
了。那么每次對內置函數(shù)或導入模塊中的組件進行引用時,都得聲明為全局變量。這種雜亂會破壞 global
聲明用于警示副作用的有效性。
為什么在循環(huán)中定義的參數(shù)各異的 lambda 都返回相同的結果??
假設用 for 循環(huán)來定義幾個取值各異的 lambda(即便是普通函數(shù)也一樣):
>>> squares = []
>>> for x in range(5):
... squares.append(lambda: x**2)
以上會得到一個包含5個 lambda 函數(shù)的列表,這些函數(shù)將計算 x**2
。大家或許期望,調用這些函數(shù)會分別返回 0
、1
、 4
、 9
和 16
。然而,真的試過就會發(fā)現(xiàn),他們都會返回 16
:
>>> squares[2]()
16
>>> squares[4]()
16
這是因為 x
不是 lambda 函數(shù)的內部變量,而是定義于外部作用域中的,并且 x
是在調用 lambda 時訪問的——而不是在定義時訪問。循環(huán)結束時 x
的值是 4
,所以此時所有的函數(shù)都將返回 4**2
,即 16
。通過改變 x
的值并查看 lambda 的結果變化,也可以驗證這一點。
>>> x = 8
>>> squares[2]()
64
為了避免發(fā)生上述情況,需要將值保存在 lambda 局部變量,以使其不依賴于全局 x
的值:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda n=x: n**2)
以上 n=x
創(chuàng)建了一個新的 lambda 本地變量 n
,并在定義 lambda 時計算其值,使其與循環(huán)當前時點的 x
值相同。這意味著 n
的值在第 1 個 lambda 中為 0
,在第 2 個 lambda 中為 1
,在第 3 個中為 2
,依此類推。因此現(xiàn)在每個 lambda 都會返回正確結果:
>>> squares[2]()
4
>>> squares[4]()
16
請注意,上述表現(xiàn)并不是 lambda 所特有的,常規(guī)的函數(shù)也同樣適用。
導入模塊的“最佳實踐”是什么??
通常請勿使用 from modulename import *
。因為這會擾亂 importer 的命名空間,且會造成未定義名稱更難以被 Linter 檢查出來。
請在代碼文件的首部就導入模塊。這樣代碼所需的模塊就一目了然了,也不用考慮模塊名是否在作用域內的問題。每行導入一個模塊則增刪起來會比較容易,每行導入多個模塊則更節(jié)省屏幕空間。
按如下順序導入模塊就是一種好做法:
標準庫模塊——比如:
sys
、os
、getopt
、re
等。第三方庫模塊(安裝于 Python site-packages 目錄中的內容)——如 mx.DateTime、ZODB、PIL.Image 等。
本地開發(fā)的模塊
為了避免循環(huán)導入引發(fā)的問題,有時需要將模塊導入語句移入函數(shù)或類的內部。Gordon McMillan 的說法如下:
當兩個模塊都采用 "import <module>" 的導入形式時,循環(huán)導入是沒有問題的。但如果第 2 個模塊想從第 1 個模塊中取出一個名稱("from module import name")并且導入處于代碼的最頂層,那導入就會失敗。原因是第 1 個模塊中的名稱還不可用,這時第 1 個模塊正忙于導入第 2 個模塊呢。
如果只是在一個函數(shù)中用到第 2 個模塊,那這時將導入語句移入該函數(shù)內部即可。當調用到導入語句時,第 1 個模塊將已經完成初始化,第 2 個模塊就可以進行導入了。
如果某些模塊是平臺相關的,可能還需要把導入語句移出最頂級代碼。這種情況下,甚至有可能無法導入文件首部的所有模塊。于是在對應的平臺相關代碼中導入正確的模塊,就是一種不錯的選擇。
只有為了避免循環(huán)導入問題,或有必要減少模塊初始化時間時,才把導入語句移入類似函數(shù)定義內部的局部作用域。如果根據程序的執(zhí)行方式,許多導入操作不是必需的,那么這種技術尤其有用。如果模塊僅在某個函數(shù)中用到,可能還要將導入操作移入該函數(shù)內部。請注意,因為模塊有一次初始化過程,所以第一次加載模塊的代價可能會比較高,但多次加載幾乎沒有什么花費,代價只是進行幾次字典檢索而已。即使模塊名超出了作用域,模塊在 sys.modules
中也是可用的。
如何將可選參數(shù)或關鍵字參數(shù)從一個函數(shù)傳遞到另一個函數(shù)??
請利用函數(shù)參數(shù)列表中的標識符 *
和 **
歸集實參;結果會是元組形式的位置實參和字典形式的關鍵字實參。然后就可利用 *
和 **
在調用其他函數(shù)時傳入這些實參:
def f(x, *args, **kwargs):
...
kwargs['width'] = '14.3c'
...
g(x, *args, **kwargs)
形參和實參之間有什么區(qū)別??
形參 是指出現(xiàn)在函數(shù)定義中的名稱,而 實參 則是在調用函數(shù)時實際傳入的值。 形參定義了一個函數(shù)能接受何種類型的實參。 例如,對于以下函數(shù)定義:
def func(foo, bar=None, **kwargs):
pass
foo 、 bar 和 kwargs 是 func
的形參。 不過在調用 func
時,例如:
func(42, bar=314, extra=somevar)
42
、 314
和 somevar
則是實參。
為什么修改列表 'y' 也會更改列表 'x'??
如果代碼編寫如下:
>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]
或許大家很想知道,為什么在 y 中添加一個元素時, x 也會改變。
產生這種結果有兩個因素:
變量只是指向對象的一個名稱。執(zhí)行
y = x
并不會創(chuàng)建列表的副本——而只是創(chuàng)建了一個新變量y
,并指向x
所指的同一對象。這就意味著只存在一個列表對象,x
和y
都是對它的引用。列表屬于 mutable 對象,這意味著它的內容是可以修改的。
在調用 append()
之后,該可變對象的內容由 []
變?yōu)?[10]
。由于 x 和 y 這兩個變量引用了同一對象,因此用其中任意一個名稱所訪問到的都是修改后的值 [10]
。
如果把賦給 x
的對象換成一個不可變對象:
>>> x = 5 # ints are immutable
>>> y = x
>>> x = x + 1 # 5 can't be mutated, we are creating a new object here
>>> x
6
>>> y
5
可見這時 x
和 y
就不再相等了。因為整數(shù)是 immutable 對象,在執(zhí)行 x = x + 1
時,并不會修改整數(shù)對象 5
,給它加上 1;而是創(chuàng)建了一個新的對象(整數(shù)對象 6
)并將其賦給 x
(也就是改變了 x
所指向的對象)。在賦值完成后,就有了兩個對象(整數(shù)對象 6
和 5
)和分別指向他倆的兩個變量( x
現(xiàn)在指向 6
而 y
仍然指向 5
)。
某些操作(例如 y.append(10)
和 y.sort()
)會直接修改原對象,而看上去相似的另一些操作(例如 y = y + [10]
和 sorted(y)
)則會創(chuàng)建新的對象。通常在 Python 中(以及所有標準庫),直接修改原對象的方法將會返回 None
,以助避免混淆這兩種不同類型的操作。因此如果誤用了 y.sort()
并期望返回 y
的有序副本,則結果只會是 None
,這可能就能讓程序引發(fā)一條容易診斷的錯誤。
不過還存在一類操作,用不同的類型執(zhí)行相同的操作有時會發(fā)生不同的行為:即增量賦值運算符。例如,+=
會修改列表,但不會修改元組或整數(shù)(a_list += [1, 2, 3]
與 a_list.extend([1, 2, 3])
同樣都會改變 a_list
,而 some_tuple += (1, 2, 3)
和 some_int += 1
則會創(chuàng)建新的對象)。
換而言之:
如何編寫帶有輸出參數(shù)的函數(shù)(按照引用調用)??
請記住,Python 中的實參是通過賦值傳遞的。由于賦值只是創(chuàng)建了對象的引用,所以調用方和被調用方的參數(shù)名都不存在別名,本質上也就不存在按引用調用的方式。通過以下幾種方式,可以得到所需的效果。
返回一個元組:
>>> def func1(a, b): ... a = 'new-value' # a and b are local names ... b = b + 1 # assigned to new objects ... return a, b # return new values ... >>> x, y = 'old-value', 99 >>> func1(x, y) ('new-value', 100)
這差不多是最明晰的解決方案了。
使用全局變量。這不是線程安全的方案,不推薦使用。
傳遞一個可變(即可原地修改的) 對象:
>>> def func2(a): ... a[0] = 'new-value' # 'a' references a mutable list ... a[1] = a[1] + 1 # changes a shared object ... >>> args = ['old-value', 99] >>> func2(args) >>> args ['new-value', 100]
傳入一個接收可變對象的字典:
>>> def func3(args): ... args['a'] = 'new-value' # args is a mutable dictionary ... args['b'] = args['b'] + 1 # change it in-place ... >>> args = {'a': 'old-value', 'b': 99} >>> func3(args) >>> args {'a': 'new-value', 'b': 100}
或者把值用類實例封裝起來:
>>> class Namespace: ... def __init__(self, /, **args): ... for key, value in args.items(): ... setattr(self, key, value) ... >>> def func4(args): ... args.a = 'new-value' # args is a mutable Namespace ... args.b = args.b + 1 # change object in-place ... >>> args = Namespace(a='old-value', b=99) >>> func4(args) >>> vars(args) {'a': 'new-value', 'b': 100}
沒有什么理由要把問題搞得這么復雜。
最佳選擇就是返回一個包含多個結果值的元組。
如何在 Python 中創(chuàng)建高階函數(shù)??
有兩種選擇:嵌套作用域、可調用對象。假定需要定義 linear(a,b)
,其返回結果是一個計算出 a*x+b
的函數(shù) f(x)
。 采用嵌套作用域的方案如下:
def linear(a, b):
def result(x):
return a * x + b
return result
或者可采用可調用對象:
class linear:
def __init__(self, a, b):
self.a, self.b = a, b
def __call__(self, x):
return self.a * x + self.b
采用這兩種方案時:
taxes = linear(0.3, 2)
都會得到一個可調用對象,可實現(xiàn) taxes(10e6) == 0.3 * 10e6 + 2
。
可調用對象的方案有個缺點,就是速度稍慢且生成的代碼略長。不過值得注意的是,同一組可調用對象能夠通過繼承來共享簽名(類聲明):
class exponential(linear):
# __init__ inherited
def __call__(self, x):
return self.a * (x ** self.b)
對象可以為多個方法的運行狀態(tài)進行封裝:
class counter:
value = 0
def set(self, x):
self.value = x
def up(self):
self.value = self.value + 1
def down(self):
self.value = self.value - 1
count = counter()
inc, dec, reset = count.up, count.down, count.set
以上 inc()
、 dec()
和 reset()
的表現(xiàn),就如同共享了同一計數(shù)變量一樣。
如何復制 Python 對象??
一般情況下,用 copy.copy()
或 copy.deepcopy()
基本就可以了。并不是所有對象都支持復制,但多數(shù)是可以的。
某些對象可以用更簡便的方法進行復制。比如字典對象就提供了 copy()
方法:
newdict = olddict.copy()
序列可以用切片操作進行復制:
new_l = l[:]
如何找到對象的方法或屬性??
假定 x 是一個用戶自定義類的實例,dir(x)
將返回一個按字母排序的名稱列表,其中包含了實例的屬性及由類定義的方法和屬性。
如何用代碼獲取對象的名稱??
一般而言這是無法實現(xiàn)的,因為對象并不存在真正的名稱。賦值本質上是把某個名稱綁定到某個值上;def
和 class
語句同樣如此,只是值換成了某個可調用對象。比如以下代碼:
>>> class A:
... pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>
可以不太嚴謹?shù)卣f,上述類具有一個名稱:即便它綁定了兩個名稱并通過名稱 B 發(fā)起調用,可是創(chuàng)建出來的實例仍被視為是類 A 的實例。但無法說出實例的名稱是 a 還是 b,因為這兩個名稱都被綁定到同一個值上了。
代碼一般沒有必要去“知曉”某個值的名稱。通常這種需求預示著還是改變方案為好,除非真的是要編寫內審程序。
在 comp.lang.python 中,F(xiàn)redrik Lundh 在回答這樣的問題時曾經給出過一個絕佳的類比:
這就像要知道家門口的那只貓的名字一樣:貓(對象)自己不會說出它的名字,它根本就不在乎自己叫什么——所以唯一方法就是問一遍你所有的鄰居(命名空間),這是不是他們家的貓(對象)……
……并且如果你發(fā)現(xiàn)它有很多名字或根本沒有名字,那也不必驚訝!
逗號運算符的優(yōu)先級是什么??
逗號不是 Python 的運算符。 請看以下例子:
>>> "a" in "b", "a"
(False, 'a')
由于逗號不是運算符,而只是表達式之間的分隔符,因此上述代碼就相當于:
("a" in "b"), "a"
而不是:
"a" in ("b", "a")
對于各種賦值運算符( =
、 +=
等)來說同樣如此。他們并不是真正的運算符,而只是賦值語句中的語法分隔符。
是否提供等價于 C 語言 "?:" 三目運算符的東西??
有的。語法如下:
[on_true] if [expression] else [on_false]
x, y = 50, 25
small = x if x < y else y
在 Python 2.5 引入上述語法之前,通常的做法是使用邏輯運算符:
[expression] and [on_true] or [on_false]
然而這種做法并不保險,因為當 on_true 為布爾值“假”時,結果將會出錯。所以肯定還是采用 ... if ... else ...
形式為妙。
是否可以用 Python 編寫讓人眼暈的單行程序??
可以。通常是在 lambda
中嵌套 lambda
來實現(xiàn)的。請參閱以下三個來自 Ulf Bartelt 的示例代碼:
from functools import reduce
# Primes < 1000
print(list(filter(None,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))
# First 10 Fibonacci numbers
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), range(10))))
# Mandelbrot set
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+y,map(lambda y,
Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM,
Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro,
i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k<=0)or (x*x+y*y
>=4.0) or 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr(
64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy),range(Sy
))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24))
# \___ ___/ \___ ___/ | | |__ lines on screen
# V V | |______ columns on screen
# | | |__________ maximum of "iterations"
# | |_________________ range on y axis
# |____________________________ range on x axis
請不要在家里嘗試,騷年!
函數(shù)形參列表中的斜杠(/)是什么意思??
函數(shù)參數(shù)列表中的斜杠表示在它之前的形參全都僅限位置形參。僅限位置形參沒有可供外部使用的名稱。在調用僅接受位置形參的函數(shù)時,實參只會根據位置映射到形參上。假定 divmod()
是一個僅接受位置形參的函數(shù)。 它的幫助文檔如下所示:
>>> help(divmod)
Help on built-in function divmod in module builtins:
divmod(x, y, /)
Return the tuple (x//y, x%y). Invariant: div*y + mod == x.
形參列表尾部的斜杠說明,兩個形參都是僅限位置形參。因此,用關鍵字參數(shù)調用 divmod()
將會引發(fā)錯誤:
>>> divmod(x=3, y=4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments
數(shù)字和字符串?
如何給出十六進制和八進制整數(shù)??
要給出八進制數(shù),需在八進制數(shù)值前面加上一個零和一個小寫或大寫字母 "o" 作為前綴。例如,要將變量 "a" 設為八進制的 "10" (十進制的 8),寫法如下:
>>> a = 0o10
>>> a
8
十六進制數(shù)也很簡單。只要在十六進制數(shù)前面加上一個零和一個小寫或大寫的字母 "x"。十六進制數(shù)中的字母可以為大寫或小寫。比如在 Python 解釋器中輸入:
>>> a = 0xa5
>>> a
165
>>> b = 0XB2
>>> b
178
為什么 -22 // 10 會返回 -3 ??
這主要是為了讓 i % j
的正負與 j
一致,如果期望如此,且期望如下等式成立:
i == (i // j) * j + (i % j)
那么整除就必須返回向下取整的結果。C 語言同樣要求保持這種一致性,于是編譯器在截斷 i // j
的結果時需要讓 i % j
的正負與 i
一致。
對于 i % j
來說 j
為負值的應用場景實際上是非常少的。 而 j
為正值的情況則非常多,并且實際上在所有情況下讓 i % j
的結果為 >= 0
會更有用處。 如果現(xiàn)在時間為 10 時,那么 200 小時前應是幾時? -190 % 12 == 2
是有用處的;-190 % 12 == -10
則是會導致意外的漏洞。
How do I get int literal attribute instead of SyntaxError??
Trying to lookup an int
literal attribute in the normal manner gives
a syntax error because the period is seen as a decimal point:
>>> 1.__class__
File "<stdin>", line 1
1.__class__
^
SyntaxError: invalid decimal literal
The solution is to separate the literal from the period with either a space or parentheses.
>>> 1 .__class__
<class 'int'>
>>> (1).__class__
<class 'int'>
如何將字符串轉換為數(shù)字??
對于整數(shù),可使用內置的 int()
類型構造器,例如 int('144') == 144
。 類似地,可使用 float()
轉換為浮點數(shù),例如 float('144') == 144.0
。
默認情況下,這些操作會將數(shù)字按十進制來解讀,因此 int('0144') == 144
為真值,而 int('0x144')
會引發(fā) ValueError
。 int(string, base)
接受第二個可選參數(shù)指定轉換的基數(shù),例如 int( '0x144', 16) == 324
。 如果指定基數(shù)為 0,則按 Python 規(guī)則解讀數(shù)字:前綴 '0o' 表示八進制,而 '0x' 表示十六進制。
如果只是想把字符串轉為數(shù)字,請不要使用內置函數(shù) eval()
。 eval()
的速度慢很多且存在安全風險:別人可能會傳入帶有不良副作用的 Python 表達式。比如可能會傳入 __import__('os').system("rm -rf $HOME")
,這會把 home 目錄給刪了。
eval()
還有把數(shù)字解析為 Python 表達式的后果,因此如 eval('09')
將會導致語法錯誤,因為 Python 不允許十進制數(shù)帶有前導 '0'('0' 除外)。
如何將數(shù)字轉換為字符串??
比如要把數(shù)字 144 轉換為字符串 '144',可使用內置類型構造器 str()
。如果要表示為十六進制或八進制數(shù)格式,可使用內置函數(shù) hex()
或 oct()
。更復雜的格式化方法請參閱 格式字符串字面值 和 格式字符串語法 等章節(jié),比如 "{:04d}".format(144)
會生成 '0144'
, "{:.3f}".format(1.0/3.0)
則會生成 '0.333'
。
如何修改字符串??
無法修改,因為字符串是不可變對象。 在大多數(shù)情況下,只要將各個部分組合起來構造出一個新字符串即可。如果需要一個能原地修改 Unicode 數(shù)據的對象,可以試試 io.StringIO
對象或 array
模塊:
>>> import io
>>> s = "Hello, world"
>>> sio = io.StringIO(s)
>>> sio.getvalue()
'Hello, world'
>>> sio.seek(7)
7
>>> sio.write("there!")
6
>>> sio.getvalue()
'Hello, there!'
>>> import array
>>> a = array.array('u', s)
>>> print(a)
array('u', 'Hello, world')
>>> a[0] = 'y'
>>> print(a)
array('u', 'yello, world')
>>> a.tounicode()
'yello, world'
如何使用字符串調用函數(shù)/方法??
有多種技巧可供選擇。
最好的做法是采用一個字典,將字符串映射為函數(shù)。其主要優(yōu)勢就是字符串不必與函數(shù)名一樣。這也是用來模擬 case 結構的主要技巧:
def a(): pass def b(): pass dispatch = {'go': a, 'stop': b} # Note lack of parens for funcs dispatch[get_input()]() # Note trailing parens to call function
利用內置函數(shù)
getattr()
:import foo getattr(foo, 'bar')()
請注意
getattr()
可用于任何對象,包括類、類實例、模塊等等。標準庫就多次使用了這個技巧,例如:
class Foo: def do_foo(self): ... def do_bar(self): ... f = getattr(foo_instance, 'do_' + opname) f()
用
locals()
解析出函數(shù)名:def myFunc(): print("hello") fname = "myFunc" f = locals()[fname] f()
是否有與Perl 的chomp() 等效的方法,用于從字符串中刪除尾隨換行符??
可以使用 S.rstrip("\r\n")
從字符串 S
的末尾刪除所有的換行符,而不刪除其他尾隨空格。如果字符串 S
表示多行,且末尾有幾個空行,則將刪除所有空行的換行符:
>>> lines = ("line 1 \r\n"
... "\r\n"
... "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '
由于通常只在一次讀取一行文本時才需要這樣做,所以使用 S.rstrip()
這種方式工作得很好。
是否有 scanf() 或 sscanf() 的等價函數(shù)??
沒有。
如果要對簡單的輸入進行解析,最容易的做法通常是利用字符串對象的 split()
方法將一行按空白符分隔拆分為多個單詞,然后用 int()
或 float()
將十進制數(shù)字符串轉換為數(shù)字值。 split()
支持可選的 "sep" 形參,適用于分隔符不用空白符的情況。
如果要對更復雜的輸入進行解析,那么正則表達式要比 C 語言的 sscanf()
更強大,也更合適。
'UnicodeDecodeError' 或 'UnicodeEncodeError' 錯誤是什么意思??
性能?
我的程序太慢了。該如何加快速度??
總的來說,這是個棘手的問題。在進一步討論之前,首先應該記住以下幾件事:
不同的 Python 實現(xiàn)具有不同的性能特點。 本 FAQ 著重解答的是 CPython。
不同操作系統(tǒng)可能會有不同表現(xiàn),尤其是涉及 I/O 和多線程時。
在嘗試優(yōu)化代碼 之前 ,務必要先找出程序中的熱點(請參閱
profile
模塊)。編寫基準測試腳本,在尋求性能提升的過程中就能實現(xiàn)快速迭代(請參閱
timeit
模塊)。強烈建議首先要保證足夠高的代碼測試覆蓋率(通過單元測試或其他技術),因為復雜的優(yōu)化有可能會導致代碼回退。
話雖如此,Python 代碼的提速還是有很多技巧的。以下列出了一些普適性的原則,對于讓性能達到可接受的水平會有很大幫助:
相較于試圖對全部代碼鋪開做微觀優(yōu)化,優(yōu)化算法(或換用更快的算法)可以產出更大的收益。
使用正確的數(shù)據結構。參考 內置類型 和
collections
模塊的文檔。如果標準庫已為某些操作提供了基礎函數(shù),則可能(當然不能保證)比所有自編的函數(shù)都要快。對于用 C 語言編寫的基礎函數(shù)則更是如此,比如內置函數(shù)和一些擴展類型。例如,一定要用內置方法
list.sort()
或sorted()
函數(shù)進行排序(某些高級用法的示例請參閱 排序指南 )。抽象往往會造成中間層,并會迫使解釋器執(zhí)行更多的操作。如果抽象出來的中間層級太多,工作量超過了要完成的有效任務,那么程序就會被拖慢。應該避免過度的抽象,而且往往也會對可讀性產生不利影響,特別是當函數(shù)或方法比較小的時候。
如果你已經達到純 Python 允許的限制,那么有一些工具可以讓你走得更遠。 例如, Cython 可以將稍微修改的 Python 代碼版本編譯為 C 擴展,并且可以在許多不同的平臺上使用。 Cython 可以利用編譯(和可選的類型注釋)來使代碼明顯快于解釋運行時的速度。 如果您對 C 編程技能有信心,也可以自己 編寫 C 擴展模塊 。
參見
專門介紹 性能提示 的wiki頁面。
將多個字符串連接在一起的最有效方法是什么??
str
和 bytes
對象是不可變的,因此連接多個字符串的效率會很低,因為每次連接都會創(chuàng)建一個新的對象。一般情況下,總耗時與字符串總長是二次方的關系。
如果要連接多個 str
對象,通常推薦的方案是先全部放入列表,最后再調用 str.join()
:
chunks = []
for s in my_strings:
chunks.append(s)
result = ''.join(chunks)
(還有一種合理高效的習慣做法,就是利用 io.StringIO
)
如果要連接多個 bytes
對象,推薦做法是用 bytearray
對象的原地連接操作( +=
運算符)追加數(shù)據:
result = bytearray()
for b in my_bytes_objects:
result += b
序列(元組/列表)?
如何在元組和列表之間進行轉換??
類型構造器 tuple(seq)
可將任意序列(實際上是任意可迭代對象)轉換為數(shù)據項和順序均不變的元組。
例如,tuple([1, 2, 3])
會生成 (1, 2, 3)
, tuple('abc')
則會生成 ('a', 'b', 'c')
。 如果參數(shù)就是元組,則不會創(chuàng)建副本而是返回同一對象,因此如果無法確定某個對象是否為元組時,直接調用 tuple()
也沒什么代價。
類型構造器 list(seq)
可將任意序列或可迭代對象轉換為數(shù)據項和順序均不變的列表。例如,list((1, 2, 3))
會生成 [1, 2, 3]
而 list('abc')
則會生成 ['a', 'b', 'c']
。如果參數(shù)即為列表,則會像 seq[:]
那樣創(chuàng)建一個副本。
什么是負數(shù)索引??
Python 序列的索引可以是正數(shù)或負數(shù)。索引為正數(shù)時,0 是第一個索引值, 1 為第二個,依此類推。索引為負數(shù)時,-1 為倒數(shù)第一個索引值,-2 為倒數(shù)第二個,依此類推??梢哉J為 seq[-n]
就相當于 seq[len(seq)-n]
。
使用負數(shù)序號有時會很方便。 例如 S[:-1]
就是原字符串去掉最后一個字符,這可以用來移除某個字符串末尾的換行符。
序列如何以逆序遍歷??
使用內置函數(shù) reversed()
:
for x in reversed(sequence):
... # do something with x ...
原序列不會變化,而是構建一個逆序的新副本以供遍歷。
如何從列表中刪除重復項??
許多完成此操作的的詳細介紹,可參閱 Python Cookbook:
如果列表允許重新排序,不妨先對其排序,然后從列表末尾開始掃描,依次刪除重復項:
if mylist:
mylist.sort()
last = mylist[-1]
for i in range(len(mylist)-2, -1, -1):
if last == mylist[i]:
del mylist[i]
else:
last = mylist[i]
如果列表的所有元素都能用作集合的鍵(即都是 hashable ),以下做法速度往往更快:
mylist = list(set(mylist))
以上操作會將列表轉換為集合,從而刪除重復項,然后返回成列表。
如何從列表中刪除多個項??
類似于刪除重復項,一種做法是反向遍歷并根據條件刪除。不過更簡單快速的做法就是切片替換操作,采用隱式或顯式的正向迭代遍歷。以下是三種變體寫法:
mylist[:] = filter(keep_function, mylist)
mylist[:] = (x for x in mylist if keep_condition)
mylist[:] = [x for x in mylist if keep_condition]
列表推導式可能是最快的。
如何在 Python 中創(chuàng)建數(shù)組??
用列表:
["this", 1, "is", "an", "array"]
列表在時間復雜度方面相當于 C 或 Pascal 的數(shù)組;主要區(qū)別在于,Python 列表可以包含多種不同類型的對象。
array
模塊也提供了一些創(chuàng)建具有緊湊格式的固定類型數(shù)組的方法,但其索引訪問速度比列表慢。 并請注意 NumPy 和其他一些第三方包也定義了一些各具特色的數(shù)組類結構。
若要得到 Lisp 風格的列表,可以用元組模擬 cons 元素:
lisp_list = ("like", ("this", ("example", None) ) )
若要具備可變性,可以不用元組而是用列表。模擬 lisp car 函數(shù)的是 lisp_list[0]
,模擬 cdr 函數(shù)的是 lisp_list[1]
。僅當真正必要時才會這么用,因為通常這種用法要比 Python 列表慢得多。
如何創(chuàng)建多維列表??
多維數(shù)組或許會用以下方式建立:
>>> A = [[None] * 2] * 3
打印出來貌似沒錯:
>>> A
[[None, None], [None, None], [None, None]]
但如果給某一項賦值,結果會同時在多個位置體現(xiàn)出來:
>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]
原因在于用 *
對列表執(zhí)行重復操作并不會創(chuàng)建副本,而只是創(chuàng)建現(xiàn)有對象的引用。 *3
創(chuàng)建的是包含 3 個引用的列表,每個引用指向的是同一個長度為 2 的列表。1 處改動會體現(xiàn)在所有地方,這一定不是應有的方案。
推薦做法是先創(chuàng)建一個所需長度的列表,然后將每個元素都填充為一個新建列表。
A = [None] * 3
for i in range(3):
A[i] = [None] * 2
以上生成了一個包含 3 個列表的列表,每個子列表的長度為 2。也可以采用列表推導式:
w, h = 2, 3
A = [[None] * w for i in range(h)]
或者你還可以使用提供矩陣類型的擴展包;其中最著名的是 NumPy。
如何將方法應用于一系列對象??
可以使用列表推導式:
result = [obj.method() for obj in mylist]
為什么 a_tuple[i] += ['item'] 會引發(fā)異常??
這是由兩個因素共同導致的,一是增強賦值運算符屬于 賦值 運算符,二是 Python 可變和不可變對象之間的差別。
只要元組的元素指向可變對象,這時對元素進行增強賦值,那么這里介紹的內容都是適用的。在此只以 list
和 +=
舉例。
如果你寫成這樣:
>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
觸發(fā)異常的原因顯而易見: 1
會與指向(1
)的對象 a_tuple[0]
相加,生成結果對象 2
,但在試圖將運算結果 2
賦值給元組的 0
號元素時就會報錯,因為元組元素的指向無法更改。
其實在幕后,上述增強賦值語句的執(zhí)行過程大致如下:
>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
由于元組是不可變的,因此賦值這步會引發(fā)錯誤。
如果寫成以下這樣:
>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
這時觸發(fā)異常會令人略感驚訝,更讓人吃驚的是雖有報錯,但加法操作卻生效了:
>>> a_tuple[0]
['foo', 'item']
要明白為何會這樣,需要知道 (a) 如果一個對象實現(xiàn)了 __iadd__
魔法方法,在執(zhí)行 +=
增強賦值時就會調用它,并采納其返回值;(b) 對于列表而言,__iadd__
相當于在列表上調用 extend
并返回該列表。因此對于列表可以說 +=
就是 list.extend
的“快捷方式”:
>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]
這相當于:
>>> result = a_list.__iadd__([1])
>>> a_list = result
a_list 所引用的對象已被修改,而引用被修改對象的指針又重新被賦值給 a_list
。 賦值的最終結果沒有變化,因為它是引用 a_list
之前所引用的同一對象的指針,但仍然發(fā)生了賦值操作。
因此,在此元組示例中,發(fā)生的事情等同于:
>>> result = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
__iadd__
成功執(zhí)行,因此列表得到了擴充,但是雖然 result
指向了 a_tuple[0]
已經指向的同一對象,最后的賦值仍然導致了報錯,因為元組是不可變的。
我想做一個復雜的排序:能用 Python 進行施瓦茨變換嗎??
歸功于 Perl 社區(qū)的 Randal Schwartz,該技術根據度量值對列表進行排序,該度量值將每個元素映射為“順序值”。在 Python 中,請利用 list.sort()
方法的 key
參數(shù):
Isorted = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))
如何根據另一個列表的值對某列表進行排序??
將它們合并到元組的迭代器中,對結果列表進行排序,然后選擇所需的元素。
>>> list1 = ["what", "I'm", "sorting", "by"]
>>> list2 = ["something", "else", "to", "sort"]
>>> pairs = zip(list1, list2)
>>> pairs = sorted(pairs)
>>> pairs
[("I'm", 'else'), ('by', 'sort'), ('sorting', 'to'), ('what', 'something')]
>>> result = [x[1] for x in pairs]
>>> result
['else', 'sort', 'to', 'something']
對象?
什么是類??
類是通過執(zhí)行 class 語句創(chuàng)建的某種對象的類型。創(chuàng)建實例對象時,用 Class 對象作為模板,實例對象既包含了數(shù)據(屬性),又包含了數(shù)據類型特有的代碼(方法)。
類可以基于一個或多個其他類(稱之為基類)進行創(chuàng)建?;惖膶傩院头椒ǘ嫉靡岳^承。這樣對象模型就可以通過繼承不斷地進行細化。比如通用的 Mailbox
類提供了郵箱的基本訪問方法.,它的子類 MboxMailbox
、 MaildirMailbox
、 OutlookMailbox
則能夠處理各種特定的郵箱格式。
什么是方法??
方法是屬于對象的函數(shù),對于對象 x
,通常以 x.name(arguments...)
的形式調用。方法以函數(shù)的形式給出定義,位于類的定義內:
class C:
def meth(self, arg):
return arg * 2 + self.attribute
什么是 self ??
Self 只是方法的第一個參數(shù)的習慣性名稱。假定某個類中有個方法定義為 meth(self, a, b, c)
,則其實例 x
應以 x.meth(a, b, c)
的形式進行調用;而被調用的方法則應視其為做了 meth(x, a, b, c)
形式的調用。
另請參閱 為什么必須在方法定義和調用中顯式使用“self”? 。
如何檢查對象是否為給定類或其子類的一個實例??
可使用內置函數(shù) isinstance(obj, cls)
。可以檢測對象是否屬于多個類中某一個的實例,只要把單個類換成元組即可,比如 isinstance(obj, (class1, class2, ...))
,還可以檢查對象是否屬于某個 Python 內置類型,例如 isinstance(obj, str)
或 isinstance(obj, (int, float, complex))
。
請注意 isinstance()
還會檢測派生自 abstract base class 的虛繼承。 因此對于已注冊的類,即便沒有直接或間接繼承自抽象基類,對抽象基類的檢測都將返回 True
。要想檢測“真正的繼承”,請掃描類的 MRO:
from collections.abc import Mapping
class P:
pass
class C(P):
pass
Mapping.register(P)
>>> c = C()
>>> isinstance(c, C) # direct
True
>>> isinstance(c, P) # indirect
True
>>> isinstance(c, Mapping) # virtual
True
# Actual inheritance chain
>>> type(c).__mro__
(<class 'C'>, <class 'P'>, <class 'object'>)
# Test for "true inheritance"
>>> Mapping in type(c).__mro__
False
請注意,大多數(shù)程序不會經常用 isinstance()
對用戶自定義類進行檢測。 如果是自已開發(fā)的類,更合適的面向對象編程風格應該是在類中定義多種方法,以封裝特定的行為,而不是檢查對象屬于什么類再據此干不同的事。假定有如下執(zhí)行某些操作的函數(shù):
def search(obj):
if isinstance(obj, Mailbox):
... # code to search a mailbox
elif isinstance(obj, Document):
... # code to search a document
elif ...
更好的方法是在所有類上定義一個 search()
方法,然后調用它:
class Mailbox:
def search(self):
... # code to search a mailbox
class Document:
def search(self):
... # code to search a document
obj.search()
什么是委托??
委托是一種面向對象的技術(也稱為設計模式)。假設對象 x
已經存在,現(xiàn)在想要改變其某個方法的行為??梢詣?chuàng)建一個新類,其中提供了所需修改方法的新實現(xiàn),而將所有其他方法都委托給 x
的對應方法。
Python 程序員可以輕松實現(xiàn)委托。比如以下實現(xiàn)了一個類似于文件的類,只是會把所有寫入的數(shù)據轉換為大寫:
class UpperOut:
def __init__(self, outfile):
self._outfile = outfile
def write(self, s):
self._outfile.write(s.upper())
def __getattr__(self, name):
return getattr(self._outfile, name)
這里 UpperOut
類重新定義了 write()
方法,在調用下層的 self._outfile.write()
方法之前,會將參數(shù)字符串轉換為大寫。其他所有方法則都被委托給下層的 self._outfile
對象。委托是通過 __getattr__
方法完成的;請參閱 語言參考 了解有關控制屬性訪問的更多信息。
請注意,更常見情況下,委托可能會變得比較棘手。如果屬性既需要寫入又需要讀取,那么類還必須定義 __setattr__()
方法,而這時就必須十分的小心?;A的 __setattr__()
實現(xiàn)代碼大致如下:
class X:
...
def __setattr__(self, name, value):
self.__dict__[name] = value
...
大多數(shù) __setattr__()
實現(xiàn)必須修改 self.__dict__
來為自身保存局部狀態(tài),而不至于引起無限遞歸。
如何在擴展基類的派生類中調用基類中定義的方法??
使用內置的 super()
函數(shù):
class Derived(Base):
def meth(self):
super().meth() # calls Base.meth
在下面的例子中,super()
將自動根據它的調用方 (self
值) 來確定實例對象,使用 type(self).__mro__
查找 method resolution order (MRO),并返回 MRO 中位于 Derived
之后的項: Base
。
如何讓代碼更容易對基類進行修改??
可以為基類賦一個別名并基于該別名進行派生。這樣只要修改賦給該別名的值即可。順便提一下,如要動態(tài)地確定(例如根據可用的資源)該使用哪個基類,這個技巧也非常方便。例如:
class Base:
...
BaseAlias = Base
class Derived(BaseAlias):
...
如何創(chuàng)建靜態(tài)類數(shù)據和靜態(tài)類方法??
Python 支持靜態(tài)數(shù)據和靜態(tài)方法(以 C++ 或 Java 的定義而言)。
靜態(tài)數(shù)據只需定義一個類屬性即可。若要為屬性賦新值,則必須在賦值時顯式使用類名:
class C:
count = 0 # number of times C.__init__ called
def __init__(self):
C.count = C.count + 1
def getcount(self):
return C.count # or return self.count
對于所有符合 isinstance(c, C)
的 c
, c.count
也同樣指向 C.count
,除非被 c
自身重載,或者被從 c.__class__
回溯到基類 C
的搜索路徑上的某個類所重載。
注意:在 C 的某個方法內部,像 self.count = 42
這樣的賦值將在 self
自身的字典中新建一個名為 "count" 的不相關實例。 想要重新綁定類靜態(tài)數(shù)據名稱就必須總是指明類名,無論是在方法內部還是外部:
C.count = 314
Python 支持靜態(tài)方法:
class C:
@staticmethod
def static(arg1, arg2, arg3):
# No 'self' parameter!
...
不過為了獲得靜態(tài)方法的效果,還有一種做法直接得多,也即使用模塊級函數(shù)即可:
def getcount():
return C.count
如果代碼的結構化比較充分,每個模塊只定義了一個類(或者多個類的層次關系密切相關),那就具備了應有的封裝。
在 Python 中如何重載構造函數(shù)(或方法)??
這個答案實際上適用于所有方法,但問題通常首先出現(xiàn)于構造函數(shù)的應用場景中。
在 C++ 中,代碼會如下所示:
class C {
C() { cout << "No arguments\n"; }
C(int i) { cout << "Argument is " << i << "\n"; }
}
在 Python 中,只能編寫一個構造函數(shù),并用默認參數(shù)捕獲所有情況。例如:
class C:
def __init__(self, i=None):
if i is None:
print("No arguments")
else:
print("Argument is", i)
這不完全等同,但在實踐中足夠接近。
也可以試試采用變長參數(shù)列表,例如:
def __init__(self, *args):
...
上述做法同樣適用于所有方法定義。
在用 __spam 的時候得到一個類似 _SomeClassName__spam 的錯誤信息。?
以雙下劃線打頭的變量名會被“破壞”,以便以一種簡單高效的方式定義類私有變量。任何形式為 __spam
的標識符(至少前綴兩個下劃線,至多后綴一個下劃線)文本均會被替換為 _classname__spam
,其中 classname
為去除了全部前綴下劃線的當前類名稱。
這并不能保證私密性:外部用戶仍然可以訪問 "_classname__spam" 屬性,私有變量值也在對象的 __dict__
中可見。 許多 Python 程序員根本不操心要去使用私有變量名。
類定義了 __del__ 方法,但是刪除對象時沒有調用它。?
這有幾個可能的原因。
del 語句不一定調用 __del__()
—— 它只是減少對象的引用計數(shù),如果(引用計數(shù))達到零,才會調用 __del__()
。
如果數(shù)據結構包含循環(huán)鏈接(比如樹的每個子節(jié)點都帶有父節(jié)點的引用,而每個父節(jié)點也帶有子節(jié)點的列表),則引用計數(shù)永遠不會回零。盡管 Python 偶爾會用某種算法檢測這種循環(huán)引用,但在數(shù)據結構的最后一條引用消失之后,垃圾收集器可能還要過段時間才會運行,因此 __del__()
方法可能會在不方便和隨機的時刻被調用。這對于重現(xiàn)一個問題,是非常不方便的。更糟糕的是,各個對象的 __del__()
方法是以隨機順序執(zhí)行的。雖然可以運行 gc.collect()
來強制執(zhí)行垃圾回收工作,但 仍會存在 一些對象永遠不會被回收的失控情況。
盡管有垃圾回收器的存在,但為對象定義顯式的 close()
方法,只要一用完即可供調用,這依然是一個好主意。這樣 close()
方法即可刪除引用子對象的屬性。請勿直接調用 __del__()
—— 而 __del__()
應該調用 close()
,并且應能確??梢詫ν粚ο蠖啻握{用 close()
。
另一種避免循環(huán)引用的做法是利用 weakref
模塊,該模塊允許指向對象但不增加其引用計數(shù)。例如,樹狀數(shù)據結構應該對父節(jié)點和同級節(jié)點使用弱引用(如果真要用的話!)
最后提一下,如果 __del__()
方法引發(fā)了異常,會將警告消息打印到 sys.stderr
。
如何獲取給定類的所有實例的列表??
Python 不會記錄類(或內置類型)的實例??梢栽陬惖臉嬙旌瘮?shù)中編寫代碼,通過保留每個實例的弱引用列表來跟蹤所有實例。
為什么 id()
的結果看起來不是唯一的??
id()
返回一個整數(shù),該整數(shù)在對象的生命周期內保證是唯一的。 因為在 CPython 中,這是對象的內存地址,所以經常發(fā)生在從內存中刪除對象之后,下一個新創(chuàng)建的對象被分配在內存中的相同位置。 這個例子說明了這一點:
>>> id(1000)
13901272
>>> id(2000)
13901272
這兩個 id 屬于不同的整數(shù)對象,之前先創(chuàng)建了對象,執(zhí)行 id()
調用后又立即被刪除了。若要確保檢測 id 時的對象仍處于活動狀態(tài),請再創(chuàng)建一個對該對象的引用:
>>> a = 1000; b = 2000
>>> id(a)
13901272
>>> id(b)
13891296
什么情況下可以依靠 is 運算符進行對象的身份相等性測試??
is
運算符可用于測試對象的身份相等性。a is b
等價于 id(a) == id(b)
。
身份相等性最重要的特性就是對象總是等同于自身,a is a
一定返回 True
。身份相等性測試的速度通常比相等性測試要快。而且與相等性測試不一樣,身份相等性測試會確保返回布爾值 True
或 False
。
但是,身份相等性測試 只能 在對象身份確定的場景下才可替代相等性測試。一般來說,有以下3種情況對象身份是可以確定的:
1) 賦值操作創(chuàng)建了新的名稱但沒有改變對象身份。 在賦值操作 new = old
之后,可以保證 new is old
。
2) 將對象置入存放對象引用的容器,對象身份不會改變。在列表賦值操作 s[0] = x
之后,可以保證 s[0] is x
。
3) 單例對象,也即該對象只能存在一個實例。在賦值操作 a = None
和 b = None
之后,可以保證 a is b
,因為 None
是單例對象。
其他大多數(shù)情況下,都不建議使用身份相等性測試,而應采用相等性測試。尤其是不應將身份相等性測試用于檢測常量值,例如 int
和 str
,因為他們并不一定是單例對象:
>>> a = 1000
>>> b = 500
>>> c = b + 500
>>> a is c
False
>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a is c
False
同樣地,可變容器的新實例,對象身份一定不同:
>>> a = []
>>> b = []
>>> a is b
False
在標準庫代碼中,給出了一些正確使用對象身份測試的常見模式:
1) 正如 PEP 8 所推薦的,對象身份測試是 None
值的推薦檢測方式。這樣的代碼讀起來就像自然的英文,并可以避免與其他可能為布爾值且計算結果為 False 的對象相混淆。
2) Detecting optional arguments can be tricky when None
is a valid input
value. In those situations, you can create a singleton sentinel object
guaranteed to be distinct from other objects. For example, here is how
to implement a method that behaves like dict.pop()
:
_sentinel = object()
def pop(self, key, default=_sentinel):
if key in self:
value = self[key]
del self[key]
return value
if default is _sentinel:
raise KeyError(key)
return default
3) 編寫容器的實現(xiàn)代碼時,有時需要用對象身份測試來加強相等性檢測。這樣代碼就不會被 float('NaN')
這類與自身不相等的對象所干擾。
例如,以下是 collections.abc.Sequence.__contains__()
的實現(xiàn)代碼:
def __contains__(self, value):
for v in self:
if v is value or v == value:
return True
return False
How can a subclass control what data is stored in an immutable instance??
When subclassing an immutable type, override the __new__()
method
instead of the __init__()
method. The latter only runs after an
instance is created, which is too late to alter data in an immutable
instance.
All of these immutable classes have a different signature than their parent class:
from datetime import date
class FirstOfMonthDate(date):
"Always choose the first day of the month"
def __new__(cls, year, month, day):
return super().__new__(cls, year, month, 1)
class NamedInt(int):
"Allow text names for some numbers"
xlat = {'zero': 0, 'one': 1, 'ten': 10}
def __new__(cls, value):
value = cls.xlat.get(value, value)
return super().__new__(cls, value)
class TitleStr(str):
"Convert str to name suitable for a URL path"
def __new__(cls, s):
s = s.lower().replace(' ', '-')
s = ''.join([c for c in s if c.isalnum() or c == '-'])
return super().__new__(cls, s)
The classes can be used like this:
>>> FirstOfMonthDate(2012, 2, 14)
FirstOfMonthDate(2012, 2, 1)
>>> NamedInt('ten')
10
>>> NamedInt(20)
20
>>> TitleStr('Blog: Why Python Rocks')
'blog-why-python-rocks'
我該如何緩存方法調用??
緩存方法的兩個主要工具是 functools.cached_property()
和 functools.lru_cache()
。 前者在實例層級上存儲結果而后者在類層級上存儲結果。
cached_property 方式僅適用于不接受任何參數(shù)的方法。 它不會創(chuàng)建對實例的引用。 被緩存的方法結果將僅在實例的生存其內被保留。
The advantage is that when an instance is no longer used, the cached method result will be released right away. The disadvantage is that if instances accumulate, so too will the accumulated method results. They can grow without bound.
lru_cache 方式適用于具有可哈希參數(shù)的方法。 它會創(chuàng)建對實例的引用,除非特別設置了傳入弱引用。
最少近期使用算法的優(yōu)點是緩存會受指定的 maxsize 限制。 它的缺點是實例會保持存活,直到其達到生存期或者緩存被清空。
這個例子演示了幾種不同的方式:
class Weather:
"Lookup weather information on a government website"
def __init__(self, station_id):
self._station_id = station_id
# The _station_id is private and immutable
def current_temperature(self):
"Latest hourly observation"
# Do not cache this because old results
# can be out of date.
@cached_property
def location(self):
"Return the longitude/latitude coordinates of the station"
# Result only depends on the station_id
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='mm'):
"Rainfall on a given date"
# Depends on the station_id, date, and units.
上面的例子假定 station_id 從不改變。 如果相關實例屬性是可變對象,則 cached_property 方式就不再適用,因為它無法檢測到屬性的改變。
To make the lru_cache approach work when the station_id is mutable, the class needs to define the __eq__ and __hash__ methods so that the cache can detect relevant attribute updates:
class Weather:
"Example with a mutable station identifier"
def __init__(self, station_id):
self.station_id = station_id
def change_station(self, station_id):
self.station_id = station_id
def __eq__(self, other):
return self.station_id == other.station_id
def __hash__(self):
return hash(self.station_id)
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='cm'):
'Rainfall on a given date'
# Depends on the station_id, date, and units.
模塊?
如何創(chuàng)建 .pyc 文件??
當首次導入模塊時(或當前已編譯文件創(chuàng)建之后源文件發(fā)生了改動),在 .py
文件所在目錄的 __pycache__
子目錄下會創(chuàng)建一個包含已編譯代碼的 .pyc
文件。該 .pyc
文件的名稱開頭部分將與 .py
文件名相同,并以 .pyc
為后綴,中間部分則依據創(chuàng)建它的 python
版本而各不相同。(詳見 PEP 3147。)
.pyc
文件有可能會無法創(chuàng)建,原因之一是源碼文件所在的目錄存在權限問題,這樣就無法創(chuàng)建 __pycache__
子目錄。假如以某個用戶開發(fā)程序而以另一用戶運行程序,就有可能發(fā)生權限問題,測試 Web 服務器就屬于這種情況。
除非設置了 PYTHONDONTWRITEBYTECODE
環(huán)境變量,否則導入模塊并且 Python 能夠創(chuàng)建``__pycache__``子目錄并把已編譯模塊寫入該子目錄(權限、存儲空間等等)時,.pyc 文件就將自動創(chuàng)建。
在最高層級運行的 Python 腳本不會被視為經過了導入操作,因此不會創(chuàng)建 .pyc
文件。假定有一個最高層級的模塊文件 foo.py
,它導入了另一個模塊 xyz.py
,當運行 foo
模塊(通過輸入 shell 命令 python foo.py
),則會為 xyz
創(chuàng)建一個 .pyc
,因為 xyz
是被導入的,但不會為 foo
創(chuàng)建 .pyc
文件,因為 foo.py
不是被導入的。
若要為 foo
創(chuàng)建 .pyc
文件 —— 即為未做導入的模塊創(chuàng)建 .pyc
文件 —— 可以利用 py_compile
和 compileall
模塊。
py_compile
模塊能夠手動編譯任意模塊。 一種做法是交互式地使用該模塊中的 compile()
函數(shù):
>>> import py_compile
>>> py_compile.compile('foo.py')
這將會將 .pyc
文件寫入與 foo.py
相同位置下的 __pycache__
子目錄(或者你也可以通過可選參數(shù) cfile
來重載該行為)。
還可以用 compileall
模塊自動編譯一個或多個目錄下的所有文件。只要在命令行提示符中運行 compileall.py
并給出要編譯的 Python 文件所在目錄路徑即可:
python -m compileall .
如何找到當前模塊名稱??
模塊可以查看預定義的全局變量 __name__
獲悉自己的名稱。如其值為 '__main__'
,程序將作為腳本運行。通常,許多通過導入使用的模塊同時也提供命令行接口或自檢代碼,這些代碼只在檢測到處于 __name__
之后才會執(zhí)行:
def main():
print('Running test...')
...
if __name__ == '__main__':
main()
如何讓模塊相互導入??
假設有以下模塊:
foo.py
:
from bar import bar_var
foo_var = 1
bar.py
:
from foo import foo_var
bar_var = 2
問題是解釋器將執(zhí)行以下步驟:
首先導入
foo
為
foo
創(chuàng)建空的全局變量編譯
foo
并開始執(zhí)行foo
導入bar
為
bar
創(chuàng)建空的全局變量編譯
bar
并開始執(zhí)行bar
導入foo
(該步驟無操作,因為已經有一個名為foo
的模塊)。導入機制嘗試從
foo_var
全局變量讀取``foo``,用來設置bar.foo_var = foo.foo_var
最后一步失敗了,因為 Python 還沒有完成對 foo 的解釋,foo 的全局符號字典仍然是空的。
當你使用 import foo
,然后嘗試在全局代碼中訪問 foo.foo_var
時,會發(fā)生同樣的事情。
這個問題有(至少)三種可能的解決方法。
Guido van Rossum 建議完全避免使用 from <module> import ...
,并將所有代碼放在函數(shù)中。全局變量和類變量的初始化只應使用常量或內置函數(shù)。這意味著導入模塊中的所有內容都以 <module>.<name>
的形式引用。
Jim Roskind 建議每個模塊都應遵循以下順序:
導出(全局變量、函數(shù)和不需要導入基類的類)
import
語句本模塊的功能代碼(包括根據導入值進行初始化的全局變量)。
Van Rossum doesn't like this approach much because the imports appear in a strange place, but it does work.
Matthias Urlichs 建議對代碼進行重構,使得遞歸導入根本就沒必要發(fā)生。
這些解決方案并不相互排斥。
__import__('x.y.z') 返回的是 <module 'x'> ;該如何得到 z 呢??
不妨考慮換用 importlib
中的函數(shù) import_module()
:
z = importlib.import_module('x.y.z')
對已導入的模塊進行了編輯并重新導入,但變動沒有得以體現(xiàn)。這是為什么??
出于效率和一致性的原因,Python 僅在第一次導入模塊時讀取模塊文件。否則,在一個多模塊的程序中,每個模塊都會導入相同的基礎模塊,那么基礎模塊將會被一而再、再而三地解析。如果要強行重新讀取已更改的模塊,請執(zhí)行以下操作:
import importlib
import modname
importlib.reload(modname)
警告:這種技術并非萬無一失。尤其是模塊包含了以下語句時:
from modname import some_objects
仍將繼續(xù)使用前一版的導入對象。如果模塊包含了類的定義,并 不會 用新的類定義更新現(xiàn)有的類實例。這樣可能會導致以下矛盾的行為:
>>> import importlib
>>> import cls
>>> c = cls.C() # Create an instance of C
>>> importlib.reload(cls)
<module 'cls' from 'cls.py'>
>>> isinstance(c, cls.C) # isinstance is false?!?
False
只要把類對象的 id 打印出來,問題的性質就會一目了然:
>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'