python 基礎複習筆記
本文是作者結合廖雪峰網站python教程和深入學習python3教程的筆記,多為個人擷取比較關鍵的知識點。是基礎的基礎,不涉及IO、正則表示式、執行緒、XML等結合知識點。具體學習還請點選下面地址:
>>> from humansize importapproximate_size
>>> approximate_size(4000,a_kilobyte_is_1024_bytes=False) ①
'4.0 KB'
>>> approximate_size(size=4000,a_kilobyte_is_1024_bytes=False) ②
'4.0 KB'
>>>approximate_size(a_kilobyte_is_1024_bytes=False, size=4000) ③
'4.0 KB'
>>>approximate_size(a_kilobyte_is_1024_bytes=False, 4000) ④
File "<stdin>", line 1
SyntaxError: non-keyword arg after keywordarg
>>> approximate_size(size=4000,False) ⑤
File "<stdin>", line 1
SyntaxError: non-keyword arg after keywordarg
① 這個對 approximate_size() 函式的呼叫給第一個引數((size)指定了值 4000,並且給名為 a_kilobyte_is_1024_bytes 的引數指定了值 False。(那碰巧是第二個引數,但這沒有關係,馬上你就會了解到。)
② 這個對 approximate_size() 函式的呼叫給名為 size 引數指定了值 4000,併為名為 a_kilobyte_is_1024_bytes 的引數指定了值 False。(這些命名引數碰巧和函式宣告時列出的引數順序一樣,但同樣不要緊。)
③ 這個對 approximate_size() 函式的呼叫給名為a_kilobyte_is_1024_bytes 的引數指定了值 False,然後給名為 size 的引數指定了值 4000。(看到了沒?我告訴過你順序沒有關係。)
④ 這個呼叫會失敗,因為你在命名引數後面緊跟了一個非命名(位置的)的引數,這個一定不會工作。從左到右的讀取引數列表,一旦你有一個命名的引數,剩下的引數也必須是命名的。
⑤ 這個呼叫也會失敗,和前面一個呼叫同樣的原因。是不是很驚訝?別忘了,你給名為 size 的引數傳入了值 4000,那麼“顯然的” False 這個值意味著對應了 a_kilobyte_is_1024_bytes 引數。但是 Python 不按照這種方式工作。只要你有一個命名引數,它右邊的所有引數也都需要是命名引數。
很多 Python 的整合開發環境(ide)使用 docstring (文件字串)來提供上下文敏感的文件,以便於當你輸入一個函式名稱的時候,它的 docstring 會以一個提示文字的方式顯式出來。這可能會極其有用,但它只有在你寫出好的 docstring (文件字串)的時候才有用。
通過使用 sys.path.insert(0, new_path),你可以插入一個新的目錄到sys.path 列表的第一項,從而使其出現在 Python 搜尋路徑的開頭。這幾乎總是你想要的。萬一出現名字衝突(例如,Python 自帶了版本 2 的一個特定的庫,但是你想使用版本 3),這個方法就能確保你的模組能夠被發現和使用,替代 Python 自帶的版本。
你可能從其他程式語言環境中聽說過 “first-class object” 的說法。在 Python 中,函式是 first-class objects,你可以將一個函式作為一個引數傳遞給另外一個函式;模組是 first-class objects,你可以把整個模組作為一個引數傳遞給一個函式;類是 first-class objects,而且類的單獨的例項也是 first-classobjects。
這個很重要,因此剛開始我會重複幾次以防你忘記了:在 Python 裡面所有東西都是物件。字串是物件,列表是物件,函式是物件,類是物件,類的例項是物件,甚至模組也是物件。
if size < 0:
raise ValueError('number must be non-negative')
丟擲一個異常的語法足夠簡單。使用 raise 語句,緊跟著異常的名稱,和一個人們可以讀取的字串用來除錯。這個語法讓人想起呼叫的函式。(實際上,異常是用類來實現的,這個 raise 語句事實上正在建立一個 ValueError 類的例項並傳遞一個字串 'number must be non-negative' 到它的初始化方法裡面。
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
在這個 try..except 塊的結尾,你匯入了某個模組並取名為 etree。由於兩個模組實現了一個公共的 api,你剩下的程式碼不需要一直去檢查哪個模組被匯入了。而且由於這個一定會被匯入的模組總是叫做 etree,你餘下的程式碼就不會被呼叫不同名稱模組的 if 語句所打亂。
if __name__ == '__main__':
print(approximate_size(1000000000000, False))
print(approximate_size(1000000000000))
那麼是什麼使得這個 if 語句特別的呢? 好吧,模組是物件,並且所有模組都有一個內建的屬性 __name__。一個模組的 __name__ 屬性取決於你怎麼來使用這個模組。如果你 import 這個模組,那麼__name__ 就是這個模組的檔名,不包含目錄的路徑或者檔案的副檔名。
>>> import humansize
>>> humansize.__name__
'humansize'
但是你也可以當作一個獨立的程式直接執行這個模組,那樣的話 __name__ 將是一個特殊的預設值 __main__。 Python 將會評估這個 if 語句,尋找一個值為 true 的表示式,然後執行這個 if 程式碼塊。在這個例子中,列印兩個值。
c:\home\diveintopython3>c:\python31\python.exe humansize.py
1.0 TB
931.3 GiB
>>> float(2) ①
2.0
>>> int(2.0) ②
2
>>> int(2.5) ③
2
>>> int(-2.5) ④
-2
>>> 1.12345678901234567890 ⑤
1.1234567890123457
>>> type(1000000000000000) ⑥
<class 'int'>
① |
通過呼叫float() 函式,可以顯示地將 int 強制轉換為 float。 |
② |
毫不出奇,也可以通過呼叫 int() 將 float 強制轉換為 int 。 |
③ |
int() 將進行取整,而不是四捨五入。 |
④ |
對於負數,int() 函式朝著 0 的方法進行取整。它是個真正的取整(截斷)函式,而不是 floor[地板] 函式。 |
⑤ |
浮點數精確到小數點後 15 位。 |
⑥ |
整數可以任意大。 |
>>> −11 // 2 ③
−6
>>> 11.0 // 2 ④
5.0
③ 當整數除以負數, // 運算子將結果朝著最近的整數“向上”四捨五入。從數學角度來說,由於−6 比 −5 要小,它是“向下”四捨五入,如果期望將結果取整為 −5,它將會誤導你。
④ //運算子並非總是返回整數結果。如果分子或者分母是 float,它仍將朝著最近的整數進行四捨五入,但實際返回的值將會是 float 型別。
>>> a_list = ['a', 'b', 'c']
>>> a_list.extend(['d', 'e','f']) ①
>>> a_list
['a', 'b', 'c', 'd', 'e', 'f']
>>> len(a_list) ②
6
>>> a_list[-1]
'f'
>>> a_list.append(['g', 'h','i']) ③
>>> a_list
['a', 'b', 'c', 'd', 'e', 'f', ['g', 'h','i']]
>>> len(a_list) ④
7
>>> a_list[-1]
['g', 'h', 'i']
① extend()方法只接受一個引數,而該引數總是一個列表,並將列表 a_list 中所有的元素都新增到該列表中。
② 如果開始有個 3 元素列表,然後將它與另一個 3 元素列表進行 extend 操作,結果是將獲得一個 6 元素列表。
③ 另一方面, append() 方法只接受一個引數,但可以是任何資料型別。在此,對一個 3 元素列表呼叫 append() 方法。
④ 如果開始的時候有個 6 元素列表,然後將一個列表 append[新增]上去,結果就會……得到一個 7 元素列表。為什麼是 7個?因為最後一個元素(剛剛 append[新增] 的元素) 本身是個列表 。列表可包含任何型別的資料,包括其它列表。這可能是你所需要的結果,也許不是。但如果這就是你想要的,那這就是你所得到的。
① 如果不帶引數呼叫, pop() 列表方法將刪除列表中最後的元素,並返回所刪除的值。
② 可以從列表中 pop[彈出]任何元素。只需傳給 pop() 方法一個位置索引值。它將刪除該元素,將其後所有元素移位以“填補縫隙”,然後返回它刪除的值。
③ 對空列表呼叫 pop() 將會引發一個例外。
① 在布林型別上下文環境中,空列表為假值。
② 任何至少包含一個上元素的列表為真值。
③ 任何至少包含一個上元素的列表為真值。元素的值無關緊要。
那麼元組有什麼好處呢?
元組的速度比列表更快。如果定義了一系列常量值,而所需做的僅是對它進行遍歷,那麼請使用元組替代列表。
對不需要改變的資料進行“防寫”將使得程式碼更加安全。使用元組替代列表就像是有一條隱含的assert 語句顯示該資料是常量,特別的想法(及特別的功能)必須重寫。(??)
一些元組可用作字典鍵(特別是包含字串、數值和其它元組這樣的不可變資料的元組)。列表永遠不能當做字典鍵使用,因為列表不是不可變的。
為建立單元素元組,需要在值之後加上一個逗號。沒有逗號,Python 會假定這只是一對額外的圓括號,雖然沒有害處,但並不建立元組。
由於從 Python 2 沿襲而來歷史的古怪規定,不能使用兩個花括號來建立空集合。該操作實際建立一個空字典,而不是一個空集合。
>>> a_set = {1, 2, 3}
>>> a_set
{1, 2, 3}
>>> a_set.update({2, 4, 6}) ①
>>> a_set ②
{1, 2, 3, 4, 6}
>>> a_set.update({3, 6, 9}, {1, 2,3, 5, 8, 13}) ③
>>> a_set
{1, 2, 3, 4, 5, 6, 8, 9, 13}
>>> a_set.update([10, 20,30]) ④
>>> a_set
{1, 2, 3, 4, 5, 6, 8, 9, 10, 13, 20, 30}
① update()方法僅接受一個集合作為引數,並將其所有成員新增到初始列表中。其行為方式就像是對引數集合中的每個成員呼叫add() 方法。
② 由於集合不能包含重複的值,因此重複的值將會被忽略。
③ 實際上,可以帶任何數量的引數呼叫 update() 方法。如果呼叫時傳遞了兩個集合, update() 將會被每個集合中的每個成員新增到初始的集合當中(丟棄重複值)。
④ update()方法還可接受一些其它資料型別的物件作為引數,包括列表。如果呼叫時傳入列表,update() 將會把列表中所有的元素新增到初始集合中。
① discard()接受一個單值作為引數,並從集合中刪除該值。
② 如果針對一個集合中不存在的值呼叫 discard() 方法,它不進行任何操作。不產生錯誤;只是一條空指令。
③ remove()方法也接受一個單值作為引數,也從集合中將其刪除。
④ 區別在這裡:如果該值不在集合中,remove() 方法引發一個 KeyError 例外。
① pop()方法從集合中刪除某個值,並返回該值。然而,由於集合是無序的,並沒有“最後一個”值的概念,因此無法控制刪除的是哪一個值。它基本上是隨機的。
② clear()方法刪除集合中 所有 的值,留下一個空集合。它等價於 a_set = set(),該語句建立一個新的空集合,並用之覆蓋 a_set 變數的之前的值。
>>> a_set = {2, 4, 5, 9, 12, 21,30, 51, 76, 127, 195}
>>> 30 in a_set ①
True
>>> 31 in a_set
False
>>> b_set = {1, 2, 3, 5, 6, 8, 9,12, 15, 17, 18, 21}
>>> a_set.union(b_set) ②
{1, 2, 195, 4, 5, 6, 8, 12, 76, 15, 17, 18,3, 21, 30, 51, 9, 127}
>>> a_set.intersection(b_set) ③
{9, 2, 12, 5, 21}
>>> a_set.difference(b_set) ④
{195, 4, 76, 51, 30, 127}
>>>a_set.symmetric_difference(b_set) ⑤
{1, 3, 4, 6, 8, 76, 15, 17, 18, 195, 127,30, 51}
① 要檢測某值是否是集合的成員,可使用 in 運算子。其工作原理和列表的一樣。
② union()方法返回一個新集合,其中裝著 在兩個 集合中出現的元素。
③ intersection()方法返回一個新集合,其中裝著 同時 在兩個集合中出現的所有元素。
④ difference()方法返回的新集合中,裝著所有在 a_set 出現但未在b_set 中的元素。
⑤ symmetric_difference()方法返回一個新集合,其中裝著所有 只在其中一個 集合中出現的元素。
至少包含一個鍵值對的字典為真值。
None 是 Python 的一個特殊常量。它是一個空 值。None 與 False 不同。None 不是 0 。None 不是空字串。將 None 與任何非 None 的東西進行比較將總是返回 False 。
None 是唯一的空值。它有著自己的資料型別(NoneType)。可將 None 賦值給任何變數,但不能建立其它 NoneType 物件。所有值為 None 變數是相等的。
你可以在列表解析中使用任何的Python表示式, 包括os 模組中用於操作檔案和目錄的函式。
>>> a_dict = {'a': 1, 'b': 2, 'c':3}
>>> {value:key for key, value ina_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}
位元組即位元組,並非字元。字元在計算機內只是一種抽象。字串則是一種抽象的序列。
>>> import humansize
>>> si_suffixes =humansize.SUFFIXES[1000] ①
>>> si_suffixes
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB','YB']
>>> '1000{0[0]} =1{0[1]}'.format(si_suffixes) ②
'1000KB = 1MB'
一個不可變(immutable)的Unicode編碼的字元序列叫做string。一串由0到255之間的數字組成的序列叫做bytes物件。
bytes物件是不可變的;我們不可以給單個位元組賦上新值。如果需要改變某個位元組,可以組合使用字串的切片和連線操作(效果跟字串是一樣的),或者我們也可以將bytes物件轉換為bytearray物件。
>>> by = b'abcd\x65'
>>> barr = bytearray(by) ①
>>> barr
bytearray(b'abcde')
>>> len(barr) ②
5
>>> barr[0] = 102 ③
>>> barr
bytearray(b'fbcde')
① 使用內建函式bytearray()來完成從bytes物件到可變的bytearray物件的轉換。
② 所有對bytes物件的操作也可以用在bytearray物件上。
③ 有一點不同的就是,我們可以使用下標標記給bytearray物件的某個位元組賦值。並且,這個值必須是0–255之間的一個整數。
s.count(by.decode('ascii'))
bytes物件有一個decode()方法,它使用某種字元編碼作為引數,然後依照這種編碼方式將bytes物件轉換為字串,對應地,字串有一個encode()方法,它也使用某種字元編碼作為引數,然後依照它將串轉換為bytes物件。
def fib(max):
a, b = 0, 1 ①
while a < max:
yield a ②
a, b = b, a + b ③
>>> assert 1 + 1 == 2 ①
>>> assert 1 + 1 == 3 ②
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 2 + 2 == 5, "Onlyfor very large values of 2" ③
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: Only for very large valuesof 2
① assert語句後面跟任何合法的Python 表示式。在這個例子裡, 表示式 1 + 1 == 2 的求值結果為 True, 所以 assert 語句沒有做任何事情。
② 然而, 如果Python 表示式求值結果為False, assert 語句會丟擲一個 AssertionError.
③ 你可以提供一個人類可讀的訊息,AssertionError異常被丟擲的時候它可以被用於列印輸出。
因此, 這行程式碼:
assert len(unique_characters) <= 10,'Too many letters'
…等價於:
if len(unique_characters) > 10:
raiseAssertionError('Too many letters')
>>>unique_characters = {'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
>>> gen= (ord(c) for c in unique_characters) ①
>>>gen ②
<generatorobject <genexpr> at 0x00BADC10>
>>>next(gen) ③
69
>>>next(gen)
68
>>>tuple(ord(c) for c in unique_characters) ④
(69, 68, 77, 79,78, 83, 82, 89)
① 生成器表示式類似一個yield值的匿名函式。表示式本身看起來像列表解析, 但不是用方括號而是用圓括號包圍起來。
② 生成器表示式返回迭代器。
③ 呼叫 next(gen) 返回迭代器的下一個值。
④ 如果你願意,你可以將生成器表示式傳給tuple(),list(), 或者 set()來迭代所有的值並且返回元組,列表或者集合。在這種情況下,你不需要一對額外的括號 — 將生成器表示式ord(c) for c in unique_characters 傳給 tuple() 函式就可以了, Python 會推斷出它是一個生成器表示式。
(x+1 for x inlst) #生成器表示式,返回迭代器。外部的括號可在用於引數時省略。
[x+1 for x inlst] #列表解析,返回list
itertools.product()函式返回包含兩個序列的笛卡爾乘積的迭代器。
eval() 並不限於布林表示式。它能處理任何 Python 表示式並且返回任何資料型別。
字母謎題:
import re
import itertools
def solve(puzzle):
words = re.findall('[A-Z]+', puzzle.upper())
unique_characters =
set(''.join(words))
assert len(unique_characters)
<= 10,
'Too many letters'
first_letters =
{word[0]
for word
in words}
n = len(first_letters)
sorted_characters =
''.join(first_letters)
+ \
''.join(unique_characters
- first_letters)
characters = tuple(ord(c)
for c in sorted_characters)
digits = tuple(ord(c)
for c in
'0123456789')
zero = digits[0]
for guess
in itertools.permutations(digits, len(characters)):
if zero
not in guess[:n]:
equation = puzzle.translate(dict(zip(characters,
guess)))
if
eval(equation):
return equation
if __name__
== '__main__':
import sys
for puzzle
in sys.argv[1:]:
print(puzzle)
solution = solve(puzzle)
if solution:
print(solution)
單元測試事實上有 三種 返回值:通過、失敗以及錯誤。“通過”,但當然就是說測試成功了──被測程式碼符合你的預期。“失敗”就是就如之前的測試用例一樣(直到你編寫程式碼令它通過)──執行了被測試的程式碼但返回值並不是所期望的。“錯誤”就是被測試的程式碼甚至沒有正確執行。
再次讀取檔案不會產生一個異常。Python不認為到達了檔案末尾(end-of-file)還繼續執行讀取操作是一個錯誤;這種情況下,它只是簡單地返回一個空字串。
seek()和tell()方法總是以位元組的方式計數,但是,由於你是以文字檔案的方式開啟的,read()方法以字元的個數計數。中文字元的UTF-8編碼需要多個位元組。而檔案裡的英文字元每一個只需要一個位元組來儲存,所以你可能會產生這樣的誤解:seek()和read()方法對相同的目標計數。而實際上,只有對部分字元的情況是這樣的。
withopen('examples/chinese.txt', encoding='utf-8') as a_file:
a_file.seek(17)
a_character = a_file.read(1)
print(a_character)
這段程式碼呼叫了open()函式,但是它卻一直沒有呼叫a_file.close()。with語句引出一個程式碼塊,就像if語句或者for迴圈一樣。在這個程式碼塊裡,你可以使用變數a_file作為open()函式返回的流物件的引用。所以流物件的常規方法都是可用的 — seek(),read(),無論你想要呼叫什麼。當with塊結束時,Python自動呼叫a_file.close()。
這就是它與眾不同的地方:無論你以何種方式跳出with塊,Python會自動關閉那個檔案…即使是因為未處理的異常而“exit”。是的,即使程式碼中引發了一個異常,整個程式突然中止了,Python也能夠保證那個檔案能被關閉掉。
with語句不只是針對檔案而言的;它是一個用來建立執行時環境的通用框架(generic framework),告訴物件它們正在進入和離開一個執行時環境。如果該物件是流物件,那麼它就會做一些類似檔案物件一樣有用的動作(就像自動關閉檔案!)。但是那個行為是被流物件自身定義的,而不是在with語句中。
只要物件包含read()方法,這個方法使用一個可選引數size並且返回值為一個串,它就是是流物件。不使用size引數呼叫read()的時候,這個方法應該從輸入源讀取所有可讀的資訊然後以單獨的一個值返回所有資料。當使用size引數呼叫read()時,它從輸入源讀取並返回指定量的資料。當再一次被呼叫時,它從上一次離開的地方開始讀取並返回下一個資料塊。
定義函式時,需要確定函式名和引數個數;
如果有必要,可以先對引數的資料型別做檢查;
函式體內部可以用return隨時返回函式結果;
函式執行完畢也沒有return語句時,自動return None。
函式可以同時返回多個值,但其實就是一個tuple。
預設引數必須指向不變物件(L[])
預設引數必須指向不變物件. 命名關鍵字引數必須傳入引數名,這和位置引數不同。如果沒有傳入引數名,呼叫將報錯
在Python中定義函式,可以用必選引數、預設引數、可變引數、關鍵字引數和命名關鍵字引數,這5種引數都可以組合使用,除了可變引數無法和命名關鍵字引數混合。但是請注意,引數定義的順序必須是:必選引數、預設引數、可變引數/命名關鍵字引數和關鍵字引數。
對於任意函式,都可以通過類似func(*args,**kw)的形式呼叫它,無論它的引數是如何定義的
Python的函式具有非常靈活的引數形態,既可以實現簡單的呼叫,又可以傳入非常複雜的引數。
預設引數一定要用不可變物件,如果是可變物件,程式執行時會有邏輯錯誤!
要注意定義可變引數和關鍵字引數的語法:
*args是可變引數,args接收的是一個tuple;
**kw是關鍵字引數,kw接收的是一個dict。
以及呼叫函式時如何傳入可變引數和關鍵字引數的語法:
可變引數既可以直接傳入:func(1,2, 3),又可以先組裝list或tuple,再通過*args傳入:func(*(1, 2, 3));
關鍵字引數既可以直接傳入:func(a=1,b=2),又可以先組裝dict,再通過**kw傳入:func(**{'a': 1, 'b': 2})。
使用*args和**kw是Python的習慣寫法,當然也可以用其他引數名,但最好使用習慣用法。
命名的關鍵字引數是為了限制呼叫者可以傳入的引數名,同時可以提供預設值。
定義命名的關鍵字引數不要忘了寫分隔符*,否則定義的將是位置引數。
函式是順序執行,遇到return語句或者最後一行函式語句就返回。而變成generator的函式,在每次呼叫next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。
def triangles():
N=[1]
while True:
yield N
N.append(0)
N=[N[i-1]+N[i] for i in range(len(N))]
既然變數可以指向函式,函式的引數能接收變數,那麼一個函式就可以接收另一個函式作為引數,這種函式就稱之為高階函式。
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 =count()
返回閉包時牢記的一點就是:返回函式不要引用任何迴圈變數,或者後續會發生變化的變數。方法是再建立一個函式,用該函式的引數繫結迴圈變數當前的值,無論該迴圈變數後續如何更改,已繫結到函式引數的值不變.
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
return fs
關鍵字lambda表示匿名函式,冒號前面的x表示函式引數。
匿名函式有個限制,就是隻能有一個表示式,不用寫return,返回值就是該表示式的結果。
用匿名函式有個好處,因為函式沒有名字,不必擔心函式名衝突。此外,匿名函式也是一個函式物件,也可以把匿名函式賦值給一個變數,再利用變數來呼叫該函式
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
簡單總結functools.partial的作用就是,把一個函式的某些引數給固定住(也就是設定預設值),返回一個新的函式,呼叫這個新函式會更簡單。
注意到上面的新的int2函式,僅僅是把base引數重新設定預設值為2,但也可以在函式呼叫時傳入其他值
建立偏函式時,實際上可以接收函式物件、*args和**kw這3個引數
,每一個包目錄下面都會有一個__init__.py的檔案,這個檔案是必須存在的,否則,Python就把這個目錄當成普通目錄,而不是一個包。__init__.py可以是空檔案,也可以有Python程式碼,因為__init__.py本身就是一個模組,而它的模組名就是mycompany
任何模組程式碼的第一個字串都被視為模組的文件註釋
private函式和變數“不應該”被直接引用,而不是“不能”被直接引用,是因為Python並沒有一種方法可以完全限制訪問private函式或變數,但是,從程式設計習慣上不應該引用private函式或變數。
注意到__init__方法(別的類的方法也是)的第一個引數永遠是self(這是強制習慣),表示建立的例項本身,因此,在__init__方法內部,就可以把各種屬性繫結到self,因為self就指向建立的例項本身。
不能直接訪問__name是因為Python直譯器對外把__name變數改成了_Student__name,所以,仍然可以通過_Student__name來訪問__name變數但是強烈建議你不要這麼幹,因為不同版本的Python直譯器可能會把__name改成不同的變數名。總的來說就是,Python本身沒有任何機制阻止你幹壞事,一切全靠自覺。
靜態語言 vs 動態語言
對於靜態語言(例如Java)來說,如果需要傳入Animal型別,則傳入的物件必須是Animal型別或者它的子類,否則,將無法呼叫run()方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal型別。我們只需要保證傳入的物件有一個run()方法就可以了。這就是動態語言的“鴨子型別”,它並不要求嚴格的繼承體系,一個物件只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
class A(object):
def run(self):
print(A.__name__)
class B(A):
def run(self):
print(B.__name__)
class C(A):
def run(self):
print(C.__name__)
class D(object):
def run(self):
pass
def runs(A):
A.run()
x = D()
runs(x)
使用__slots__要注意,__slots__定義的屬性僅對當前類例項起作用,對繼承的子類是不起作用的,子類例項允許定義的屬性就是自身的__slots__加上父類的__slots__
MixIn的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn的功能,而不是設計多層次的複雜的繼承關係。
>>> classStudent(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name:%s)' % self.name
...
>>> print(Student('Michael'))
Student object(name: Michael)
這樣打印出來的例項,不但好看,而且容易看出例項內部重要的資料。
但是細心的朋友會發現直接敲變數不用print,打印出來的例項還是不好看:
>>> s =Student('Michael')
>>> s
<__main__.Studentobject at 0x109afb310>
這是因為直接顯示變數呼叫的不是__str__(),而是__repr__(),兩者的區別是__str__()返回使用者看到的字串,而__repr__()返回程式開發者看到的字串,也就是說,__repr__()是為除錯服務的。
只有在沒有找到屬性的情況下,才呼叫__getattr__,已有的屬性,比如name,不會在__getattr__中查詢
如果你把物件看成函式,那麼函式本身其實也可以在執行期動態創建出來,因為類的例項都是執行期創建出來的,這麼一來,我們就模糊了物件和函式的界限
動態語言和靜態語言最大的不同,就是函式和類的定義,不是編譯時定義的,而是執行時動態建立的。
def fn(self,name='world'): # 先定義函式
... print('Hello, %s.' % name)
...
>>>Hello = type('Hello', (object,), dict(hello=fn)) # 建立Helloclass
要建立一個class物件,type()函式依次傳入3個引數:
- class的名稱;
- 繼承的父類集合,注意Python支援多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
- class的方法名稱與函式繫結,這裡我們把函式fn繫結到方法名hello上。
通過type()函式建立的類和直接寫class是完全一樣的,因為Python直譯器遇到class定義時,僅僅是掃描一下class定義的語法,然後呼叫type()函式創建出class。
metaclass,直譯為元類,簡單的解釋就是:
當我們定義了類以後,就可以根據這個類創建出例項,所以:先定義類,然後建立例項。
但是如果我們想創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然後建立類。
連線起來就是:先定義metaclass,就可以建立類,最後建立例項。
所以,metaclass允許你建立類或者修改類。換句話說,你可以把類看成是metaclass創建出來的“例項”。
由於沒有錯誤發生,所以except語句塊不會被執行,但是finally如果有,則一定會被執行(可以沒有finally語句)Python的錯誤其實也是class,所有的錯誤型別都繼承自BaseException,所以在使用except時需要注意的是,它不但捕獲該型別的錯誤,還把其子類也“一網打盡”
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')
第二個except永遠也捕獲不到UnicodeError,因為UnicodeError是ValueError的子類,如果有,也被第一個except給捕獲了。
Python內建的logging模組可以非常容易地記錄錯誤資訊
如果要丟擲錯誤,首先根據需要,可以定義一個錯誤的class,選擇好繼承關係,然後,用raise語句丟擲一個錯誤的例項:
#err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value:%s' % s)
return 10 / n
foo('0')
斷言
凡是用print()來輔助檢視的地方,都可以用斷言(assert)來替代:
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
assert的意思是,表示式n != 0應該是True,否則,根據程式執行的邏輯,後面的程式碼肯定會出錯。
如果斷言失敗,assert語句本身就會丟擲AssertionError:
$ python3 err.py
Traceback (mostrecent call last):
...
AssertionError:n is zero!
logging
把print()替換為logging是第3種方式,和assert比,logging不會丟擲錯誤,而且可以輸出到檔案:
import logging
s = '0'
n = int(s)
logging.info('n= %d' % n)
print(10 / n)
logging.info()就可以輸出一段文字。
編寫單元測試時,我們需要編寫一個測試類,從unittest.TestCase繼承。
以test開頭的方法就是測試方法,不以test開頭的方法不被認為是測試方法,測試的時候不會被執行。
對每一類測試都需要編寫一個test_xxx()方法。由於unittest.TestCase提供了很多內建的條件判斷,我們只需要呼叫這些方法就可以斷言輸出是否是我們所期望的。最常用的斷言就是assertEqual()。可以在單元測試中編寫兩個特殊的setUp()和tearDown()方法。這兩個方法會分別在每呼叫一個測試方法的前後分別被執行。
import math
def abbs(n):
'''
ABS
Example:
>>> abbs(1)
1
>>> abbs(-1)
1
>>> abs(0)
0
'''
return abs(n)
if __name__ == '__main__':
import doctest
doctest.testmod()
注:作者初學,水平有限,若有不妥之處,請多多指教。