Python 簡單入門指北(一)

Python 簡單入門指北(一)

Python 是一門很是容易上手的語言,經過查閱資料和教程,也許一夜就能寫出一個簡單的爬蟲。但 Python 也是一門很難精通的語言,由於簡潔的語法背後隱藏了許多黑科技。本文主要針對的讀者是:python

  1. 毫無 Python 經驗的小白
  2. 有一些簡單 Python 經驗,但只會複製粘貼代碼,不知其因此然的讀者
  3. 以爲單獨一篇文章太瑣碎,質量沒保證,卻沒空讀完一本書,但又想對 Python 有全面瞭解的讀者

固然, 用一篇文章來說完某個語言是不可能的事情,我但願讀完本文的讀者能夠:swift

  1. 對 Python 的總體知識結構造成初步的概念
  2. 瞭解 Python 特有的知識點,好比裝飾器、上下文、生成器等等,不只會寫 Demo,還對背後的原理有必定了解
  3. 避免 C++/Java 等風格的 Python 代碼,可以寫出地道的 Python 代碼
  4. 可以熟練的使用 Python 編寫腳本實現平常的簡單需求,可以維護小型 Python 項目,可以閱讀較複雜的 Python 源碼

若是以上介紹符合你對本身的定位,在開始閱讀前,還須要明確幾點:數組

  1. 本文不會只介紹用法,那樣太膚淺
  2. 本文不會深刻介紹某個知識點,好比分析源碼等,那樣太囉嗦,我但願作一名引路人,描述各個知識點的概貌並略做引伸,爲讀者指出下一步的研究方向
  3. 代碼註釋很是重要,必定要看,幾乎全部的代碼段均可以執行,強烈建議手敲一遍!

0. 準備工做

請不要在學習 Python2 仍是 Python3 之間猶豫了,除非你很明確本身只接觸 Python2,不然就從 Python3 學起,新版本的語言老是意味着進步的生產力(Swift 和 Xcode 除外)。Python 2 和 3 之間語法不兼容,但這並不影響熟悉 Python3 的開發者迅速寫出 Python 2 的代碼,反之亦然。因此與其在反覆糾結中浪費時間,不如馬上行動起來。數據結構

推薦使用 CodeRunner 來運行本文中的 demo,它比文本編輯器功能更強大,好比支持自動補全和斷點調試,又比 PyCharm 輕量得多。app

1. 數據結構

1.1 數組

1.1.1 列表推導

若是要對數組中的全部內容作一些修改,能夠用 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']

1.1.2 元組

元組能夠簡單的理解爲不可變的數組,也就是沒有 appenddel 等方法,一旦建立,就沒法新增或刪除元素,元素自身的值也不能改變,但元素內部的屬性是否可變並不受元組的影響,這一點符合其餘語言中的常識。工具

 
 
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

1.1.3 數組切片

切片的基本格式是 array[start:end:step],表示對 array 在 start 到 end 以前以 step 爲間隔取切片。注意這裏的區間是 [start, end),也就是左閉右開。好比:性能

 
 
s = 'hello'
s[0:5:2]
# 表示取 s 的第 024 個字符,結果是 '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)
 
 

1.1.5 魔術方法

也許你已經注意到了,數組和字符串都支持切片,並且語法高度統一。這在某些強類型語言(好比我常常接觸的 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__ 的做用和用法。

1.2 字典

1.2.1 初始化字典

最簡單的建立一個字典的方式就是直接寫字面量:

{'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 的判斷來檢查鍵是否存在,甚至能夠先 trycatch 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.2.4 字典的魔術方法

在 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 有不少強大的功能,能夠具有這種內部進行修改,可是對外保持透明的能力。這多是咱們第一次體會到,後續還會不斷的經歷。

1.2.5 集合

 
 
集合更像是不會有重複元素的數組,但它的本質是以元素的哈希值做爲 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)

1.3 字符串

1.3.1 字符串編碼

用 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

1.3.2 字符串的經常使用方法

字符串的 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 個)的鏈式調用,以避免影響可讀性。

1.3.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} 表示第二個參數

1.3.4 HereDoc

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語句等等。

相關文章
相關標籤/搜索