編程常見(jiàn)問(wèn)題?

目錄

一般問(wèn)題?

Python 有沒(méi)有提供帶有斷點(diǎn)、單步調試等功能的源碼級調試器??

有的。

以下介紹了一些 Python 的調試器,用內置函數 breakpoint() 即可切入這些調試器中。

pdb 模塊是一個(gè)簡(jiǎn)單但是夠用的控制臺模式 Python 調試器。 它是標準 Python 庫的一部分,并且 已收錄于庫參考手冊。 你也可以通過(guò)使用 pdb 代碼作為樣例來(lái)編寫(xiě)你自己的調試器。

作為標準 Python 發(fā)行版附帶組件的 IDLE 交互式環(huán)境(通常位于 Tools/scripts/idle)中包含一個(gè)圖形化的調試器。

PythonWin 是一種 Python IDE,其中包含了一個(gè)基于 pdb 的 GUI 調試器。PythonWin 的調試器會(huì )為斷點(diǎn)著(zhù)色,并提供了相當多的超酷特性,例如調試非 PythonWin 程序等。PythonWin 是 pywin32 項目的組成部分,也是 ActivePython 發(fā)行版的組成部分。

Eric 是一個(gè)基于PyQt和Scintilla編輯組件構建的IDE。

trepan3k 是一個(gè)類(lèi)似 gdb 的調試器。

Visual Studio Code 是包含了調試工具的 IDE,并集成了版本控制軟件。

有許多商業(yè) Python IDE 都包含了圖形化調試器。包括:

是否有能幫助尋找漏洞或執行靜態(tài)分析的工具??

有的。

PylintPyflakes 可進(jìn)行基本檢查來(lái)幫助你盡早捕捉漏洞。

靜態(tài)類(lèi)型檢查器,例如 Mypy 、 PyrePytype 可以檢查Python源代碼中的類(lèi)型提示。

如何由 Python 腳本創(chuàng )建能獨立運行的二進(jìn)制程序??

如果只是想要一個(gè)獨立的程序,以便用戶(hù)不必預先安裝 Python 即可下載和運行它,則不需要將 Python 編譯成 C 代碼。有許多工具可以檢測程序所需的模塊,并將這些模塊與 Python 二進(jìn)制程序捆綁在一起生成單個(gè)可執行文件。

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 語(yǔ)句,并在標準 Python 路徑和源碼目錄(用于內置模塊)檢索這些模塊。然后,把這些模塊的 Python 字節碼轉換為 C 代碼(可以利用 marshal 模塊轉換為代碼對象的數組初始化器),并創(chuàng )建一個(gè)定制的配置文件,該文件僅包含程序實(shí)際用到的內置模塊。然后,編譯生成的 C 代碼并將其與 Python 解釋器的其余部分鏈接,形成一個(gè)自給自足的二進(jìn)制文件,其功能與 Python 腳本代碼完全相同。

下列包可以用于幫助創(chuàng )建控制臺和 GUI 的可執行文件:

是否有 Python 編碼標準或風(fēng)格指南??

有的。 標準庫模塊所要求的編碼風(fēng)格記錄于 PEP 8 之中。

語(yǔ)言核心內容?

變量明明有值,為什么還會(huì )出現 UnboundLocalError??

因為在函數內部某處添加了一條賦值語(yǔ)句,導致之前正常工作的代碼報出 UnboundLocalError 錯誤,這可能是有點(diǎn)令人驚訝。

以下代碼:

>>>
>>> x = 10
>>> def bar():
...     print(x)
>>> bar()
10

正常工作,但是以下代碼

>>>
>>> x = 10
>>> def foo():
...     print(x)
...     x += 1

會(huì )得到一個(gè) UnboundLocalError :

>>>
>>> foo()
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'x' referenced before assignment

原因就是,當對某作用域內的變量進(jìn)行賦值時(shí),該變量將成為該作用域內的局部變量,并覆蓋外部作用域中的同名變量。由于 foo 的最后一條語(yǔ)句為 x 分配了一個(gè)新值,編譯器會(huì )將其識別為局部變量。因此,前面的 print(x) 試圖輸出未初始化的局部變量,就會(huì )引發(fā)錯誤。

在上面的示例中,可以將外部作用域的變量聲明為全局變量以便訪(fǎng)問(wèn):

>>>
>>> x = 10
>>> def foobar():
...     global x
...     print(x)
...     x += 1
>>> foobar()
10

與類(lèi)和實(shí)例變量貌似但不一樣,其實(shí)以上是在修改外部作用域的變量值,為了提示這一點(diǎn),這里需要顯式聲明一下。

>>>
>>> print(x)
11

你可以使用 nonlocal 關(guān)鍵字在嵌套作用域中執行類(lèi)似的操作:

>>>
>>> def foo():
...    x = 10
...    def bar():
...        nonlocal x
...        print(x)
...        x += 1
...    bar()
...    print(x)
>>> foo()
10
11

Python 的局部變量和全局變量有哪些規則??

函數內部只作引用的 Python 變量隱式視為全局變量。如果在函數內部任何位置為變量賦值,則除非明確聲明為全局變量,否則均將其視為局部變量。

起初盡管有點(diǎn)令人驚訝,不過(guò)考慮片刻即可釋然。一方面,已分配的變量要求加上 global 可以防止意外的副作用發(fā)生。另一方面,如果所有全局引用都要加上 global ,那處處都得用上 global 了。那么每次對內置函數或導入模塊中的組件進(jìn)行引用時(shí),都得聲明為全局變量。這種雜亂會(huì )破壞 global 聲明用于警示副作用的有效性。

為什么在循環(huán)中定義的參數各異的 lambda 都返回相同的結果??

假設用 for 循環(huán)來(lái)定義幾個(gè)取值各異的 lambda(即便是普通函數也一樣):

>>>
>>> squares = []
>>> for x in range(5):
...     squares.append(lambda: x**2)

以上會(huì )得到一個(gè)包含5個(gè) lambda 函數的列表,這些函數將計算 x**2。大家或許期望,調用這些函數會(huì )分別返回 0 、1 、 4 、 916。然而,真的試過(guò)就會(huì )發(fā)現,他們都會(huì )返回 16

>>>
>>> squares[2]()
16
>>> squares[4]()
16

這是因為 x 不是 lambda 函數的內部變量,而是定義于外部作用域中的,并且 x 是在調用 lambda 時(shí)訪(fǎng)問(wèn)的——而不是在定義時(shí)訪(fǎng)問(wèn)。循環(huán)結束時(shí) x 的值是 4 ,所以此時(shí)所有的函數都將返回 4**2 ,即 16 。通過(guò)改變 x 的值并查看 lambda 的結果變化,也可以驗證這一點(diǎn)。

>>>
>>> x = 8
>>> squares[2]()
64

為了避免發(fā)生上述情況,需要將值保存在 lambda 局部變量,以使其不依賴(lài)于全局 x 的值:

>>>
>>> squares = []
>>> for x in range(5):
...     squares.append(lambda n=x: n**2)

以上 n=x 創(chuàng )建了一個(gè)新的 lambda 本地變量 n,并在定義 lambda 時(shí)計算其值,使其與循環(huán)當前時(shí)點(diǎn)的 x 值相同。這意味著(zhù) n 的值在第 1 個(gè) lambda 中為 0 ,在第 2 個(gè) lambda 中為 1 ,在第 3 個(gè)中為 2,依此類(lèi)推。因此現在每個(gè) lambda 都會(huì )返回正確結果:

>>>
>>> squares[2]()
4
>>> squares[4]()
16

請注意,上述表現并不是 lambda 所特有的,常規的函數也同樣適用。

如何跨模塊共享全局變量??

在單個(gè)程序中跨模塊共享信息的規范方法是創(chuàng )建一個(gè)特殊模塊(通常稱(chēng)為 config 或 cfg)。只需在應用程序的所有模塊中導入該 config 模塊;然后該模塊就可當作全局名稱(chēng)使用了。因為每個(gè)模塊只有一個(gè)實(shí)例,所以對該模塊對象所做的任何更改將會(huì )在所有地方得以體現。 例如:

config.py:

x = 0   # Default value of the 'x' configuration setting

mod.py:

import config
config.x = 1

main.py:

import config
import mod
print(config.x)

請注意,出于同樣的原因,采用模塊也是實(shí)現單例設計模式的基礎。

導入模塊的“最佳實(shí)踐”是什么??

通常請勿使用 from modulename import * 。因為這會(huì )擾亂 importer 的命名空間,且會(huì )造成未定義名稱(chēng)更難以被 Linter 檢查出來(lái)。

請在代碼文件的首部就導入模塊。這樣代碼所需的模塊就一目了然了,也不用考慮模塊名是否在作用域內的問(wèn)題。每行導入一個(gè)模塊則增刪起來(lái)會(huì )比較容易,每行導入多個(gè)模塊則更節省屏幕空間。

按如下順序導入模塊就是一種好做法:

  1. 標準庫模塊——比如: sys 、 os 、 getopt 、 re 等。

  2. 第三方庫模塊(安裝于 Python site-packages 目錄中的內容)——如 mx.DateTime、ZODB、PIL.Image 等。

  3. 本地開(kāi)發(fā)的模塊

為了避免循環(huán)導入引發(fā)的問(wèn)題,有時(shí)需要將模塊導入語(yǔ)句移入函數或類(lèi)的內部。Gordon McMillan 的說(shuō)法如下:

當兩個(gè)模塊都采用 "import <module>" 的導入形式時(shí),循環(huán)導入是沒(méi)有問(wèn)題的。但如果第 2 個(gè)模塊想從第 1 個(gè)模塊中取出一個(gè)名稱(chēng)("from module import name")并且導入處于代碼的最頂層,那導入就會(huì )失敗。原因是第 1 個(gè)模塊中的名稱(chēng)還不可用,這時(shí)第 1 個(gè)模塊正忙于導入第 2 個(gè)模塊呢。

如果只是在一個(gè)函數中用到第 2 個(gè)模塊,那這時(shí)將導入語(yǔ)句移入該函數內部即可。當調用到導入語(yǔ)句時(shí),第 1 個(gè)模塊將已經(jīng)完成初始化,第 2 個(gè)模塊就可以進(jìn)行導入了。

如果某些模塊是平臺相關(guān)的,可能還需要把導入語(yǔ)句移出最頂級代碼。這種情況下,甚至有可能無(wú)法導入文件首部的所有模塊。于是在對應的平臺相關(guān)代碼中導入正確的模塊,就是一種不錯的選擇。

只有為了避免循環(huán)導入問(wèn)題,或有必要減少模塊初始化時(shí)間時(shí),才把導入語(yǔ)句移入類(lèi)似函數定義內部的局部作用域。如果根據程序的執行方式,許多導入操作不是必需的,那么這種技術(shù)尤其有用。如果模塊僅在某個(gè)函數中用到,可能還要將導入操作移入該函數內部。請注意,因為模塊有一次初始化過(guò)程,所以第一次加載模塊的代價(jià)可能會(huì )比較高,但多次加載幾乎沒(méi)有什么花費,代價(jià)只是進(jìn)行幾次字典檢索而已。即使模塊名超出了作用域,模塊在 sys.modules 中也是可用的。

為什么對象之間會(huì )共享默認值??

新手程序員常常中招這類(lèi) Bug。請看以下函數:

def foo(mydict={}):  # Danger: shared reference to one dict for all calls
    ... compute something ...
    mydict[key] = value
    return mydict

第一次調用此函數時(shí), mydict 中只有一個(gè)數據項。第二次調用 mydict 則會(huì )包含兩個(gè)數據項,因為 foo() 開(kāi)始執行時(shí), mydict 中已經(jīng)帶有一個(gè)數據項了。

大家往往希望,函數調用會(huì )為默認值創(chuàng )建新的對象。但事實(shí)并非如此。默認值只會(huì )在函數定義時(shí)創(chuàng )建一次。如果對象發(fā)生改變,就如上例中的字典那樣,則后續調用該函數時(shí)將會(huì )引用這個(gè)改動(dòng)的對象。

按照定義,不可變對象改動(dòng)起來(lái)是安全的,諸如數字、字符串、元組和 None 之類(lèi)。而可變對象的改動(dòng)則可能引起困惑,例如字典、列表和類(lèi)實(shí)例等。

因此,不把可變對象用作默認值是一種良好的編程做法。而應采用 None 作為默認值,然后在函數中檢查參數是否為 None 并新建列表、字典或其他對象。例如,代碼不應如下所示:

def foo(mydict={}):
    ...

而應這么寫(xiě):

def foo(mydict=None):
    if mydict is None:
        mydict = {}  # create a new dict for local namespace

參數默認值的特性有時(shí)會(huì )很有用處。 如果有個(gè)函數的計算過(guò)程會(huì )比較耗時(shí),有一種常見(jiàn)技巧是將每次函數調用的參數和結果緩存起來(lái),并在同樣的值被再次請求時(shí)返回緩存的值。這種技巧被稱(chēng)為“memoize”,實(shí)現代碼可如下所示:

# Callers can only provide two parameters and optionally pass _cache by keyword
def expensive(arg1, arg2, *, _cache={}):
    if (arg1, arg2) in _cache:
        return _cache[(arg1, arg2)]

    # Calculate the value
    result = ... expensive computation ...
    _cache[(arg1, arg2)] = result           # Store result in the cache
    return result

也可以不用參數默認值來(lái)實(shí)現,而是采用全局的字典變量;這取決于個(gè)人偏好。

如何將可選參數或關(guān)鍵字參數從一個(gè)函數傳遞到另一個(gè)函數??

請利用函數參數列表中的標識符 *** 歸集實(shí)參;結果會(huì )是元組形式的位置實(shí)參和字典形式的關(guān)鍵字實(shí)參。然后就可利用 *** 在調用其他函數時(shí)傳入這些實(shí)參:

def f(x, *args, **kwargs):
    ...
    kwargs['width'] = '14.3c'
    ...
    g(x, *args, **kwargs)

形參和實(shí)參之間有什么區別??

形參 是指出現在函數定義中的名稱(chēng),而 實(shí)參 則是在調用函數時(shí)實(shí)際傳入的值。 形參定義了一個(gè)函數能接受何種類(lèi)型的實(shí)參。 例如,對于以下函數定義:

def func(foo, bar=None, **kwargs):
    pass

foo 、 barkwargsfunc 的形參。 不過(guò)在調用 func 時(shí),例如:

func(42, bar=314, extra=somevar)

42 、 314somevar 則是實(shí)參。

為什么修改列表 'y' 也會(huì )更改列表 'x'??

如果代碼編寫(xiě)如下:

>>>
>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]

或許大家很想知道,為什么在 y 中添加一個(gè)元素時(shí), x 也會(huì )改變。

產(chǎn)生這種結果有兩個(gè)因素:

  1. 變量只是指向對象的一個(gè)名稱(chēng)。執行 y = x 并不會(huì )創(chuàng )建列表的副本——而只是創(chuàng )建了一個(gè)新變量 y,并指向 x 所指的同一對象。這就意味著(zhù)只存在一個(gè)列表對象,xy 都是對它的引用。

  2. 列表屬于 mutable 對象,這意味著(zhù)它的內容是可以修改的。

在調用 append() 之后,該可變對象的內容由 [] 變?yōu)?[10]。由于 x 和 y 這兩個(gè)變量引用了同一對象,因此用其中任意一個(gè)名稱(chēng)所訪(fǎng)問(wèn)到的都是修改后的值 [10]。

如果把賦給 x 的對象換成一個(gè)不可變對象:

>>>
>>> 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

可見(jiàn)這時(shí) xy 就不再相等了。因為整數是 immutable 對象,在執行 x = x + 1 時(shí),并不會(huì )修改整數對象 5,給它加上 1;而是創(chuàng )建了一個(gè)新的對象(整數對象 6 )并將其賦給 x (也就是改變了 x 所指向的對象)。在賦值完成后,就有了兩個(gè)對象(整數對象 65 )和分別指向他倆的兩個(gè)變量( x 現在指向 6y 仍然指向 5 )。

某些操作(例如 y.append(10)y.sort() )會(huì )直接修改原對象,而看上去相似的另一些操作(例如 y = y + [10]sorted(y) )則會(huì )創(chuàng )建新的對象。通常在 Python 中(以及所有標準庫),直接修改原對象的方法將會(huì )返回 None ,以助避免混淆這兩種不同類(lèi)型的操作。因此如果誤用了 y.sort() 并期望返回 y 的有序副本,則結果只會(huì )是 None ,這可能就能讓程序引發(fā)一條容易診斷的錯誤。

不過(guò)還存在一類(lèi)操作,用不同的類(lèi)型執行相同的操作有時(shí)會(huì )發(fā)生不同的行為:即增量賦值運算符。例如,+= 會(huì )修改列表,但不會(huì )修改元組或整數(a_list += [1, 2, 3]a_list.extend([1, 2, 3]) 同樣都會(huì )改變 a_list,而 some_tuple += (1, 2, 3)some_int += 1 則會(huì )創(chuàng )建新的對象)。

換而言之:

  • 對于一個(gè)可變對象( list 、 dict 、 set 等等),可以利用某些特定的操作進(jìn)行修改,所有引用它的變量都會(huì )反映出改動(dòng)情況。

  • 對于一個(gè)不可變對象( str 、 int 、 tuple 等),所有引用它的變量都會(huì )給出相同的值,但所有改變其值的操作都將返回一個(gè)新的對象。

如要知道兩個(gè)變量是否指向同一個(gè)對象,可以利用 is 運算符或內置函數 id()。

如何編寫(xiě)帶有輸出參數的函數(按照引用調用)??

請記住,Python 中的實(shí)參是通過(guò)賦值傳遞的。由于賦值只是創(chuàng )建了對象的引用,所以調用方和被調用方的參數名都不存在別名,本質(zhì)上也就不存在按引用調用的方式。通過(guò)以下幾種方式,可以得到所需的效果。

  1. 返回一個(gè)元組:

    >>>
    >>> 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)
    

    這差不多是最明晰的解決方案了。

  2. 使用全局變量。這不是線(xiàn)程安全的方案,不推薦使用。

  3. 傳遞一個(gè)可變(即可原地修改的) 對象:

    >>>
    >>> 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]
    
  4. 傳入一個(gè)接收可變對象的字典:

    >>>
    >>> 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}
    
  5. 或者把值用類(lèi)實(shí)例封裝起來(lái):

    >>>
    >>> 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}
    

    沒(méi)有什么理由要把問(wèn)題搞得這么復雜。

最佳選擇就是返回一個(gè)包含多個(gè)結果值的元組。

如何在 Python 中創(chuàng )建高階函數??

有兩種選擇:嵌套作用域、可調用對象。假定需要定義 linear(a,b) ,其返回結果是一個(gè)計算出 a*x+b 的函數 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

采用這兩種方案時(shí):

taxes = linear(0.3, 2)

都會(huì )得到一個(gè)可調用對象,可實(shí)現 taxes(10e6) == 0.3 * 10e6 + 2 。

可調用對象的方案有個(gè)缺點(diǎn),就是速度稍慢且生成的代碼略長(cháng)。不過(guò)值得注意的是,同一組可調用對象能夠通過(guò)繼承來(lái)共享簽名(類(lèi)聲明):

class exponential(linear):
    # __init__ inherited
    def __call__(self, x):
        return self.a * (x ** self.b)

對象可以為多個(gè)方法的運行狀態(tài)進(jìn)行封裝:

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() 的表現,就如同共享了同一計數變量一樣。

如何復制 Python 對象??

一般情況下,用 copy.copy()copy.deepcopy() 基本就可以了。并不是所有對象都支持復制,但多數是可以的。

某些對象可以用更簡(jiǎn)便的方法進(jìn)行復制。比如字典對象就提供了 copy() 方法:

newdict = olddict.copy()

序列可以用切片操作進(jìn)行復制:

new_l = l[:]

如何找到對象的方法或屬性??

假定 x 是一個(gè)用戶(hù)自定義類(lèi)的實(shí)例,dir(x) 將返回一個(gè)按字母排序的名稱(chēng)列表,其中包含了實(shí)例的屬性及由類(lèi)定義的方法和屬性。

如何用代碼獲取對象的名稱(chēng)??

一般而言這是無(wú)法實(shí)現的,因為對象并不存在真正的名稱(chēng)。賦值本質(zhì)上是把某個(gè)名稱(chēng)綁定到某個(gè)值上;defclass 語(yǔ)句同樣如此,只是值換成了某個(gè)可調用對象。比如以下代碼:

>>>
>>> class A:
...     pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>

可以不太嚴謹地說(shuō),上述類(lèi)具有一個(gè)名稱(chēng):即便它綁定了兩個(gè)名稱(chēng)并通過(guò)名稱(chēng) B 發(fā)起調用,可是創(chuàng )建出來(lái)的實(shí)例仍被視為是類(lèi) A 的實(shí)例。但無(wú)法說(shuō)出實(shí)例的名稱(chēng)是 a 還是 b,因為這兩個(gè)名稱(chēng)都被綁定到同一個(gè)值上了。

代碼一般沒(méi)有必要去“知曉”某個(gè)值的名稱(chēng)。通常這種需求預示著(zhù)還是改變方案為好,除非真的是要編寫(xiě)內審程序。

在 comp.lang.python 中,Fredrik Lundh 在回答這樣的問(wèn)題時(shí)曾經(jīng)給出過(guò)一個(gè)絕佳的類(lèi)比:

這就像要知道家門(mén)口的那只貓的名字一樣:貓(對象)自己不會(huì )說(shuō)出它的名字,它根本就不在乎自己叫什么——所以唯一方法就是問(wèn)一遍你所有的鄰居(命名空間),這是不是他們家的貓(對象)……

……并且如果你發(fā)現它有很多名字或根本沒(méi)有名字,那也不必驚訝!

逗號運算符的優(yōu)先級是什么??

逗號不是 Python 的運算符。 請看以下例子:

>>>
>>> "a" in "b", "a"
(False, 'a')

由于逗號不是運算符,而只是表達式之間的分隔符,因此上述代碼就相當于:

("a" in "b"), "a"

而不是:

"a" in ("b", "a")

對于各種賦值運算符( = 、 += 等)來(lái)說(shuō)同樣如此。他們并不是真正的運算符,而只是賦值語(yǔ)句中的語(yǔ)法分隔符。

是否提供等價(jià)于 C 語(yǔ)言 "?:" 三目運算符的東西??

有的。語(yǔ)法如下:

[on_true] if [expression] else [on_false]

x, y = 50, 25
small = x if x < y else y

在 Python 2.5 引入上述語(yǔ)法之前,通常的做法是使用邏輯運算符:

[expression] and [on_true] or [on_false]

然而這種做法并不保險,因為當 on_true 為布爾值“假”時(shí),結果將會(huì )出錯。所以肯定還是采用 ... if ... else ... 形式為妙。

是否可以用 Python 編寫(xiě)讓人眼暈的單行程序??

可以。通常是在 lambda 中嵌套 lambda 來(lái)實(shí)現的。請參閱以下三個(gè)來(lái)自 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

請不要在家里嘗試,騷年!

函數形參列表中的斜杠(/)是什么意思??

函數參數列表中的斜杠表示在它之前的形參全都僅限位置形參。僅限位置形參沒(méi)有可供外部使用的名稱(chēng)。在調用僅接受位置形參的函數時(shí),實(shí)參只會(huì )根據位置映射到形參上。假定 divmod() 是一個(gè)僅接受位置形參的函數。 它的幫助文檔如下所示:

>>>
>>> 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.

形參列表尾部的斜杠說(shuō)明,兩個(gè)形參都是僅限位置形參。因此,用關(guān)鍵字參數調用 divmod() 將會(huì )引發(fā)錯誤:

>>>
>>> divmod(x=3, y=4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments

數字和字符串?

如何給出十六進(jìn)制和八進(jìn)制整數??

要給出八進(jìn)制數,需在八進(jìn)制數值前面加上一個(gè)零和一個(gè)小寫(xiě)或大寫(xiě)字母 "o" 作為前綴。例如,要將變量 "a" 設為八進(jìn)制的 "10" (十進(jìn)制的 8),寫(xiě)法如下:

>>>
>>> a = 0o10
>>> a
8

十六進(jìn)制數也很簡(jiǎn)單。只要在十六進(jìn)制數前面加上一個(gè)零和一個(gè)小寫(xiě)或大寫(xiě)的字母 "x"。十六進(jìn)制數中的字母可以為大寫(xiě)或小寫(xiě)。比如在 Python 解釋器中輸入:

>>>
>>> a = 0xa5
>>> a
165
>>> b = 0XB2
>>> b
178

為什么 -22 // 10 會(huì )返回 -3 ??

這主要是為了讓 i % j 的正負與 j 一致,如果期望如此,且期望如下等式成立:

i == (i // j) * j + (i % j)

那么整除就必須返回向下取整的結果。C 語(yǔ)言同樣要求保持這種一致性,于是編譯器在截斷 i // j 的結果時(shí)需要讓 i % j 的正負與 i 一致。

對于 i % j 來(lái)說(shuō) j 為負值的應用場(chǎng)景實(shí)際上是非常少的。 而 j 為正值的情況則非常多,并且實(shí)際上在所有情況下讓 i % j 的結果為 >= 0 會(huì )更有用處。 如果現在時(shí)間為 10 時(shí),那么 200 小時(shí)前應是幾時(shí)? -190 % 12 == 2 是有用處的;-190 % 12 == -10 則是會(huì )導致意外的漏洞。

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'>

如何將字符串轉換為數字??

對于整數,可使用內置的 int() 類(lèi)型構造器,例如 int('144') == 144。 類(lèi)似地,可使用 float() 轉換為浮點(diǎn)數,例如 float('144') == 144.0。

默認情況下,這些操作會(huì )將數字按十進(jìn)制來(lái)解讀,因此 int('0144') == 144 為真值,而 int('0x144') 會(huì )引發(fā) ValueError。 int(string, base) 接受第二個(gè)可選參數指定轉換的基數,例如 int( '0x144', 16) == 324。 如果指定基數為 0,則按 Python 規則解讀數字:前綴 '0o' 表示八進(jìn)制,而 '0x' 表示十六進(jìn)制。

如果只是想把字符串轉為數字,請不要使用內置函數 eval()。 eval() 的速度慢很多且存在安全風(fēng)險:別人可能會(huì )傳入帶有不良副作用的 Python 表達式。比如可能會(huì )傳入 __import__('os').system("rm -rf $HOME") ,這會(huì )把 home 目錄給刪了。

eval() 還有把數字解析為 Python 表達式的后果,因此如 eval('09') 將會(huì )導致語(yǔ)法錯誤,因為 Python 不允許十進(jìn)制數帶有前導 '0'('0' 除外)。

如何將數字轉換為字符串??

比如要把數字 144 轉換為字符串 '144',可使用內置類(lèi)型構造器 str()。如果要表示為十六進(jìn)制或八進(jìn)制數格式,可使用內置函數 hex()oct()。更復雜的格式化方法請參閱 格式字符串字面值格式字符串語(yǔ)法 等章節,比如 "{:04d}".format(144) 會(huì )生成 '0144' , "{:.3f}".format(1.0/3.0) 則會(huì )生成 '0.333'。

如何修改字符串??

無(wú)法修改,因為字符串是不可變對象。 在大多數情況下,只要將各個(gè)部分組合起來(lái)構造出一個(gè)新字符串即可。如果需要一個(gè)能原地修改 Unicode 數據的對象,可以試試 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'

如何使用字符串調用函數/方法??

有多種技巧可供選擇。

  • 最好的做法是采用一個(gè)字典,將字符串映射為函數。其主要優(yōu)勢就是字符串不必與函數名一樣。這也是用來(lái)模擬 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
    
  • 利用內置函數 getattr()

    import foo
    getattr(foo, 'bar')()
    

    請注意 getattr() 可用于任何對象,包括類(lèi)、類(lèi)實(shí)例、模塊等等。

    標準庫就多次使用了這個(gè)技巧,例如:

    class Foo:
        def do_foo(self):
            ...
    
        def do_bar(self):
            ...
    
    f = getattr(foo_instance, 'do_' + opname)
    f()
    
  • locals() 解析出函數名:

    def myFunc():
        print("hello")
    
    fname = "myFunc"
    
    f = locals()[fname]
    f()
    

是否有與Perl 的chomp() 等效的方法,用于從字符串中刪除尾隨換行符??

可以使用 S.rstrip("\r\n") 從字符串 S 的末尾刪除所有的換行符,而不刪除其他尾隨空格。如果字符串 S 表示多行,且末尾有幾個(gè)空行,則將刪除所有空行的換行符:

>>>
>>> lines = ("line 1 \r\n"
...          "\r\n"
...          "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '

由于通常只在一次讀取一行文本時(shí)才需要這樣做,所以使用 S.rstrip() 這種方式工作得很好。

是否有 scanf() 或 sscanf() 的等價(jià)函數??

沒(méi)有。

如果要對簡(jiǎn)單的輸入進(jìn)行解析,最容易的做法通常是利用字符串對象的 split() 方法將一行按空白符分隔拆分為多個(gè)單詞,然后用 int()float() 將十進(jìn)制數字符串轉換為數字值。 split() 支持可選的 "sep" 形參,適用于分隔符不用空白符的情況。

如果要對更復雜的輸入進(jìn)行解析,那么正則表達式要比 C 語(yǔ)言的 sscanf() 更強大,也更合適。

'UnicodeDecodeError' 或 'UnicodeEncodeError' 錯誤是什么意思??

見(jiàn) Unicode 指南

性能?

我的程序太慢了。該如何加快速度??

總的來(lái)說(shuō),這是個(gè)棘手的問(wèn)題。在進(jìn)一步討論之前,首先應該記住以下幾件事:

  • 不同的 Python 實(shí)現具有不同的性能特點(diǎn)。 本 FAQ 著(zhù)重解答的是 CPython。

  • 不同操作系統可能會(huì )有不同表現,尤其是涉及 I/O 和多線(xiàn)程時(shí)。

  • 在嘗試優(yōu)化代碼 之前 ,務(wù)必要先找出程序中的熱點(diǎn)(請參閱 profile 模塊)。

  • 編寫(xiě)基準測試腳本,在尋求性能提升的過(guò)程中就能實(shí)現快速迭代(請參閱 timeit 模塊)。

  • 強烈建議首先要保證足夠高的代碼測試覆蓋率(通過(guò)單元測試或其他技術(shù)),因為復雜的優(yōu)化有可能會(huì )導致代碼回退。

話(huà)雖如此,Python 代碼的提速還是有很多技巧的。以下列出了一些普適性的原則,對于讓性能達到可接受的水平會(huì )有很大幫助:

  • 相較于試圖對全部代碼鋪開(kāi)做微觀(guān)優(yōu)化,優(yōu)化算法(或換用更快的算法)可以產(chǎn)出更大的收益。

  • 使用正確的數據結構。參考 內置類(lèi)型collections 模塊的文檔。

  • 如果標準庫已為某些操作提供了基礎函數,則可能(當然不能保證)比所有自編的函數都要快。對于用 C 語(yǔ)言編寫(xiě)的基礎函數則更是如此,比如內置函數和一些擴展類(lèi)型。例如,一定要用內置方法 list.sort()sorted() 函數進(jìn)行排序(某些高級用法的示例請參閱 排序指南 )。

  • 抽象往往會(huì )造成中間層,并會(huì )迫使解釋器執行更多的操作。如果抽象出來(lái)的中間層級太多,工作量超過(guò)了要完成的有效任務(wù),那么程序就會(huì )被拖慢。應該避免過(guò)度的抽象,而且往往也會(huì )對可讀性產(chǎn)生不利影響,特別是當函數或方法比較小的時(shí)候。

如果你已經(jīng)達到純 Python 允許的限制,那么有一些工具可以讓你走得更遠。 例如, Cython 可以將稍微修改的 Python 代碼版本編譯為 C 擴展,并且可以在許多不同的平臺上使用。 Cython 可以利用編譯(和可選的類(lèi)型注釋?zhuān)﹣?lái)使代碼明顯快于解釋運行時(shí)的速度。 如果您對 C 編程技能有信心,也可以自己 編寫(xiě) C 擴展模塊 。

參見(jiàn)

專(zhuān)門(mén)介紹 性能提示 的wiki頁(yè)面。

將多個(gè)字符串連接在一起的最有效方法是什么??

strbytes 對象是不可變的,因此連接多個(gè)字符串的效率會(huì )很低,因為每次連接都會(huì )創(chuàng )建一個(gè)新的對象。一般情況下,總耗時(shí)與字符串總長(cháng)是二次方的關(guān)系。

如果要連接多個(gè) str 對象,通常推薦的方案是先全部放入列表,最后再調用 str.join()

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(還有一種合理高效的習慣做法,就是利用 io.StringIO

如果要連接多個(gè) bytes 對象,推薦做法是用 bytearray 對象的原地連接操作( += 運算符)追加數據:

result = bytearray()
for b in my_bytes_objects:
    result += b

序列(元組/列表)?

如何在元組和列表之間進(jìn)行轉換??

類(lèi)型構造器 tuple(seq) 可將任意序列(實(shí)際上是任意可迭代對象)轉換為數據項和順序均不變的元組。

例如,tuple([1, 2, 3]) 會(huì )生成 (1, 2, 3) , tuple('abc') 則會(huì )生成 ('a', 'b', 'c') 。 如果參數就是元組,則不會(huì )創(chuàng )建副本而是返回同一對象,因此如果無(wú)法確定某個(gè)對象是否為元組時(shí),直接調用 tuple() 也沒(méi)什么代價(jià)。

類(lèi)型構造器 list(seq) 可將任意序列或可迭代對象轉換為數據項和順序均不變的列表。例如,list((1, 2, 3)) 會(huì )生成 [1, 2, 3]list('abc') 則會(huì )生成 ['a', 'b', 'c']。如果參數即為列表,則會(huì )像 seq[:] 那樣創(chuàng )建一個(gè)副本。

什么是負數索引??

Python 序列的索引可以是正數或負數。索引為正數時(shí),0 是第一個(gè)索引值, 1 為第二個(gè),依此類(lèi)推。索引為負數時(shí),-1 為倒數第一個(gè)索引值,-2 為倒數第二個(gè),依此類(lèi)推??梢哉J為 seq[-n] 就相當于 seq[len(seq)-n]。

使用負數序號有時(shí)會(huì )很方便。 例如 S[:-1] 就是原字符串去掉最后一個(gè)字符,這可以用來(lái)移除某個(gè)字符串末尾的換行符。

序列如何以逆序遍歷??

使用內置函數 reversed()

for x in reversed(sequence):
    ...  # do something with x ...

原序列不會(huì )變化,而是構建一個(gè)逆序的新副本以供遍歷。

如何從列表中刪除重復項??

許多完成此操作的的詳細介紹,可參閱 Python Cookbook:

如果列表允許重新排序,不妨先對其排序,然后從列表末尾開(kāi)始掃描,依次刪除重復項:

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))

以上操作會(huì )將列表轉換為集合,從而刪除重復項,然后返回成列表。

如何從列表中刪除多個(gè)項??

類(lèi)似于刪除重復項,一種做法是反向遍歷并根據條件刪除。不過(guò)更簡(jiǎn)單快速的做法就是切片替換操作,采用隱式或顯式的正向迭代遍歷。以下是三種變體寫(xiě)法:

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 )建數組??

用列表:

["this", 1, "is", "an", "array"]

列表在時(shí)間復雜度方面相當于 C 或 Pascal 的數組;主要區別在于,Python 列表可以包含多種不同類(lèi)型的對象。

array 模塊也提供了一些創(chuàng )建具有緊湊格式的固定類(lèi)型數組的方法,但其索引訪(fǎng)問(wèn)速度比列表慢。 并請注意 NumPy 和其他一些第三方包也定義了一些各具特色的數組類(lèi)結構。

若要得到 Lisp 風(fēng)格的列表,可以用元組模擬 cons 元素:

lisp_list = ("like",  ("this",  ("example", None) ) )

若要具備可變性,可以不用元組而是用列表。模擬 lisp car 函數的是 lisp_list[0] ,模擬 cdr 函數的是 lisp_list[1] 。僅當真正必要時(shí)才會(huì )這么用,因為通常這種用法要比 Python 列表慢得多。

如何創(chuàng )建多維列表??

多維數組或許會(huì )用以下方式建立:

>>>
>>> A = [[None] * 2] * 3

打印出來(lái)貌似沒(méi)錯:

>>>
>>> A
[[None, None], [None, None], [None, None]]

但如果給某一項賦值,結果會(huì )同時(shí)在多個(gè)位置體現出來(lái):

>>>
>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]

原因在于用 * 對列表執行重復操作并不會(huì )創(chuàng )建副本,而只是創(chuàng )建現有對象的引用。 *3 創(chuàng )建的是包含 3 個(gè)引用的列表,每個(gè)引用指向的是同一個(gè)長(cháng)度為 2 的列表。1 處改動(dòng)會(huì )體現在所有地方,這一定不是應有的方案。

推薦做法是先創(chuàng )建一個(gè)所需長(cháng)度的列表,然后將每個(gè)元素都填充為一個(gè)新建列表。

A = [None] * 3
for i in range(3):
    A[i] = [None] * 2

以上生成了一個(gè)包含 3 個(gè)列表的列表,每個(gè)子列表的長(cháng)度為 2。也可以采用列表推導式:

w, h = 2, 3
A = [[None] * w for i in range(h)]

或者你還可以使用提供矩陣類(lèi)型的擴展包;其中最著(zhù)名的是 NumPy。

如何將方法應用于一系列對象??

可以使用列表推導式:

result = [obj.method() for obj in mylist]

為什么 a_tuple[i] += ['item'] 會(huì )引發(fā)異常??

這是由兩個(gè)因素共同導致的,一是增強賦值運算符屬于 賦值 運算符,二是 Python 可變和不可變對象之間的差別。

只要元組的元素指向可變對象,這時(shí)對元素進(jìn)行增強賦值,那么這里介紹的內容都是適用的。在此只以 list+= 舉例。

如果你寫(xiě)成這樣:

>>>
>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (most recent call last):
   ...
TypeError: 'tuple' object does not support item assignment

觸發(fā)異常的原因顯而易見(jiàn): 1 會(huì )與指向(1)的對象 a_tuple[0] 相加,生成結果對象 2,但在試圖將運算結果 2 賦值給元組的 0 號元素時(shí)就會(huì )報錯,因為元組元素的指向無(wú)法更改。

其實(shí)在幕后,上述增強賦值語(yǔ)句的執行過(guò)程大致如下:

>>>
>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

由于元組是不可變的,因此賦值這步會(huì )引發(fā)錯誤。

如果寫(xiě)成以下這樣:

>>>
>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

這時(shí)觸發(fā)異常會(huì )令人略感驚訝,更讓人吃驚的是雖有報錯,但加法操作卻生效了:

>>>
>>> a_tuple[0]
['foo', 'item']

要明白為何會(huì )這樣,需要知道 (a) 如果一個(gè)對象實(shí)現了 __iadd__ 魔法方法,在執行 += 增強賦值時(shí)就會(huì )調用它,并采納其返回值;(b) 對于列表而言,__iadd__ 相當于在列表上調用 extend 并返回該列表。因此對于列表可以說(shuō) += 就是 list.extend 的“快捷方式”:

>>>
>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]

這相當于:

>>>
>>> result = a_list.__iadd__([1])
>>> a_list = result

a_list 所引用的對象已被修改,而引用被修改對象的指針又重新被賦值給 a_list。 賦值的最終結果沒(méi)有變化,因為它是引用 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__ 成功執行,因此列表得到了擴充,但是雖然 result 指向了 a_tuple[0] 已經(jīng)指向的同一對象,最后的賦值仍然導致了報錯,因為元組是不可變的。

我想做一個(gè)復雜的排序:能用 Python 進(jìn)行施瓦茨變換嗎??

歸功于 Perl 社區的 Randal Schwartz,該技術(shù)根據度量值對列表進(jìn)行排序,該度量值將每個(gè)元素映射為“順序值”。在 Python 中,請利用 list.sort() 方法的 key 參數:

Isorted = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))

如何根據另一個(gè)列表的值對某列表進(jìn)行排序??

將它們合并到元組的迭代器中,對結果列表進(jìn)行排序,然后選擇所需的元素。

>>>
>>> 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']

對象?

什么是類(lèi)??

類(lèi)是通過(guò)執行 class 語(yǔ)句創(chuàng )建的某種對象的類(lèi)型。創(chuàng )建實(shí)例對象時(shí),用 Class 對象作為模板,實(shí)例對象既包含了數據(屬性),又包含了數據類(lèi)型特有的代碼(方法)。

類(lèi)可以基于一個(gè)或多個(gè)其他類(lèi)(稱(chēng)之為基類(lèi))進(jìn)行創(chuàng )建?;?lèi)的屬性和方法都得以繼承。這樣對象模型就可以通過(guò)繼承不斷地進(jìn)行細化。比如通用的 Mailbox 類(lèi)提供了郵箱的基本訪(fǎng)問(wèn)方法.,它的子類(lèi) MboxMailbox、 MaildirMailbox、 OutlookMailbox 則能夠處理各種特定的郵箱格式。

什么是方法??

方法是屬于對象的函數,對于對象 x ,通常以 x.name(arguments...) 的形式調用。方法以函數的形式給出定義,位于類(lèi)的定義內:

class C:
    def meth(self, arg):
        return arg * 2 + self.attribute

什么是 self ??

Self 只是方法的第一個(gè)參數的習慣性名稱(chēng)。假定某個(gè)類(lèi)中有個(gè)方法定義為 meth(self, a, b, c) ,則其實(shí)例 x 應以 x.meth(a, b, c) 的形式進(jìn)行調用;而被調用的方法則應視其為做了 meth(x, a, b, c) 形式的調用。

另請參閱 為什么必須在方法定義和調用中顯式使用“self”? 。

如何檢查對象是否為給定類(lèi)或其子類(lèi)的一個(gè)實(shí)例??

可使用內置函數 isinstance(obj, cls)??梢詸z測對象是否屬于多個(gè)類(lèi)中某一個(gè)的實(shí)例,只要把單個(gè)類(lèi)換成元組即可,比如 isinstance(obj, (class1, class2, ...)),還可以檢查對象是否屬于某個(gè) Python 內置類(lèi)型,例如 isinstance(obj, str)isinstance(obj, (int, float, complex))。

請注意 isinstance() 還會(huì )檢測派生自 abstract base class 的虛繼承。 因此對于已注冊的類(lèi),即便沒(méi)有直接或間接繼承自抽象基類(lèi),對抽象基類(lèi)的檢測都將返回 True 。要想檢測“真正的繼承”,請掃描類(lèi)的 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

請注意,大多數程序不會(huì )經(jīng)常用 isinstance() 對用戶(hù)自定義類(lèi)進(jìn)行檢測。 如果是自已開(kāi)發(fā)的類(lèi),更合適的面向對象編程風(fēng)格應該是在類(lèi)中定義多種方法,以封裝特定的行為,而不是檢查對象屬于什么類(lèi)再據此干不同的事。假定有如下執行某些操作的函數:

def search(obj):
    if isinstance(obj, Mailbox):
        ...  # code to search a mailbox
    elif isinstance(obj, Document):
        ...  # code to search a document
    elif ...

更好的方法是在所有類(lèi)上定義一個(gè) search() 方法,然后調用它:

class Mailbox:
    def search(self):
        ...  # code to search a mailbox

class Document:
    def search(self):
        ...  # code to search a document

obj.search()

什么是委托??

委托是一種面向對象的技術(shù)(也稱(chēng)為設計模式)。假設對象 x 已經(jīng)存在,現在想要改變其某個(gè)方法的行為??梢詣?chuàng )建一個(gè)新類(lèi),其中提供了所需修改方法的新實(shí)現,而將所有其他方法都委托給 x 的對應方法。

Python 程序員可以輕松實(shí)現委托。比如以下實(shí)現了一個(gè)類(lèi)似于文件的類(lèi),只是會(huì )把所有寫(xiě)入的數據轉換為大寫(xiě):

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 類(lèi)重新定義了 write() 方法,在調用下層的 self._outfile.write() 方法之前,會(huì )將參數字符串轉換為大寫(xiě)。其他所有方法則都被委托給下層的 self._outfile 對象。委托是通過(guò) __getattr__ 方法完成的;請參閱 語(yǔ)言參考 了解有關(guān)控制屬性訪(fǎng)問(wèn)的更多信息。

請注意,更常見(jiàn)情況下,委托可能會(huì )變得比較棘手。如果屬性既需要寫(xiě)入又需要讀取,那么類(lèi)還必須定義 __setattr__() 方法,而這時(shí)就必須十分的小心?;A的 __setattr__() 實(shí)現代碼大致如下:

class X:
    ...
    def __setattr__(self, name, value):
        self.__dict__[name] = value
    ...

大多數 __setattr__() 實(shí)現必須修改 self.__dict__ 來(lái)為自身保存局部狀態(tài),而不至于引起無(wú)限遞歸。

如何在擴展基類(lèi)的派生類(lèi)中調用基類(lèi)中定義的方法??

使用內置的 super() 函數:

class Derived(Base):
    def meth(self):
        super().meth()  # calls Base.meth

在下面的例子中,super() 將自動(dòng)根據它的調用方 (self 值) 來(lái)確定實(shí)例對象,使用 type(self).__mro__ 查找 method resolution order (MRO),并返回 MRO 中位于 Derived 之后的項: Base。

如何讓代碼更容易對基類(lèi)進(jìn)行修改??

可以為基類(lèi)賦一個(gè)別名并基于該別名進(jìn)行派生。這樣只要修改賦給該別名的值即可。順便提一下,如要動(dòng)態(tài)地確定(例如根據可用的資源)該使用哪個(gè)基類(lèi),這個(gè)技巧也非常方便。例如:

class Base:
    ...

BaseAlias = Base

class Derived(BaseAlias):
    ...

如何創(chuàng )建靜態(tài)類(lèi)數據和靜態(tài)類(lèi)方法??

Python 支持靜態(tài)數據和靜態(tài)方法(以 C++ 或 Java 的定義而言)。

靜態(tài)數據只需定義一個(gè)類(lèi)屬性即可。若要為屬性賦新值,則必須在賦值時(shí)顯式使用類(lèi)名:

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__ 回溯到基類(lèi) C 的搜索路徑上的某個(gè)類(lèi)所重載。

注意:在 C 的某個(gè)方法內部,像 self.count = 42 這樣的賦值將在 self 自身的字典中新建一個(gè)名為 "count" 的不相關(guān)實(shí)例。 想要重新綁定類(lèi)靜態(tài)數據名稱(chēng)就必須總是指明類(lèi)名,無(wú)論是在方法內部還是外部:

C.count = 314

Python 支持靜態(tài)方法:

class C:
    @staticmethod
    def static(arg1, arg2, arg3):
        # No 'self' parameter!
        ...

不過(guò)為了獲得靜態(tài)方法的效果,還有一種做法直接得多,也即使用模塊級函數即可:

def getcount():
    return C.count

如果代碼的結構化比較充分,每個(gè)模塊只定義了一個(gè)類(lèi)(或者多個(gè)類(lèi)的層次關(guān)系密切相關(guān)),那就具備了應有的封裝。

在 Python 中如何重載構造函數(或方法)??

這個(gè)答案實(shí)際上適用于所有方法,但問(wèn)題通常首先出現于構造函數的應用場(chǎng)景中。

在 C++ 中,代碼會(huì )如下所示:

class C {
    C() { cout << "No arguments\n"; }
    C(int i) { cout << "Argument is " << i << "\n"; }
}

在 Python 中,只能編寫(xiě)一個(gè)構造函數,并用默認參數捕獲所有情況。例如:

class C:
    def __init__(self, i=None):
        if i is None:
            print("No arguments")
        else:
            print("Argument is", i)

這不完全等同,但在實(shí)踐中足夠接近。

也可以試試采用變長(cháng)參數列表,例如:

def __init__(self, *args):
    ...

上述做法同樣適用于所有方法定義。

在用 __spam 的時(shí)候得到一個(gè)類(lèi)似 _SomeClassName__spam 的錯誤信息。?

以雙下劃線(xiàn)打頭的變量名會(huì )被“破壞”,以便以一種簡(jiǎn)單高效的方式定義類(lèi)私有變量。任何形式為 __spam 的標識符(至少前綴兩個(gè)下劃線(xiàn),至多后綴一個(gè)下劃線(xiàn))文本均會(huì )被替換為 _classname__spam,其中 classname 為去除了全部前綴下劃線(xiàn)的當前類(lèi)名稱(chēng)。

這并不能保證私密性:外部用戶(hù)仍然可以訪(fǎng)問(wèn) "_classname__spam" 屬性,私有變量值也在對象的 __dict__ 中可見(jiàn)。 許多 Python 程序員根本不操心要去使用私有變量名。

類(lèi)定義了 __del__ 方法,但是刪除對象時(shí)沒(méi)有調用它。?

這有幾個(gè)可能的原因。

del 語(yǔ)句不一定調用 __del__() —— 它只是減少對象的引用計數,如果(引用計數)達到零,才會(huì )調用 __del__()。

如果數據結構包含循環(huán)鏈接(比如樹(shù)的每個(gè)子節點(diǎn)都帶有父節點(diǎn)的引用,而每個(gè)父節點(diǎn)也帶有子節點(diǎn)的列表),則引用計數永遠不會(huì )回零。盡管 Python 偶爾會(huì )用某種算法檢測這種循環(huán)引用,但在數據結構的最后一條引用消失之后,垃圾收集器可能還要過(guò)段時(shí)間才會(huì )運行,因此 __del__() 方法可能會(huì )在不方便和隨機的時(shí)刻被調用。這對于重現一個(gè)問(wèn)題,是非常不方便的。更糟糕的是,各個(gè)對象的 __del__() 方法是以隨機順序執行的。雖然可以運行 gc.collect() 來(lái)強制執行垃圾回收工作,但 仍會(huì )存在 一些對象永遠不會(huì )被回收的失控情況。

盡管有垃圾回收器的存在,但為對象定義顯式的 close() 方法,只要一用完即可供調用,這依然是一個(gè)好主意。這樣 close() 方法即可刪除引用子對象的屬性。請勿直接調用 __del__() —— 而 __del__() 應該調用 close(),并且應能確??梢詫ν粚ο蠖啻握{用 close() 。

另一種避免循環(huán)引用的做法是利用 weakref 模塊,該模塊允許指向對象但不增加其引用計數。例如,樹(shù)狀數據結構應該對父節點(diǎn)和同級節點(diǎn)使用弱引用(如果真要用的話(huà)?。?/p>

最后提一下,如果 __del__() 方法引發(fā)了異常,會(huì )將警告消息打印到 sys.stderr 。

如何獲取給定類(lèi)的所有實(shí)例的列表??

Python 不會(huì )記錄類(lèi)(或內置類(lèi)型)的實(shí)例??梢栽陬?lèi)的構造函數中編寫(xiě)代碼,通過(guò)保留每個(gè)實(shí)例的弱引用列表來(lái)跟蹤所有實(shí)例。

為什么 id() 的結果看起來(lái)不是唯一的??

id() 返回一個(gè)整數,該整數在對象的生命周期內保證是唯一的。 因為在 CPython 中,這是對象的內存地址,所以經(jīng)常發(fā)生在從內存中刪除對象之后,下一個(gè)新創(chuàng )建的對象被分配在內存中的相同位置。 這個(gè)例子說(shuō)明了這一點(diǎn):

>>>
>>> id(1000) 
13901272
>>> id(2000) 
13901272

這兩個(gè) id 屬于不同的整數對象,之前先創(chuàng )建了對象,執行 id() 調用后又立即被刪除了。若要確保檢測 id 時(shí)的對象仍處于活動(dòng)狀態(tài),請再創(chuàng )建一個(gè)對該對象的引用:

>>>
>>> a = 1000; b = 2000
>>> id(a) 
13901272
>>> id(b) 
13891296

什么情況下可以依靠 is 運算符進(jìn)行對象的身份相等性測試??

is 運算符可用于測試對象的身份相等性。a is b 等價(jià)于 id(a) == id(b)。

身份相等性最重要的特性就是對象總是等同于自身,a is a 一定返回 True。身份相等性測試的速度通常比相等性測試要快。而且與相等性測試不一樣,身份相等性測試會(huì )確保返回布爾值 TrueFalse。

但是,身份相等性測試 只能 在對象身份確定的場(chǎng)景下才可替代相等性測試。一般來(lái)說(shuō),有以下3種情況對象身份是可以確定的:

1) 賦值操作創(chuàng )建了新的名稱(chēng)但沒(méi)有改變對象身份。 在賦值操作 new = old 之后,可以保證 new is old。

2) 將對象置入存放對象引用的容器,對象身份不會(huì )改變。在列表賦值操作 s[0] = x 之后,可以保證 s[0] is x。

3) 單例對象,也即該對象只能存在一個(gè)實(shí)例。在賦值操作 a = Noneb = None 之后,可以保證 a is b,因為 None 是單例對象。

其他大多數情況下,都不建議使用身份相等性測試,而應采用相等性測試。尤其是不應將身份相等性測試用于檢測常量值,例如 intstr,因為他們并不一定是單例對象:

>>>
>>> a = 1000
>>> b = 500
>>> c = b + 500
>>> a is c
False

>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a is c
False

同樣地,可變容器的新實(shí)例,對象身份一定不同:

>>>
>>> a = []
>>> b = []
>>> a is b
False

在標準庫代碼中,給出了一些正確使用對象身份測試的常見(jiàn)模式:

1) 正如 PEP 8 所推薦的,對象身份測試是 None 值的推薦檢測方式。這樣的代碼讀起來(lái)就像自然的英文,并可以避免與其他可能為布爾值且計算結果為 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ě)容器的實(shí)現代碼時(shí),有時(shí)需要用對象身份測試來(lái)加強相等性檢測。這樣代碼就不會(huì )被 float('NaN') 這類(lèi)與自身不相等的對象所干擾。

例如,以下是 collections.abc.Sequence.__contains__() 的實(shí)現代碼:

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'

我該如何緩存方法調用??

緩存方法的兩個(gè)主要工具是 functools.cached_property()functools.lru_cache()。 前者在實(shí)例層級上存儲結果而后者在類(lèi)層級上存儲結果。

cached_property 方式僅適用于不接受任何參數的方法。 它不會(huì )創(chuàng )建對實(shí)例的引用。 被緩存的方法結果將僅在實(shí)例的生存其內被保留。

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 方式適用于具有可哈希參數的方法。 它會(huì )創(chuàng )建對實(shí)例的引用,除非特別設置了傳入弱引用。

最少近期使用算法的優(yōu)點(diǎn)是緩存會(huì )受指定的 maxsize 限制。 它的缺點(diǎn)是實(shí)例會(huì )保持存活,直到其達到生存期或者緩存被清空。

這個(gè)例子演示了幾種不同的方式:

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 從不改變。 如果相關(guān)實(shí)例屬性是可變對象,則 cached_property 方式就不再適用,因為它無(wú)法檢測到屬性的改變。

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 文件??

當首次導入模塊時(shí)(或當前已編譯文件創(chuàng )建之后源文件發(fā)生了改動(dòng)),在 .py 文件所在目錄的 __pycache__ 子目錄下會(huì )創(chuàng )建一個(gè)包含已編譯代碼的 .pyc 文件。該 .pyc 文件的名稱(chēng)開(kāi)頭部分將與 .py 文件名相同,并以 .pyc 為后綴,中間部分則依據創(chuàng )建它的 python 版本而各不相同。(詳見(jiàn) PEP 3147。)

.pyc 文件有可能會(huì )無(wú)法創(chuàng )建,原因之一是源碼文件所在的目錄存在權限問(wèn)題,這樣就無(wú)法創(chuàng )建 __pycache__ 子目錄。假如以某個(gè)用戶(hù)開(kāi)發(fā)程序而以另一用戶(hù)運行程序,就有可能發(fā)生權限問(wèn)題,測試 Web 服務(wù)器就屬于這種情況。

除非設置了 PYTHONDONTWRITEBYTECODE 環(huán)境變量,否則導入模塊并且 Python 能夠創(chuàng )建``__pycache__``子目錄并把已編譯模塊寫(xiě)入該子目錄(權限、存儲空間等等)時(shí),.pyc 文件就將自動(dòng)創(chuàng )建。

在最高層級運行的 Python 腳本不會(huì )被視為經(jīng)過(guò)了導入操作,因此不會(huì )創(chuàng )建 .pyc 文件。假定有一個(gè)最高層級的模塊文件 foo.py,它導入了另一個(gè)模塊 xyz.py,當運行 foo 模塊(通過(guò)輸入 shell 命令 python foo.py ),則會(huì )為 xyz 創(chuàng )建一個(gè) .pyc,因為 xyz 是被導入的,但不會(huì )為 foo 創(chuàng )建 .pyc 文件,因為 foo.py 不是被導入的。

若要為 foo 創(chuàng )建 .pyc 文件 —— 即為未做導入的模塊創(chuàng )建 .pyc 文件 —— 可以利用 py_compilecompileall 模塊。

py_compile 模塊能夠手動(dòng)編譯任意模塊。 一種做法是交互式地使用該模塊中的 compile() 函數:

>>>
>>> import py_compile
>>> py_compile.compile('foo.py')                 

這將會(huì )將 .pyc 文件寫(xiě)入與 foo.py 相同位置下的 __pycache__ 子目錄(或者你也可以通過(guò)可選參數 cfile 來(lái)重載該行為)。

還可以用 compileall 模塊自動(dòng)編譯一個(gè)或多個(gè)目錄下的所有文件。只要在命令行提示符中運行 compileall.py 并給出要編譯的 Python 文件所在目錄路徑即可:

python -m compileall .

如何找到當前模塊名稱(chēng)??

模塊可以查看預定義的全局變量 __name__ 獲悉自己的名稱(chēng)。如其值為 '__main__' ,程序將作為腳本運行。通常,許多通過(guò)導入使用的模塊同時(shí)也提供命令行接口或自檢代碼,這些代碼只在檢測到處于 __name__ 之后才會(huì )執行:

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

問(wèn)題是解釋器將執行以下步驟:

  • 首先導入 foo

  • foo 創(chuàng )建空的全局變量

  • 編譯 foo 并開(kāi)始執行

  • foo 導入 bar

  • bar 創(chuàng )建空的全局變量

  • 編譯 bar 并開(kāi)始執行

  • bar 導入 foo (該步驟無(wú)操作,因為已經(jīng)有一個(gè)名為 foo 的模塊)。

  • 導入機制嘗試從 foo_var 全局變量讀取``foo``,用來(lái)設置 bar.foo_var = foo.foo_var

最后一步失敗了,因為 Python 還沒(méi)有完成對 foo 的解釋?zhuān)琭oo 的全局符號字典仍然是空的。

當你使用 import foo ,然后嘗試在全局代碼中訪(fǎng)問(wèn) foo.foo_var 時(shí),會(huì )發(fā)生同樣的事情。

這個(gè)問(wèn)題有(至少)三種可能的解決方法。

Guido van Rossum 建議完全避免使用 from <module> import ... ,并將所有代碼放在函數中。全局變量和類(lèi)變量的初始化只應使用常量或內置函數。這意味著(zhù)導入模塊中的所有內容都以 <module>.<name> 的形式引用。

Jim Roskind 建議每個(gè)模塊都應遵循以下順序:

  • 導出(全局變量、函數和不需要導入基類(lèi)的類(lèi))

  • import 語(yǔ)句

  • 本模塊的功能代碼(包括根據導入值進(jìn)行初始化的全局變量)。

Van Rossum doesn't like this approach much because the imports appear in a strange place, but it does work.

Matthias Urlichs 建議對代碼進(jìn)行重構,使得遞歸導入根本就沒(méi)必要發(fā)生。

這些解決方案并不相互排斥。

__import__('x.y.z') 返回的是 <module 'x'> ;該如何得到 z 呢??

不妨考慮換用 importlib 中的函數 import_module()

z = importlib.import_module('x.y.z')

對已導入的模塊進(jìn)行了編輯并重新導入,但變動(dòng)沒(méi)有得以體現。這是為什么??

出于效率和一致性的原因,Python 僅在第一次導入模塊時(shí)讀取模塊文件。否則,在一個(gè)多模塊的程序中,每個(gè)模塊都會(huì )導入相同的基礎模塊,那么基礎模塊將會(huì )被一而再、再而三地解析。如果要強行重新讀取已更改的模塊,請執行以下操作:

import importlib
import modname
importlib.reload(modname)

警告:這種技術(shù)并非萬(wàn)無(wú)一失。尤其是模塊包含了以下語(yǔ)句時(shí):

from modname import some_objects

仍將繼續使用前一版的導入對象。如果模塊包含了類(lèi)的定義,并 不會(huì ) 用新的類(lèi)定義更新現有的類(lèi)實(shí)例。這樣可能會(huì )導致以下矛盾的行為:

>>>
>>> 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

只要把類(lèi)對象的 id 打印出來(lái),問(wèn)題的性質(zhì)就會(huì )一目了然:

>>>
>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'