Python 是一門很是容易上手的語言,經過查閱資料和教程,也許一夜就能寫出一個簡單的爬蟲。但 Python 也是一門很難精通的語言,由於簡潔的語法背後隱藏了許多黑科技。本文主要針對的讀者是:python
固然, 用一篇文章來說完某個語言是不可能的事情,我但願讀完本文的讀者能夠:swift
若是以上介紹符合你對本身的定位,在開始閱讀前,還須要明確幾點:數組
請不要在學習 Python2 仍是 Python3 之間猶豫了,除非你很明確本身只接觸 Python2,不然就從 Python3 學起,新版本的語言老是意味着進步的生產力(Swift 和 Xcode 除外)。Python 2 和 3 之間語法不兼容,但這並不影響熟悉 Python3 的開發者迅速寫出 Python 2 的代碼,反之亦然。因此與其在反覆糾結中浪費時間,不如馬上行動起來。數據結構
推薦使用 CodeRunner 來運行本文中的 demo,它比文本編輯器功能更強大,好比支持自動補全和斷點調試,又比 PyCharm 輕量得多。app
若是要對數組中的全部內容作一些修改,能夠用 for 循環或者 map 函數:編輯器
array = [1, 2, 3, 4, 5, 6] small = [] for n in array: if n < 4: small.append(n * 2) print(small) # [2, 4, 6]
比較地道的 Python 寫法是使用列表推導:ide
array = [1, 2, 3, 4, 5, 6] small = [n * 2 for n in array if n < 4]
for in
能夠寫兩次,相似於嵌套的 for 循環,會獲得一個笛卡爾積:函數
signs = ['+', '-'] numbers = [1, 2] ascii = ['{sign}{number}'.format(sign=sign, number=number) for sign in signs for number in numbers] # 獲得:['+1', '+2', '-1', '-2']
元組能夠簡單的理解爲不可變的數組,也就是沒有 append
、del
等方法,一旦建立,就沒法新增或刪除元素,元素自身的值也不能改變,但元素內部的屬性是否可變並不受元組的影響,這一點符合其餘語言中的常識。工具
t = (1, []) t[0] = 3 # 拋出錯誤 TypeError: 'tuple' object does not support item assignment t[1].append(2) # 正常運行,如今的 t 是 (1, [2])
除了不可變性之外,有時候元組也會被當作不具名的數據結構,這時候元素的位置就再也不是無關緊要的了: coordinate = (33.9425, -118.408056) # coordinate 的第一個位置用來表示精度,第二個位置表示維度 在解析元組數據時,能夠一一對應的寫上變量名: t = (1, 2) a, b = t # a = 1, b = 2 有時候變量名比較長, 但我只關心其中某一個,能夠這樣寫: t = (1, 2) a, _ = t # a = 1 若是元組中元素特別多,即便挨個寫下劃線也比較累,能夠用 * 來批量解包: t = (1, 2, 3, 4, 5) first, *middle, last = t # first = 1 # middle = [2, 3, 4] # last = 5 固然,若是元素數量較多,含義較複雜,我仍是建議使用具名元組: import collections People = collections.namedtuple('People', ['name', 'age']) p = People('bestswifter', '22') p.name # 22 具名元組更像是一個不能定義方法的簡化版的類,能提供友好的數據展現。 元組的一個小技巧是能夠避免用臨時變量來交換兩個數的值: a = 1 b = 2 a, b = b, a # a = 2, b = 1
切片的基本格式是 array[start:end:step]
,表示對 array 在 start 到 end 以前以 step 爲間隔取切片。注意這裏的區間是 [start, end),也就是左閉右開。好比:性能
s = 'hello' s[0:5:2] # 表示取 s 的第 0、2、4 個字符,結果是 'hlo'
再舉幾個例子
s[0:5] # 不寫 step 默認就是 1,所以獲得 'hello' s[1:] # 不寫 end 默認到結尾,所以仍是獲得 'ello' s[n:] # 獲取 s 的最後 len(s) - n 個元素 s[:2] # 不寫 start 默認從 0 開始,所以獲得 'he' s[:n] # 獲取 s 的前 n 個元素 s[:-1] # 負數表示倒過來數,所以這會刨除最後一個字符,獲得 'hell' s[-2:] # 同上,表示獲取最後兩個字符,獲得 'lo' s[::-1] # 獲取字符串的倒序排列,至關於 reverse 函數
step 和它前面的冒號要麼同時寫,要麼同時不寫,但 start 和 end 之間的冒號不能省,不然就不是切片而是獲取元素了。再次強調 array[start:end]
表示的區間是 [a, b),也許你會以爲這很難記,但一樣的,這會得出如下美妙的公式:
array[:n] + array[n:] = array (0 <= n <= len(array))
用代碼來表示就是:
s = 'hello' s[:2] + s[2:] == s # True,由於 s[:2] 是 'he',s[2:] 是 'llo'
切片不只能夠用來獲取數組的一部分值,修改切片也能夠直接修改數組的對應部分,好比:
a = [1, 2, 3, 4, 5, 6] a[1:3] = [22, 33, 44] # a = [1, 22, 33, 44, 4, 5, 6]
並無人規定切片的新值必須和原來的長度一致:
a = [1, 2, 3, 4, 5, 6] a[1:3] = [3] # a = [1, 3, 4, 5, 6] a[1:4] = [] # a = [1, 6],至關於刪除了中間的三個數字
但切片的新值必須也是可迭代的對象,好比這樣寫是不合法的:
a = [1, 2, 3, 4, 5, 6] a[1:3] = 3 # TypeError: can only assign an iterable
1.1.4 循環與遍歷
通常來講,在 Python 中咱們不會寫出 for (int i = 0; i < len(array); ++i)
這種風格的代碼,而是使用 for in
這種語法:
for i in [1, 2, 3]: print(i)
雖然你們都知道 for in
語法,但它的某些靈活用法或許就不是那麼衆所周知了。
有時候,咱們會在 if 語句中對某個變量的值作屢次判斷,只要知足一個條件便可: name = 'bs' if name == 'hello' or name == 'hi' or name == 'bs' or name == 'admin': print('Valid') 這種狀況推薦用 in 來代替: name = 'bs' if name in ('hello', 'hi', 'bs', 'admin'): print('Valid') 有時候,若是咱們想要把某件事重複固定的次數,用 for in 會顯得有些囉嗦,這時候能夠藉助 range 類型: for i in range(5): print('Hi') # 打印五次 'Hi' range 的語法和切片相似,好比咱們須要訪問數組全部奇數下標的元素,能夠這麼寫: a = [1, 2, 3, 4, 5] for i in range(0, len(a), 2): print(a[i]) 在這種寫法中,咱們不只能得到元素,還能知道元素的下標,這與使用 enumerate(iterable [, start ]) 函數相似: a = [1, 2, 3, 4, 5] for i, n in enumerate(a): print(i, n)
也許你已經注意到了,數組和字符串都支持切片,並且語法高度統一。這在某些強類型語言(好比我常常接觸的 Objective-C 和 Java)中是不可能的,事實上,Python 可以支持這樣統一的語法,並不是巧合,而是由於全部用中括號進行下標訪問的操做,其實都是調用這個類的 __getitem__
方法。
好比咱們徹底可讓本身的類也支持經過下標訪問:
class Book: def __init__(self): self.chapters = [1, 2, 3] def __getitem__(self, n): return self.chapters[n] b = Book() print(b[1]) # 結果是 2
須要注意的是,這段代碼幾乎不會出問題(除非數組越界),這是由於咱們直接把下標傳到了內部的 self.chapters
數組上。但若是要本身處理下標,須要牢記它不必定是數字,也能夠是切片,所以更完整的邏輯應該是:
def __getitem__(self, n): if isinstance(n, int): # n是索引 # 處理索引 if isinstance(n, slice): # n是切片 # 經過 n.start,n.stop 和 n.step 來處理切片
與靜態語言不一樣的是,任何實現了 __getitem__
都支持經過下標訪問,而不用聲明爲實現了某個協議,這種特性也被稱爲 「鴨子類型」。鴨子類型並不要求某個類 是什麼,僅僅要求這個類 能作什麼。
順便說一句,實現了 __getitem__
方法的類都是可迭代的,好比:
b = Book() for c in b: print(c)
後續的章節還會介紹更多 Python 中的魔術方法,這種方法的名稱先後都有兩個下劃線,若是讀做 「下劃線-下劃線-getitem」 會比較拗口,所以能夠讀做 「dunder-getitem」 或者 「雙下-getitem」,相似的,我想每一個人都能猜到 __setitem__
的做用和用法。
最簡單的建立一個字典的方式就是直接寫字面量: {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} 字典字面量由大括號包住(注意區別於數組的中括號),鍵值對之間由逗號分割,每一個鍵值對內部用冒號分割鍵和值。 若是數組的每一個元素都是二元的元組,這個數組能夠直接轉成字典: dict([('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)]) 就像數組能夠推導同樣,字典也能夠推導: a = [('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)] d = {letter: number for letter, number in a} # 這裏用到了元組拆包 只要記得外面仍是大括號就好了。 兩個獨立的數組能夠被壓縮成一個字典: numbers = [61, 62, 63, 64, 65] letters = ['a', 'b', 'c', 'd', 'e'] dict(zip(letters, numbers)) 正如 zip 的意思所表示的,超出長處的那部分數組會被拋棄。 1.2.2 查詢字典 最簡單方法是直接寫鍵名,但若是鍵名不存在會拋出 KeyError: d = {'a': 61} d['a'] # 值是 61 d['b'] # KeyError: 'b' 能夠用 if key in dict 的判斷來檢查鍵是否存在,甚至能夠先 try 再 catch KeyError,但更加優雅簡潔一些的寫法是用
get(k, default) 方法來提供默認值: d = {'a': 61} d.get('a', 62) # 獲得 61 d.get('b', 62) # 獲得 62 不過有時候,咱們可能不只僅要讀出默認屬性,更但願能把這個默認屬性能寫入到字典中,好比: d = {} # 咱們想對字典中某個 Value 作操做,若是 Key 不存在,就先寫入一個空值 if 'list' not in d: d['list'] = [] d['list'].append(1) 這種狀況下,seddefault(key, default) 函數或許更合適: d.setdefault('key', []).append(1) 這個函數雖然名爲 set,但做用實際上是查找,僅僅在查找不到時纔會把默認值寫入字典。
1.2.3 遍歷字典
直接遍歷字典其實是遍歷了字典的鍵,所以也能夠經過鍵獲取值: d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} for i in d: print(i, d[i]) #b 62 #a 61 #e 65 #d 64 #c 63 咱們也能夠用字典的 keys() 或者 values() 方法顯式的獲取鍵和值。字典還有一個 items() 方法,它返回一個數組,
每一個元素都是由鍵和值組成的二元元組: d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} for (k, v) in d.items(): print(k, v) #e 65 #d 64 #a 61 #c 63 #b 62
可見 items()
方法和字典的構造方法互爲逆操做,由於這個公式老是成立的:
dict(d.items()) == d
在 1.1.4 節中介紹過,經過下標訪問最終都會由 __getitem__
這個魔術方法處理,所以字典的 d[key]
這種寫法也不例外, 若是鍵不存在,則會走到 __missing__
方法,再給一次挽救的機會。好比咱們能夠實現一個字典, 自動忽略鍵的大小寫:
class MyDict(dict): def __missing__(self, key): if key.islower(): raise KeyError(key) else: return self[key.lower()] d = MyDict({'a': 61}) d['A'] # 返回 61 'A' in d # False
這個字典比較簡陋,好比 key 可能不是字符串,不過我沒有處理太多狀況,由於它主要是用來演示 __missing__
的用法,若是想要最後一行的 in
語法正確工做,須要重寫 __contains__
這個魔術方法,過程相似,就不贅述了。
雖然經過自定義的函數也能實現類似的效果,不過這個自定義字典對用戶更加透明,若是不在文檔中說明,調用方很難察覺到字典的內部邏輯被修改了。 Python 有不少強大的功能,能夠具有這種內部進行修改,可是對外保持透明的能力。這多是咱們第一次體會到,後續還會不斷的經歷。
集合更像是不會有重複元素的數組,但它的本質是以元素的哈希值做爲 Key,從而實現去重的邏輯。所以,集合也能夠推導,不過得用字典的語法: a = [1,2,3,4,5,4,3,2,1] d = {i for i in a if i < 5} # d = {1, 2, 3, 4},注意這裏的大括號 回憶一下,二進制邏輯運算一共有三個運算符,按位或 |,按位與 & 和異或 ^,這三個運算符也能夠用在集合之間,並且含義變化不大。好比: a = {1, 2, 3} b = {3, 4, 5} c = a | b # c = {1, 2, 3, 4, 5} 這裏的 | 運算表示交集,也就是 c 中的任意元素,要麼在 a,要麼在 b 集合中。相似的,按位與 & 運算求的就是交集: a = {1, 2, 3} b = {3, 4, 5} c = a & b # c = {3} 而異或則表示那些只在 a 不在 b 或者只在 b 不在 a 的元素。或者換個說法,表示那些在集合 a 和 b 中出現了且僅出現了一次的元素: a = {1, 2, 3} b = {3, 4, 5} c = a ^ b # c = {1, 2, 4, 5} 還有一個差集運算 -,表示在集合 a 中但不在集合 b 中的元素: a = {1, 2, 3} b = {3, 4, 5} c = a - b # c = {1, 2}
回憶一下韋恩圖,就會獲得如下公式(雖然並無什麼卵用):
A | B = (A ^ B) | (A & B)
A ^ B = (A - B) | (B - A)
用 Python 寫過爬蟲的人都應該感覺過被字符串編碼支配的恐懼。簡單來講,編碼指的是將可讀的字符串轉換成不太可讀的數字,用來存儲或者傳輸。解碼則指的是將數字還原成字符串的過程。常見的編碼有 ASCII、GBK 等。
ASCII 編碼是一個至關小的字符集合,只有一百多個經常使用的字符,所以只用一個字節(8 位)就能表示,爲了存儲本國語言,各個國家都開發出了本身的編碼,好比中文的 GBK。這就帶來了一個問題,若是我想要在一篇文章中同時寫中文和日文,就沒法實現了,除非能對每一個字符指定編碼,這個成本高到沒法接受。
Unicode 則是一個最全的編碼方式,每一個 Unicode 字符佔據 6 個字節,能夠表示出 2 ^ 48 種字符。但隨之而來的是 Unicode 編碼後的內容不適合存儲和發送,所以誕生了基於 Unicode 的再次編碼,目的是爲了更高效的存儲。
更詳細的概念分析和配圖說明能夠參考個人這篇文章:字符串編碼入門科普,這裏咱們主要聊聊 Python 對字符串編碼的處理。
首先,編碼的函數是 encode
,它是字符串的方法:
s = 'hello' s.encode() # 獲得 b'hello' s.encode('utf16') # 獲得 b'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
encode
函數有兩個參數,第一個參數不寫表示使用默認的 utf8
編碼,理論上會輸出二進制格式的編碼結果,但在終端打印時,被自動還原回字符串了。若是用 utf16
進行編碼,則會看到編碼之後的二進制結果。
前面說過,編碼是字符轉到二進制的轉化過程,有時候在某個編碼規範中,並無指定某個字符是如何編碼的,也就是找不到對應的數字,這時候編碼就會報錯:
city = 'São Paulo' b_city = city.encode('cp437') # UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined> 此時須要用到 encode 函數的第二個參數,用來指定遇到錯誤時的行爲。它的值能夠是 'ignore',表示忽略這個不能編碼的字符,
也能夠是 'replace',表示用默認字符代替: b_city = city.encode('cp437', errors='ignore') # b'So Paulo' b_city = city.encode('cp437', errors='replace') # b'S?o Paulo'
decode
徹底是 encode
的逆操做,只有二進制類型纔有這個函數。它的兩個參數含義和 encode
函數徹底一致,就再也不介紹了。
從理論上來講,僅從編碼後的內容上來看,是沒法肯定編碼方式的,也沒法解碼出原來的字符。但不一樣的編碼有各自的特色,雖然沒法徹底倒推,但能夠從機率上來猜想,若是發現某個二進制內容,有 99% 的可能性是 utf8
編碼生成的,咱們就能夠用 utf8
進行解碼。Python 提供了一個強大的工具包 Chardet
來完成這一任務:
octets = b'Montr\xe9al' chardet.detect(octets) # {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''} octets.decode('ISO-8859-1') # Montréal
返回結果中包含了猜想的編碼方式,以及可信度。可信度越高,說明是這種編碼方式的可能性越大。
有時候,咱們拿到的是二進制的字符串字面量,好比 68 65 6c 6c 6f
,前文說過只有二進制類型纔有 decode
函數,因此須要經過二進制的字面量生成二進制變量:
s = '68 65 6c 6c 6f' b = bytearray.fromhex(s) b.decode() # hello
字符串的 split(sep, maxsplit)
方法能夠以指定的分隔符進行分割,有點相似於 Shell 中的 awk -F ' '
',第一個 sep
參數表示分隔符,不填則爲空格:
s = 'a b c d e' a = s.split() # a = ['a', 'b', 'c', 'd', 'e']
第二個參數 maxsplit 表示最多分割多少次,所以返回數組的長度是 maxsplit + 1。舉個例子說明下: s = 'a;b;c;d;e' a = s.split(';') # a = ['a', 'b', 'c', 'd', 'e'] b = s.split(';', 2) # b = ['a', 'b', 'c;d;e'] 若是想批量替換,則能夠用 replace(old, new[, count]) 方法,由中括號括起來的參數表示選填。 old = 'a;b;c;d;e' new = old.replace(';', ' ', 3) # new = 'a b c d;e' strip[chars] 用於移除指定的字符們: old = "*****!!!Hello!!!*****" new = old.strip('*') # 獲得 '!!!Hello!!!' new = old.strip('*!') # 獲得 'Hello' 若是不傳參數,則默認移除空格。其實 strip 等價於分別執行 lstrip() 和 rstrip(),即分別從左側和右側進行移除。
好比 lstrip() 表示從左側第一個字符開始,移除空格,直到第一個非空格字符爲止,因此字符串中間的空格,不管是 lstrip
仍是 strip() 都是沒法移除的。 old = ' Hello world ' new = old.strip() # 獲得 'Hello wrold' new = old.lstrip() # 獲得 'Hello world ' 最後一個經常使用方法是 join,其實這個能夠理解爲字符串的構造方法,它能夠把數組轉換成字符串: array = 'a b c d e'.split() # 以前說過,結果是 ['a', 'b', 'c', 'd', 'e'] s = ';'.join(array) # 以分號爲鏈接符,把數組中的元素鏈接起來 # s = 'a;b;c;d;e'
因此 join
能夠理解爲 split
的逆操做,這個公式始終是成立的:
c.join(string.split(c)) = string
上面這些字符串處理的函數,大多返回的仍是字符串,所以能夠鏈式調用,避免使用臨時變量和多行代碼,但也要避免過長(超過 3 個)的鏈式調用,以避免影響可讀性。
最初級的字符串格式化方法是使用 +
來拼接:
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' p = Person() print('Name: ' + p.name + ', Age: ' + str(p.age) + ', Sex: ' + p.sex) # 輸出:Name: bestswifter, Age: 22, Sex: m
這裏必需要把 int
類型的年齡轉成字符串之後才能進行拼接,這是由於 Python 是強類型語言,不支持類型的隱式轉換。
這種作法的缺點在於若是輸出結構比較複雜,極容易出現引號匹配錯誤的問題,可讀性很是低。
Python 2 中的作法是使用佔位符,相似於 C 語言中 printf
:
content = 'Name: %s, Age: %i, Sex: %c' % (p.name, p.age, p.sex) print(content)
從結構上看,要比上一種寫法清楚得多, 但每一個變量都須要指定類型,這和 Python 的簡潔不符。實際上每一個對象均可以經過 str()
函數轉換成字符串,這個函數的背後是 __str__
魔術方法。
Python 3 中的寫法是使用 format
函數,好比咱們來實現一下 __str__
方法:
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' def __str__(self): return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}' .format(user=self) p = Person() print(p) # 輸出:Name: bestswifter, Age: 22, Sex: m 除了把對象傳給 format 函數並在字符串中展開之外, 也能夠傳入多個參數,而且經過下標訪問他們: print('{0}, {1}, {0}'.format(1, 2)) # 輸出:1, 2, 1,這裏的 {1} 表示第二個參數
Heredoc 不是 Python 特有的概念, 命令行和各類腳本中都會見到,它表示一種所見即所得的文本。
假設咱們在寫一個 HTML 的模板,絕大多數字符串都是常量,只有有限的幾個地方會用變量去替換,那這個字符串該如何表示呢?
一種寫法是直接用單引號去定義: s = '<HTML><HEAD><TITLE>\nFriends CGI Demo</TITLE></HEAD>\n<BODY><H3>ERROR </H3>\n<B>%s</B><P>\n<FORM><INPUT TYPE=button VALUE=Back\nONCLICK=\'window .history .back()\'></FORM>\n</BODY></HTML>' 這段代碼是自動生成的還好,若是是手動維護的,那麼可讀性就很是差,由於換行符和轉義後的引號增長了理解的難度。
若是用 heredoc 來寫,就很是簡單了: s = '''<HTML><HEAD><TITLE> Friends CGI Demo</TITLE></HEAD> <BODY><H3>ERROR</H3> <B>%s</B><P> <FORM><INPUT TYPE=button VALUE=Back ONCLICK='window.history.back()'></FORM> </BODY></HTML> '''
Heredoc 主要是用來書寫大段的字符串常量,好比 HTML 模板,SQL語句等等。