花下貓語: 先祝大家假期快樂!今天,我要分享一篇長文,選自 Python 的官方文件。它列舉了 27 個設計及歷史的問題,其中有些問題我曾經分享過,例如為什麼使用顯式的 self、浮點數的問題、len(x) 而非 x.len() 等等。大部分的回答很簡略精要,適合在空閒之餘翻閱。建議你先收藏起來,隨時檢視,溫故知新。
目錄
- 為什麼Python使用縮排來分組語句?
- 為什麼簡單的算術運算得到奇怪的結果?
- 為什麼浮點計算不準確?
- 為什麼Python字串是不可變的?
- 為什麼必須在方法定義和呼叫中顯式使用“self”?
- 為什麼不能在表示式中賦值?
- 為什麼Python對某些功能(例如list.index())使用方法來實現,而其他功能(例如len(List))使用函式實現?
- 為什麼 join()是一個字串方法而不是列表或元組方法?
- 異常有多快?
- 為什麼Python中沒有switch或case語句?
- 難道不能在直譯器中模擬執行緒,而非得依賴特定於作業系統的執行緒實現嗎?
- 為什麼lambda表示式不能包含語句?
- 可以將Python編譯為機器程式碼,C或其他語言嗎?
- Python如何管理記憶體?
- 為什麼CPython不使用更傳統的垃圾回收方案?
- CPython退出時為什麼不釋放所有記憶體?
- 為什麼有單獨的元組和列表資料型別?
- 列表是如何在CPython中實現的?
- 字典是如何在CPython中實現的?
- 為什麼字典key必須是不可變的?
- 為什麼 list.sort() 沒有返回排序列表?
- 如何在Python中指定和實施介面規範?
- 為什麼沒有goto?
- 為什麼原始字串(r-strings)不能以反斜槓結尾?
- 為什麼Python沒有屬性賦值的“with”語句?
- 為什麼 if/while/def/class語句需要冒號?
- 為什麼Python在列表和元組的末尾允許使用逗號?
Guido van Rossum 認為使用縮排進行分組非常優雅,並且大大提高了普通Python程式的清晰度。大多數人在一段時間後就學會並喜歡上這個功能。
由於沒有開始/結束括號,因此解析器感知的分組與人類讀者之間不會存在分歧。偶爾C程式設計師會遇到像這樣的程式碼片段:
if (x <= y)
x++;
y--;
z++;
複製程式碼
如果條件為真,則只執行 x++
語句,但縮排會使你認為情況並非如此。即使是經驗豐富的C程式設計師有時會長時間盯著它,想知道為什麼即使 x > y
, y
也在減少。
因為沒有開始/結束括號,所以Python不太容易發生編碼式衝突。在C中,括號可以放到許多不同的位置。如果您習慣於閱讀和編寫使用一種風格的程式碼,那麼在閱讀(或被要求編寫)另一種風格時,您至少會感到有些不安。
許多編碼風格將開始/結束括號單獨放在一行上。這使得程式相當長,浪費了寶貴的螢幕空間,使得更難以對程式進行全面的瞭解。理想情況下,函式應該適合一個螢幕(例如,20--30行)。 20行Python可以完成比20行C更多的工作。這不僅僅是由於缺少開始/結束括號 -- 缺少宣告和高階資料型別也是其中的原因 -- 但縮排基於語法肯定有幫助。
請看下一個問題。
使用者經常對這樣的結果感到驚訝:
>>> 1.2 - 1.0
0.19999999999999996
複製程式碼
並且認為這是 Python中的一個 bug。其實不是這樣。這與 Python 關係不大,而與底層平臺如何處理浮點數字關係更大。
CPython 中的 float
型別使用C語言的 double
型別進行儲存。 float
物件的值是以固定的精度(通常為 53 位)儲存的二進位制浮點數,由於 Python 使用 C 操作,而後者依賴於處理器中的硬體實現來執行浮點運算。 這意味著就浮點運算而言,Python 的行為類似於許多流行的語言,包括 C 和 Java。
許多可以輕鬆地用十進位制表示的數字不能用二進位制浮點表示。例如,在輸入以下語句後:
>>> x = 1.2
複製程式碼
為 x
儲存的值是與十進位制的值 1.2
(非常接近) 的近似值,但不完全等於它。 在典型的機器上,實際儲存的值是:
1.0011001100110011001100110011001100110011001100110011 (binary)
複製程式碼
它對應於十進位制數值:
1.1999999999999999555910790149937383830547332763671875 (decimal)
複製程式碼
典型的 53 位精度為 Python 浮點數提供了 15-16 位小數的精度。
要獲得更完整的解釋,請參閱 Python 教程中的 浮點算術 一章。
有幾個優點。
一個是效能:知道字串是不可變的,意味著我們可以在建立時為它分配空間,並且儲存需求是固定不變的。這也是元組和列表之間區別的原因之一。
另一個優點是,Python 中的字串被視為與數字一樣“基本”。 任何動作都不會將值 8 更改為其他值,在 Python 中,任何動作都不會將字串 "8" 更改為其他值。
這個想法借鑑了 Modula-3 語言。 出於多種原因它被證明是非常有用的。
首先,更明顯的顯示出,使用的是方法或例項屬性而不是區域性變數。 閱讀 self.x
或 self.meth()
可以清楚地表明,即使您不知道類的定義,也會使用例項變數或方法。在 C++ 中,可以通過缺少區域性變數宣告來判斷(假設全域性變數很少見或容易識別) —— 但是在 Python 中沒有區域性變數宣告,所以必須查詢類定義才能確定。 一些 C++ 和 Java 編碼標準要求例項屬性具有 m_
字首,因此這種顯式性在這些語言中仍然有用。
其次,這意味著如果要顯式引用或從特定類呼叫該方法,不需要特殊語法。 在 C++ 中,如果你想使用在派生類中重寫基類中的方法,你必須使用 ::
運算子 -- 在 Python 中你可以編寫 baseclass.methodname(self,
。 這對於 init__
方法非常有用,特別是在派生類方法想要擴充套件同名的基類方法,而必須以某種方式呼叫基類方法時。
最後,它解決了變數賦值的語法問題:為了 Python 中的區域性變數(根據定義!)在函式體中賦值的那些變數(並且沒有明確宣告為全域性)賦值,就必須以某種方式告訴直譯器一個賦值是為了分配一個例項變數而不是一個區域性變數,它最好是通過語法實現的(出於效率原因)。 C++ 通過宣告來做到這一點,但是 Python 沒有宣告,僅僅為了這個目的而引入它們會很可惜。 使用顯式的 self.var
很好地解決了這個問題。 類似地,對於使用例項變數,必須編寫 self.var
意味著對方法內部的非限定名稱的引用不必搜尋例項的目錄。 換句話說,區域性變數和例項變數存在於兩個不同的名稱空間中,您需要告訴 Python 使用哪個名稱空間。
許多習慣於C或Perl的人抱怨,他們想要使用C 的這個特性:
while (line = readline(f)) {
// do something with line
}
複製程式碼
但在Python中被強制寫成這樣:
while True:
line = f.readline()
if not line:
break
... # do something with line
複製程式碼
不允許在 Python 表示式中賦值的原因是這些其他語言中常見的、很難發現的錯誤,是由這個結構引起的:
if (x = 0) {
// error handling
}
else {
// code that only works for nonzero x
}
複製程式碼
錯誤是一個簡單的錯字: x = 0
,將0賦給變數 x
,而比較 x == 0
肯定是可以預期的。
已經有許多替代方案提案。 大多數是為了少打一些字的黑客方案,但使用任意或隱含的語法或關鍵詞,並不符合語言變更提案的簡單標準:它應該直觀地向尚未被介紹到這一概念的人類讀者提供正確的含義。
一個有趣的現象是,大多數有經驗的Python程式設計師都認識到 while True
的習慣用法,也不太在意是否能在表示式構造中賦值; 只有新人表達了強烈的願望希望將其新增到語言中。
有一種替代的拼寫方式看起來很有吸引力,但通常不如"while True"解決方案可靠:
line = f.readline()
while line:
... # do something with line...
line = f.readline()
複製程式碼
問題在於,如果你改變主意(例如你想把它改成 sys.stdin.readline()
),如何知道下一行。你必須記住改變程式中的兩個地方 -- 第二次出現隱藏在迴圈的底部。
最好的方法是使用迭代器,這樣能通過 for
語句來迴圈遍歷物件。例如 file objects 支援迭代器協議,因此可以簡單地寫成:
for line in f:
... # do something with line...
複製程式碼
正如Guido所說:
(a) 對於某些操作,字首表示法比字尾更容易閱讀 -- 字首(和中綴!)運算在數學中有著悠久的傳統,就像在視覺上幫助數學家思考問題的記法。比較一下我們將 x(a+b) 這樣的公式改寫為 xa+x*b 的容易程度,以及使用原始OO符號做相同事情的笨拙程度。
(b) 當讀到寫有len(X)的程式碼時,就知道它要求的是某件東西的長度。這告訴我們兩件事:結果是一個整數,引數是某種容器。相反,當閱讀x.len()時,必須已經知道x是某種實現介面的容器,或者是從具有標準len()的類繼承的容器。當沒有實現對映的類有get()或key()方法,或者不是檔案的類有write()方法時,我們偶爾會感到困惑。
從Python 1.6開始,字串變得更像其他標準型別,當新增方法時,這些方法提供的功能與始終使用String模組的函式時提供的功能相同。這些新方法中的大多數已被廣泛接受,但似乎讓一些程式設計師感到不舒服的一種方法是:
", ".join(['1', '2', '4', '8', '16'])
複製程式碼
結果如下:
"1, 2, 4, 8, 16"
複製程式碼
反對這種用法有兩個常見的論點。
第一條是這樣的:“使用字串文字(String Constant)的方法看起來真的很難看”,答案是也許吧,但是字串文字只是一個固定值。如果在繫結到字串的名稱上允許使用這些方法,則沒有邏輯上的理由使其在文字上不可用。
第二個異議通常是這樣的:“我實際上是在告訴序列使用字串常量將其成員連線在一起”。遺憾的是並非如此。出於某種原因,把 split()
作為一個字串方法似乎要容易得多,因為在這種情況下,很容易看到:
"1, 2, 4, 8, 16".split(", ")
複製程式碼
是對字串文字的指令,用於返回由給定分隔符分隔的子字串(或在預設情況下,返回任意空格)。
join()
是字串方法,因為在使用該方法時,您告訴分隔符字串去迭代一個字串序列,並在相鄰元素之間插入自身。此方法的引數可以是任何遵循序列規則的物件,包括您自己定義的任何新的類。對於位元組和位元組陣列物件也有類似的方法。
如果沒有引發異常,則try/except塊的效率極高。實際上捕獲異常是昂貴的。在2.0之前的Python版本中,通常使用這個習慣用法:
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
複製程式碼
只有當你期望dict在任何時候都有key時,這才有意義。如果不是這樣的話,你就是應該這樣編碼:
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
複製程式碼
對於這種特定的情況,您還可以使用 value = dict.setdefault(key, getvalue(key))
,但前提是呼叫 getvalue()
足夠便宜,因為在所有情況下都會對其進行評估。
你可以通過一系列 if... elif... elif... else
.輕鬆完成這項工作。對於switch語句語法已經有了一些建議,但尚未就是否以及如何進行範圍測試達成共識。有關完整的詳細資訊和當前狀態,請參閱 PEP 275 。
對於需要從大量可能性中進行選擇的情況,可以建立一個字典,將case 值對映到要呼叫的函式。例如:
def function_1(...):
...
functions = {'a': function_1,
'b': function_2,
'c': self.method_1, ...}
func = functions[value]
func()
複製程式碼
對於物件呼叫方法,可以通過使用 getattr()
內建檢索具有特定名稱的方法來進一步簡化:
def visit_a(self, ...):
...
...
def dispatch(self, value):
method_name = 'visit_' + str(value)
method = getattr(self, method_name)
method()
複製程式碼
建議對方法名使用字首,例如本例中的 visit_
。如果沒有這樣的字首,如果值來自不受信任的源,攻擊者將能夠呼叫物件上的任何方法。
答案1: 不幸的是,直譯器為每個Python堆疊幀推送至少一個C堆疊幀。此外,擴充套件可以隨時回撥Python。因此,一個完整的執行緒實現需要對C的執行緒支援。
答案2: 幸運的是, Stackless Python 有一個完全重新設計的直譯器迴圈,可以避免C堆疊。
Python的 lambda表示式不能包含語句,因為Python的語法框架不能處理巢狀在表示式內部的語句。然而,在Python中,這並不是一個嚴重的問題。與其他語言中新增功能的lambda表單不同,Python的 lambdas只是一種速記符號,如果您懶得定義函式的話。
函式已經是Python中的第一類物件,可以在本地範圍內宣告。 因此,使用lambda而不是本地定義的函式的唯一優點是你不需要為函式建立一個名稱 -- 這只是一個分配了函式物件(與lambda表示式生成的物件型別完全相同)的區域性變數!
Cython 將帶有可選註釋的Python修改版本編譯到C擴充套件中。 Nuitka 是一個將Python編譯成 C++ 程式碼的新興編譯器,旨在支援完整的Python語言。要編譯成Java,可以考慮 VOC 。
Python 記憶體管理的細節取決於實現。 Python 的標準實現 CPython 使用引用計數來檢測不可訪問的物件,並使用另一種機制來收集引用迴圈,定期執行迴圈檢測演算法來查詢不可訪問的迴圈並刪除所涉及的物件。 gc
模組提供了執行垃圾回收、獲取除錯統計資訊和優化收集器引數的函式。
但是,其他實現(如 Jython 或 PyPy ),)可以依賴不同的機制,如完全的垃圾回收器 。如果你的Python程式碼依賴於引用計數實現的行為,則這種差異可能會導致一些微妙的移植問題。
在一些Python實現中,以下程式碼(在CPython中工作的很好)可能會耗盡檔案描述符:
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
複製程式碼
實際上,使用CPython的引用計數和解構函式方案, 每個新賦值的 f 都會關閉前一個檔案。然而,對於傳統的GC,這些檔案物件只能以不同的時間間隔(可能很長的時間間隔)被收集(和關閉)。
如果要編寫可用於任何python實現的程式碼,則應顯式關閉該檔案或使用 with
語句;無論記憶體管理方案如何,這都有效:
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
複製程式碼
首先,這不是C標準特性,因此不能移植。(是的,我們知道Boehm GC庫。它包含了 大多數 常見平臺(但不是所有平臺)的彙編程式碼,儘管它基本上是透明的,但也不是完全透明的; 要讓Python使用它,需要使用補丁。)
當Python嵌入到其他應用程式中時,傳統的GC也成為一個問題。在獨立的Python中,可以用GC庫提供的版本替換標準的malloc()和free(),嵌入Python的應用程式可能希望用 它自己 替代malloc()和free(),而可能不需要Python的。現在,CPython可以正確地實現malloc()和free()。
當Python退出時,從全域性名稱空間或Python模組引用的物件並不總是被釋放。 如果存在迴圈引用,則可能發生這種情況 C庫分配的某些記憶體也是不可能釋放的(例如像Purify這樣的工具會抱怨這些內容)。 但是,Python在退出時清理記憶體並嘗試銷燬每個物件。
如果要強制 Python 在釋放時刪除某些內容,請使用 atexit
模組執行一個函式,強制刪除這些內容。
雖然列表和元組在許多方面是相似的,但它們的使用方式通常是完全不同的。可以認為元組類似於Pascal記錄或C結構;它們是相關資料的小集合,可以是不同型別的資料,可以作為一個組進行操作。例如,笛卡爾座標適當地表示為兩個或三個數字的元組。
另一方面,列表更像其他語言中的陣列。它們傾向於持有不同數量的物件,所有物件都具有相同的型別,並且逐個操作。例如, os.listdir('.')
返回表示當前目錄中的檔案的字串列表。如果向目錄中新增了一兩個檔案,對此輸出進行操作的函式通常不會中斷。
元組是不可變的,這意味著一旦建立了元組,就不能用新值替換它的任何元素。列表是可變的,這意味著您始終可以更改列表的元素。只有不變元素可以用作字典的key,因此只能將元組和非列表用作key。
CPython的列表實際上是可變長度的陣列,而不是lisp風格的連結串列。該實現使用對其他物件的引用的連續陣列,並在列表頭結構中保留指向該陣列和陣列長度的指標。
這使得索引列表 a[i]
的操作成本與列表的大小或索引的值無關。
當新增或插入項時,將調整引用陣列的大小。並採用了一些巧妙的方法來提高重複新增項的效能; 當陣列必須增長時,會分配一些額外的空間,以便在接下來的幾次中不需要實際調整大小。
CPython的字典實現為可調整大小的雜湊表。與B-樹相比,這在大多數情況下為查詢(目前最常見的操作)提供了更好的效能,並且實現更簡單。
字典的工作方式是使用 hash()
內建函式計算字典中儲存的每個鍵的hash程式碼。hash程式碼根據鍵和每個程式的種子而變化很大;例如,"Python" 的hash值為-539294296,而"python"(一個按位不同的字串)的hash值為1142331976。然後,hash程式碼用於計算內部陣列中將儲存該值的位置。假設您儲存的鍵都具有不同的hash值,這意味著字典需要恆定的時間 -- O(1),用Big-O表示法 -- 來檢索一個鍵。
字典的雜湊表實現使用從鍵值計算的雜湊值來查詢鍵。如果鍵是可變物件,則其值可能會發生變化,因此其雜湊值也會發生變化。但是,由於無論誰更改鍵物件都無法判斷它是否被用作字典鍵值,因此無法在字典中修改條目。然後,當你嘗試在字典中查詢相同的物件時,將無法找到它,因為其雜湊值不同。如果你嘗試查詢舊值,也不會找到它,因為在該雜湊表中找到的物件的值會有所不同。
如果你想要一個用列表索引的字典,只需先將列表轉換為元組;用函式 tuple(L)
建立一個元組,其條目與列表 L
相同。 元組是不可變的,因此可以用作字典鍵。
已經提出的一些不可接受的解決方案:
- 雜湊按其地址(物件ID)列出。這不起作用,因為如果你構造一個具有相同值的新列表,它將無法找到;例如:
mydict = {[1, 2]: '12'}
print(mydict[[1, 2]])
複製程式碼
會引發一個 KeyError
異常,因為第二行中使用的 [1, 2]
的 id 與第一行中的 id 不同。換句話說,應該使用 ==
來比較字典鍵,而不是使用 is
。
- 使用列表作為鍵時進行復制。這沒有用的,因為作為可變物件的列表可以包含對自身的引用,然後複製程式碼將進入無限迴圈。
- 允許列表作為鍵,但告訴使用者不要修改它們。當你意外忘記或修改列表時,這將產生程式中的一類難以跟蹤的錯誤。它還使一個重要的字典不變數無效:
d.keys()
中的每個值都可用作字典的鍵。 - 將列表用作字典鍵後,應標記為其只讀。問題是,它不僅僅是可以改變其值的頂級物件;你可以使用包含列表作為鍵的元組。將任何內容作為鍵關聯到字典中都需要將從那裡可到達的所有物件標記為只讀 —— 並且自引用物件可能會導致無限迴圈。
如果需要,可以使用以下方法來解決這個問題,但使用它需要你自擔風險:你可以將一個可變結構包裝在一個類例項中,該例項同時具有 eq__
和 hash__
方法。然後,你必須確保駐留在字典(或其他基於 hash 的結構)中的所有此類包裝器物件的雜湊值在物件位於字典(或其他結構)中時保持固定。:
class ListWrapper:
def __init__(self, the_list):
self.the_list = the_list
def __eq__(self, other):
return self.the_list == other.the_list
def __hash__(self):
l = self.the_list
result = 98767 - len(l)*555
for i, el in enumerate(l):
try:
result = result + (hash(el) % 9999999) * 1001 + i
except Exception:
result = (result % 7777777) + i * 333
return result
複製程式碼
注意,雜湊計算由於列表的某些成員可能不可用以及算術溢位的可能性而變得複雜。
此外,必須始終如此,如果 o1 == o2
(即 o1.__eq__(o2) is True
)則 hash(o1) == hash(o2)
(即
o1.__hash__() == o2.__hash__()
),無論物件是否在字典中。 如果你不能滿足這些限制,字典和其他基於 hash 的結構將會出錯。
對於 ListWrapper ,只要包裝器物件在字典中,包裝列表就不能更改以避免異常。除非你準備好認真考慮需求以及不正確地滿足這些需求的後果,否則不要這樣做。請留意。
在效能很重要的情況下,僅僅為了排序而複製一份列表將是一種浪費。因此, list.sort()
對列表進行了適當的排序。為了提醒您這一事實,它不會返回已排序的列表。這樣,當您需要排序的副本,但也需要保留未排序的版本時,就不會意外地覆蓋列表。
如果要返回新列表,請使用內建 sorted()
函式。此函式從提供的可迭代列表中建立新列表,對其進行排序並返回。例如,下面是如何迭代遍歷字典並按keys排序:
for key in sorted(mydict):
... # do whatever with mydict[key]...
複製程式碼
由C++和Java等語言提供的模組介面規範描述了模組的方法和函式的原型。許多人認為介面規範的編譯時強制執行有助於構建大型程式。
Python 2.6新增了一個 abc
模組,允許定義抽象基類 (ABCs)。然後可以使用 isinstance()
和 issubclass()
來檢查例項或類是否實現了特定的ABC。 collections.abc
模組定義了一組有用的ABCs 例如 Iterable
, Container
, 和 MutableMapping
對於Python,通過對元件進行適當的測試規程,可以獲得介面規範的許多好處。還有一個工具PyChecker,可用於查詢由於子類化引起的問題。
一個好的模組測試套件既可以提供迴歸測試,也可以作為模組介面規範和一組示例。許多Python模組可以作為指令碼執行,以提供簡單的“自我測試”。即使是使用複雜外部介面的模組,也常常可以使用外部介面的簡單“樁程式碼(stub)”模擬進行隔離測試。可以使用 doctest
和 unittest
模組或第三方測試框架來構造詳盡的測試套件,以執行模組中的每一行程式碼。
適當的測試規程可以幫助在Python中構建大型的、複雜的應用程式以及介面規範。事實上,它可能會更好,因為介面規範不能測試程式的某些屬性。例如, append()
方法將向一些內部列表的末尾新增新元素;介面規範不能測試您的 append()
實現是否能夠正確執行此操作,但是在測試套件中檢查這個屬性是很簡單的。
編寫測試套件非常有用,您可能希望設計程式碼時著眼於使其易於測試。一種日益流行的技術是面向測試的開發,它要求在編寫任何實際程式碼之前,首先編寫測試套件的各個部分。當然,Python允許您草率行事,根本不編寫測試用例。
可以使用異常捕獲來提供 “goto結構” ,甚至可以跨函式呼叫工作的 。許多人認為異常捕獲可以方便地模擬C,Fortran和其他語言的 "go" 或 "goto" 結構的所有合理用法。例如:
class label(Exception): pass # declare a label
try:
...
if condition: raise label() # goto label
...
except label: # where to goto
pass
...
複製程式碼
但是不允許你跳到迴圈的中間,這通常被認為是濫用goto。謹慎使用。
更準確地說,它們不能以奇數個反斜槓結束:結尾處的不成對反斜槓會轉義結束引號字元,留下未結束的字串。
原始字串的設計是為了方便想要執行自己的反斜槓轉義處理的處理器(主要是正規表示式引擎)建立輸入。此類處理器將不匹配的尾隨反斜槓視為錯誤,因此原始字串不允許這樣做。反過來,允許通過使用引號字元轉義反斜槓轉義字串。當r-string用於它們的預期目的時,這些規則工作的很好。
如果您正在嘗試構建Windows路徑名,請注意所有Windows系統呼叫都使用正斜槓:
f = open("/mydir/file.txt") # works fine!
複製程式碼
如果您正在嘗試為DOS命令構建路徑名,請嘗試以下示例
dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"
複製程式碼
Python有一個 'with' 語句,它封裝了塊的執行,在塊的入口和出口呼叫程式碼。有些語言的結構是這樣的:
with obj:
a = 1 # equivalent to obj.a = 1
total = total + 1 # obj.total = obj.total + 1
複製程式碼
在Python中,這樣的結構是不明確的。
其他語言,如ObjectPascal、Delphi和C++ 使用靜態型別,因此可以毫不含糊地知道分配給什麼成員。這是靜態型別的要點 -- 編譯器 總是 在編譯時知道每個變數的作用域。
Python使用動態型別。事先不可能知道在執行時引用哪個屬性。可以動態地在物件中新增或刪除成員屬性。這使得無法通過簡單的閱讀就知道引用的是什麼屬性:區域性屬性、全域性屬性還是成員屬性?
例如,採用以下不完整的程式碼段:
def foo(a):
with a:
print(x)
複製程式碼
該程式碼段假設 "a" 必須有一個名為 "x" 的成員屬性。然而,Python中並沒有告訴直譯器這一點。假設 "a" 是整數,會發生什麼?如果有一個名為 "x" 的全域性變數,它是否會在with塊中使用?如您所見,Python的動態特性使得這樣的選擇更加困難。
然而,Python 可以通過賦值輕鬆實現 "with" 和類似語言特性(減少程式碼量)的主要好處。代替:
function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63
複製程式碼
寫成這樣:
ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63
複製程式碼
這也具有提高執行速度的副作用,因為Python在執行時解析名稱繫結,而第二個版本只需要執行一次解析。
冒號主要用於增強可讀性(ABC語言實驗的結果之一)。考慮一下這個:
if a == b
print(a)
複製程式碼
與
if a == b:
print(a)
複製程式碼
注意第二種方法稍微容易一些。請進一步注意,在這個FAQ解答的示例中,冒號是如何設定的;這是英語中的標準用法。
另一個次要原因是冒號使帶有語法突出顯示的編輯器更容易工作;他們可以尋找冒號來決定何時需要增加縮排,而不必對程式文字進行更精細的解析。
Python 允許您在列表,元組和字典的末尾新增一個尾隨逗號:
[1, 2, 3,]
('a', 'b', 'c',)
d = {
"A": [1, 5],
"B": [6, 7], # last trailing comma is optional but good style
}
複製程式碼
有幾個理由允許這樣做。
如果列表,元組或字典的字面值分佈在多行中,則更容易新增更多元素,因為不必記住在上一行中新增逗號。這些行也可以重新排序,而不會產生語法錯誤。
不小心省略逗號會導致難以診斷的錯誤。例如:
x = [
"fee",
"fie"
"foo",
"fum"
]
複製程式碼
這個列表看起來有四個元素,但實際上包含三個 : "fee", "fiefoo" 和 "fum" 。總是加上逗號可以避免這個錯誤的來源。
允許尾隨逗號也可以使程式設計程式碼更容易生成。
公眾號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。