編程常見問題?

目錄

一般問題?

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

有的。

以下介紹了一些 Python 的調(diào)試器,用內(nèi)置函數(shù) breakpoint() 即可切入這些調(diào)試器中。

pdb 模塊是一個(gè)簡單但是夠用的控制臺模式 Python 調(diào)試器。 它是標(biāo)準(zhǔn) Python 庫的一部分,并且 已收錄于庫參考手冊。 你也可以通過使用 pdb 代碼作為樣例來編寫你自己的調(diào)試器。

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

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

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

trepan3k 是一個(gè)類似 gdb 的調(diào)試器。

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

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

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

有的。

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

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

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

如果只是想要一個(gè)獨(dú)立的程序,以便用戶不必預(yù)先安裝 Python 即可下載和運(yùn)行它,則不需要將 Python 編譯成 C 代碼。有許多工具可以檢測程序所需的模塊,并將這些模塊與 Python 二進(jìn)制程序捆綁在一起生成單個(gè)可執(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 語句,并在標(biāo)準(zhǔn) Python 路徑和源碼目錄(用于內(nèi)置模塊)檢索這些模塊。然后,把這些模塊的 Python 字節(jié)碼轉(zhuǎn)換為 C 代碼(可以利用 marshal 模塊轉(zhuǎn)換為代碼對象的數(shù)組初始化器),并創(chuàng)建一個(gè)定制的配置文件,該文件僅包含程序?qū)嶋H用到的內(nèi)置模塊。然后,編譯生成的 C 代碼并將其與 Python 解釋器的其余部分鏈接,形成一個(gè)自給自足的二進(jìn)制文件,其功能與 Python 腳本代碼完全相同。

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

是否有 Python 編碼標(biāo)準(zhǔn)或風(fēng)格指南??

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

語言核心內(nèi)容?

變量明明有值,為什么還會出現(xiàn) UnboundLocalError??

因?yàn)樵诤瘮?shù)內(nèi)部某處添加了一條賦值語句,導(dǎo)致之前正常工作的代碼報(bào)出 UnboundLocalError 錯誤,這可能是有點(diǎn)令人驚訝。

以下代碼:

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

正常工作,但是以下代碼

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

會得到一個(gè) UnboundLocalError :

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

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

在上面的示例中,可以將外部作用域的變量聲明為全局變量以便訪問:

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

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

>>>
>>> print(x)
11

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

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

Python 的局部變量和全局變量有哪些規(guī)則??

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

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

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

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

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

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

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

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

>>>
>>> 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)建了一個(gè)新的 lambda 本地變量 n,并在定義 lambda 時(shí)計(jì)算其值,使其與循環(huán)當(dāng)前時(shí)點(diǎn)的 x 值相同。這意味著 n 的值在第 1 個(gè) lambda 中為 0 ,在第 2 個(gè) lambda 中為 1 ,在第 3 個(gè)中為 2,依此類推。因此現(xiàn)在每個(gè) lambda 都會返回正確結(jié)果:

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

請注意,上述表現(xiàn)并不是 lambda 所特有的,常規(guī)的函數(shù)也同樣適用。

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

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

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í)現(xiàn)單例設(shè)計(jì)模式的基礎(chǔ)。

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

通常請勿使用 from modulename import * 。因?yàn)檫@會擾亂 importer 的命名空間,且會造成未定義名稱更難以被 Linter 檢查出來。

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

按如下順序?qū)肽K就是一種好做法:

  1. 標(biāo)準(zhǔn)庫模塊——比如: sys 、 osgetopt 、 re 等。

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

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

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

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

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

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

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

為什么對象之間會共享默認(rèn)值??

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

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

第一次調(diào)用此函數(shù)時(shí), mydict 中只有一個(gè)數(shù)據(jù)項(xiàng)。第二次調(diào)用 mydict 則會包含兩個(gè)數(shù)據(jù)項(xiàng),因?yàn)?foo() 開始執(zhí)行時(shí), mydict 中已經(jīng)帶有一個(gè)數(shù)據(jù)項(xiàng)了。

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

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

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

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

而應(yīng)這么寫:

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

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

# 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

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

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

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

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

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

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

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

foobarkwargsfunc 的形參。 不過在調(diào)用 func 時(shí),例如:

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

42314somevar 則是實(shí)參。

為什么修改列表 'y' 也會更改列表 'x'??

如果代碼編寫如下:

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

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

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

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

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

在調(diào)用 append() 之后,該可變對象的內(nèi)容由 [] 變?yōu)?[10]。由于 x 和 y 這兩個(gè)變量引用了同一對象,因此用其中任意一個(gè)名稱所訪問到的都是修改后的值 [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

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

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

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

換而言之:

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

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

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

如何編寫帶有輸出參數(shù)的函數(shù)(按照引用調(diào)用)??

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

  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. 使用全局變量。這不是線程安全的方案,不推薦使用。

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

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

    沒有什么理由要把問題搞得這么復(fù)雜。

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

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

有兩種選擇:嵌套作用域、可調(diào)用對象。假定需要定義 linear(a,b) ,其返回結(jié)果是一個(gè)計(jì)算出 a*x+b 的函數(shù) f(x)。 采用嵌套作用域的方案如下:

def linear(a, b):
    def result(x):
        return a * x + b
    return result

或者可采用可調(diào)用對象:

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)

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

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

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

對象可以為多個(gè)方法的運(yùn)行狀態(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() 的表現(xiàn),就如同共享了同一計(jì)數(shù)變量一樣。

如何復(fù)制 Python 對象??

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

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

newdict = olddict.copy()

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

new_l = l[:]

如何找到對象的方法或?qū)傩裕?/a>?

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

如何用代碼獲取對象的名稱??

一般而言這是無法實(shí)現(xiàn)的,因?yàn)閷ο蟛⒉淮嬖谡嬲拿Q。賦值本質(zhì)上是把某個(gè)名稱綁定到某個(gè)值上;defclass 語句同樣如此,只是值換成了某個(gè)可調(diào)用對象。比如以下代碼:

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

可以不太嚴(yán)謹(jǐn)?shù)卣f,上述類具有一個(gè)名稱:即便它綁定了兩個(gè)名稱并通過名稱 B 發(fā)起調(diào)用,可是創(chuàng)建出來的實(shí)例仍被視為是類 A 的實(shí)例。但無法說出實(shí)例的名稱是 a 還是 b,因?yàn)檫@兩個(gè)名稱都被綁定到同一個(gè)值上了。

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

在 comp.lang.python 中,F(xiàn)redrik Lundh 在回答這樣的問題時(shí)曾經(jīng)給出過一個(gè)絕佳的類比:

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

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

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

逗號不是 Python 的運(yùn)算符。 請看以下例子:

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

由于逗號不是運(yùn)算符,而只是表達(dá)式之間的分隔符,因此上述代碼就相當(dāng)于:

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

而不是:

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

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

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

有的。語法如下:

[on_true] if [expression] else [on_false]

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

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

[expression] and [on_true] or [on_false]

然而這種做法并不保險(xiǎn),因?yàn)楫?dāng) on_true 為布爾值“假”時(shí),結(jié)果將會出錯。所以肯定還是采用 ... if ... else ... 形式為妙。

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

可以。通常是在 lambda 中嵌套 lambda 來實(shí)現(xiàn)的。請參閱以下三個(gè)來自 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ù)列表中的斜杠表示在它之前的形參全都僅限位置形參。僅限位置形參沒有可供外部使用的名稱。在調(diào)用僅接受位置形參的函數(shù)時(shí),實(shí)參只會根據(jù)位置映射到形參上。假定 divmod() 是一個(gè)僅接受位置形參的函數(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.

形參列表尾部的斜杠說明,兩個(gè)形參都是僅限位置形參。因此,用關(guān)鍵字參數(shù)調(diào)用 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ù)字和字符串?

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

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

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

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

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

為什么 -22 // 10 會返回 -3 ??

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

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

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

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

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

如何將字符串轉(zhuǎn)換為數(shù)字??

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

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

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

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

如何將數(shù)字轉(zhuǎn)換為字符串??

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

如何修改字符串??

無法修改,因?yàn)樽址遣豢勺儗ο蟆?在大多數(shù)情況下,只要將各個(gè)部分組合起來構(gòu)造出一個(gè)新字符串即可。如果需要一個(gè)能原地修改 Unicode 數(shù)據(jù)的對象,可以試試 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'

如何使用字符串調(diào)用函數(shù)/方法??

有多種技巧可供選擇。

  • 最好的做法是采用一個(gè)字典,將字符串映射為函數(shù)。其主要優(yōu)勢就是字符串不必與函數(shù)名一樣。這也是用來模擬 case 結(jié)構(gòu)的主要技巧:

    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
    
  • 利用內(nèi)置函數(shù) getattr()

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

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

    標(biāo)準(zhǔn)庫就多次使用了這個(gè)技巧,例如:

    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 表示多行,且末尾有幾個(gè)空行,則將刪除所有空行的換行符:

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

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

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

沒有。

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

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

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

Unicode 指南

性能?

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

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

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

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

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

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

  • 強(qiáng)烈建議首先要保證足夠高的代碼測試覆蓋率(通過單元測試或其他技術(shù)),因?yàn)閺?fù)雜的優(yōu)化有可能會導(dǎo)致代碼回退。

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

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

  • 使用正確的數(shù)據(jù)結(jié)構(gòu)。參考 內(nèi)置類型collections 模塊的文檔。

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

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

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

參見

專門介紹 性能提示 的wiki頁面。

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

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

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

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

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

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

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

序列(元組/列表)?

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

類型構(gòu)造器 tuple(seq) 可將任意序列(實(shí)際上是任意可迭代對象)轉(zhuǎn)換為數(shù)據(jù)項(xiàng)和順序均不變的元組。

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

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

什么是負(fù)數(shù)索引??

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

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

序列如何以逆序遍歷??

使用內(nèi)置函數(shù) reversed()

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

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

如何從列表中刪除重復(fù)項(xiàng)??

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

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

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

以上操作會將列表轉(zhuǎn)換為集合,從而刪除重復(fù)項(xiàng),然后返回成列表。

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

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

mylist[:] = filter(keep_function, mylist)
mylist[:] = (x for x in mylist if keep_condition)
mylist[:] = [x for x in mylist if keep_condition]

列表推導(dǎo)式可能是最快的。

如何在 Python 中創(chuàng)建數(shù)組??

用列表:

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

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

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

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

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

若要具備可變性,可以不用元組而是用列表。模擬 lisp car 函數(shù)的是 lisp_list[0] ,模擬 cdr 函數(shù)的是 lisp_list[1] 。僅當(dāng)真正必要時(shí)才會這么用,因?yàn)橥ǔ_@種用法要比 Python 列表慢得多。

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

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

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

打印出來貌似沒錯:

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

但如果給某一項(xiàng)賦值,結(jié)果會同時(shí)在多個(gè)位置體現(xiàn)出來:

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

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

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

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

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

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

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

如何將方法應(yīng)用于一系列對象??

可以使用列表推導(dǎo)式:

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

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

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

只要元組的元素指向可變對象,這時(shí)對元素進(jìn)行增強(qiáng)賦值,那么這里介紹的內(nèi)容都是適用的。在此只以 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] 相加,生成結(jié)果對象 2,但在試圖將運(yùn)算結(jié)果 2 賦值給元組的 0 號元素時(shí)就會報(bào)錯,因?yàn)樵M元素的指向無法更改。

其實(shí)在幕后,上述增強(qiáng)賦值語句的執(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

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

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

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

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

這相當(dāng)于:

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

a_list 所引用的對象已被修改,而引用被修改對象的指針又重新被賦值給 a_list。 賦值的最終結(jié)果沒有變化,因?yàn)樗且?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í)行,因此列表得到了擴(kuò)充,但是雖然 result 指向了 a_tuple[0] 已經(jīng)指向的同一對象,最后的賦值仍然導(dǎo)致了報(bào)錯,因?yàn)樵M是不可變的。

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

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

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

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

將它們合并到元組的迭代器中,對結(jié)果列表進(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']

對象?

什么是類??

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

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

什么是方法??

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

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

什么是 self ??

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

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

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

可使用內(nèi)置函數(shù) isinstance(obj, cls)??梢詸z測對象是否屬于多個(gè)類中某一個(gè)的實(shí)例,只要把單個(gè)類換成元組即可,比如 isinstance(obj, (class1, class2, ...)),還可以檢查對象是否屬于某個(gè) Python 內(nèi)置類型,例如 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ù)程序不會經(jīng)常用 isinstance() 對用戶自定義類進(jìn)行檢測。 如果是自已開發(fā)的類,更合適的面向?qū)ο缶幊田L(fēng)格應(yīng)該是在類中定義多種方法,以封裝特定的行為,而不是檢查對象屬于什么類再據(jù)此干不同的事。假定有如下執(zhí)行某些操作的函數(shù):

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

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

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

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

obj.search()

什么是委托??

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

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

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() 方法,在調(diào)用下層的 self._outfile.write() 方法之前,會將參數(shù)字符串轉(zhuǎn)換為大寫。其他所有方法則都被委托給下層的 self._outfile 對象。委托是通過 __getattr__ 方法完成的;請參閱 語言參考 了解有關(guān)控制屬性訪問的更多信息。

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

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

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

如何在擴(kuò)展基類的派生類中調(diào)用基類中定義的方法??

使用內(nèi)置的 super() 函數(shù):

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

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

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

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

class Base:
    ...

BaseAlias = Base

class Derived(BaseAlias):
    ...

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

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

靜態(tài)數(shù)據(jù)只需定義一個(gè)類屬性即可。若要為屬性賦新值,則必須在賦值時(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)cc.count 也同樣指向 C.count ,除非被 c 自身重載,或者被從 c.__class__ 回溯到基類 C 的搜索路徑上的某個(gè)類所重載。

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

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

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

在 Python 中如何重載構(gòu)造函數(shù)(或方法)??

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

在 C++ 中,代碼會如下所示:

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

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

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

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

也可以試試采用變長參數(shù)列表,例如:

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

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

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

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

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

類定義了 __del__ 方法,但是刪除對象時(shí)沒有調(diào)用它。?

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

del 語句不一定調(diào)用 __del__() —— 它只是減少對象的引用計(jì)數(shù),如果(引用計(jì)數(shù))達(dá)到零,才會調(diào)用 __del__()。

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

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

另一種避免循環(huán)引用的做法是利用 weakref 模塊,該模塊允許指向?qū)ο蟮辉黾悠湟糜?jì)數(shù)。例如,樹狀數(shù)據(jù)結(jié)構(gòu)應(yīng)該對父節(jié)點(diǎn)和同級節(jié)點(diǎn)使用弱引用(如果真要用的話!)

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

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

Python 不會記錄類(或內(nèi)置類型)的實(shí)例??梢栽陬惖臉?gòu)造函數(shù)中編寫代碼,通過保留每個(gè)實(shí)例的弱引用列表來跟蹤所有實(shí)例。

為什么 id() 的結(jié)果看起來不是唯一的??

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

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

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

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

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

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

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

但是,身份相等性測試 只能 在對象身份確定的場景下才可替代相等性測試。一般來說,有以下3種情況對象身份是可以確定的:

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

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

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

其他大多數(shù)情況下,都不建議使用身份相等性測試,而應(yīng)采用相等性測試。尤其是不應(yīng)將身份相等性測試用于檢測常量值,例如 intstr,因?yàn)樗麄儾⒉灰欢ㄊ菃卫龑ο螅?/p>

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

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

同樣地,可變?nèi)萜鞯男聦?shí)例,對象身份一定不同:

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

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

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

例如,以下是 collections.abc.Sequence.__contains__() 的實(shí)現(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'

我該如何緩存方法調(diào)用??

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

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

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)建對實(shí)例的引用,除非特別設(shè)置了傳入弱引用。

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

這個(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 方式就不再適用,因?yàn)樗鼰o法檢測到屬性的改變。

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

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

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

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

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

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

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

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

這將會將 .pyc 文件寫入與 foo.py 相同位置下的 __pycache__ 子目錄(或者你也可以通過可選參數(shù) cfile 來重載該行為)。

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

python -m compileall .

如何找到當(dāng)前模塊名稱??

模塊可以查看預(yù)定義的全局變量 __name__ 獲悉自己的名稱。如其值為 '__main__' ,程序?qū)⒆鳛槟_本運(yùn)行。通常,許多通過導(dǎo)入使用的模塊同時(shí)也提供命令行接口或自檢代碼,這些代碼只在檢測到處于 __name__ 之后才會執(zhí)行:

def main():
    print('Running test...')
    ...

if __name__ == '__main__':
    main()

如何讓模塊相互導(dǎo)入??

假設(shè)有以下模塊:

foo.py:

from bar import bar_var
foo_var = 1

bar.py:

from foo import foo_var
bar_var = 2

問題是解釋器將執(zhí)行以下步驟:

  • 首先導(dǎo)入 foo

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

  • 編譯 foo 并開始執(zhí)行

  • foo 導(dǎo)入 bar

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

  • 編譯 bar 并開始執(zhí)行

  • bar 導(dǎo)入 foo (該步驟無操作,因?yàn)橐呀?jīng)有一個(gè)名為 foo 的模塊)。

  • 導(dǎo)入機(jī)制嘗試從 foo_var 全局變量讀取``foo``,用來設(shè)置 bar.foo_var = foo.foo_var

最后一步失敗了,因?yàn)?Python 還沒有完成對 foo 的解釋,foo 的全局符號字典仍然是空的。

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

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

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

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

  • 導(dǎo)出(全局變量、函數(shù)和不需要導(dǎo)入基類的類)

  • import 語句

  • 本模塊的功能代碼(包括根據(jù)導(dǎo)入值進(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)行重構(gòu),使得遞歸導(dǎo)入根本就沒必要發(fā)生。

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

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

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

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

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

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

import importlib
import modname
importlib.reload(modname)

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

from modname import some_objects

仍將繼續(xù)使用前一版的導(dǎo)入對象。如果模塊包含了類的定義,并 不會 用新的類定義更新現(xiàn)有的類實(shí)例。這樣可能會導(dǎo)致以下矛盾的行為:

>>>
>>> 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 打印出來,問題的性質(zhì)就會一目了然:

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