Life is short, you need Python
人生苦短,我用Python
— Bruce Eckel
給深度學習入門者的Python快速教程
5.1 Python簡介
本章將介紹Python的最基本語法,以及一些和深度學習還有計算機視覺最相關的基本使用。
5.1.1 Python簡史
Python是一門解釋型的高階程式語言,特點是簡單明確。Python作者是荷蘭人Guido van Rossum,1982年他獲得數學和計算機碩士學位後,在荷蘭數學與計算科學研究所(Centrum Wiskunde & Informatica, CWI)謀了份差事。在CWI期間,Guido參與到了一門叫做ABC的語言開發工作中。ABC是一門教學語言,所以擁有簡單,可讀性好,語法更接近自然語言等特點。在那個C語言一統天下的年代,ABC就是一股簡單的清流,畢竟是門教學語言,最後沒有流行起來,不過這段經歷影響了Guido。1989年的聖誕假期,閒得蛋疼的Guido決定設計一門簡單易用的新語言,要介於C和Shell之間,同時吸取ABC語法中的優點。Guido用自己喜歡的一部喜劇電視劇來命名這門語言:《Monty Python‘s Flying Circus》。
1991年,第一版基於C實現的Python編譯器誕生,因為簡單,擴充性好,Python很快就在Guido的同事中大受歡迎,不久Python的核心開發人員就從Guido一人變成了一個小團隊。後來隨著網際網路時代的到來,開源及社群合作的方式蓬勃發展,Python也藉此上了發展的快車道。因為Python非常容易擴充,在不同領域的開發者貢獻下,許多受歡迎的功能和特徵被開發出來,漸漸形成了各種各樣的庫,其中一部分被加入到Python的標準庫中,這讓本來就不需要過多思考底層細節的Python變得更加強大好用。在不過多考慮執行效率的前提下,使用Python進行開發的週期相比傳統的C/C++甚至Java等語言都大大縮短,程式碼量也大幅降低,所以出bug的可能性也小了很多。因此有了語言專家Bruce Eckel的那句名言:Life is short, you need Python. 後來這句話的中文版“人生苦短,我用Python”被Guido印在了T恤上。發展至今,Python漸漸成了最流行的語言之一,在程式語言排行榜TOBIE中常年佔據前5的位置。另外隨著Python的使用者群越來越壯大,慢慢在本身特點上發展出了自己的哲學,叫做Python的禪(The Zen of Python)。遵循Python哲學的做法叫做很Python(Pythonic),具體參見:
或者在Python中執行:
1 |
>> import this |
Python擁有很好的擴充性,可以非常輕鬆地用其他語言編寫模組供呼叫,用Python編寫的模組也可以通過各種方式輕鬆被其他語言呼叫。所以一種常見的Python使用方式是,底層複雜且對效率要求高的模組用C/C++等語言實現,頂層呼叫的API用Python封裝,這樣可以通過簡單的語法實現頂層邏輯,故而Python又被稱為“膠水語言”。這種特性的好處是,無需花費很多時間在程式設計實現上,更多的時間可以專注于思考問題的邏輯。尤其是對做演算法和深度學習的從業人員,這種方式是非常理想的,所以如今的深度學習框架中,除了MATLAB,或是Deeplearning4j這種擺明了給Java用的,其他框架基本上要麼官方介面就是Python,要麼支援Python介面。
5.1.2 安裝和使用Python
Python有兩個大版本,考慮到使用者群數量和庫的各種框架的相容性,本文以Python2(2.7)為準,語法儘量考慮和Python3的相容。
Unix/Linux下的Python基本都是系統自帶的,一般預設為Python2,使用時在終端直接鍵入python就能進入Python直譯器介面:
在直譯器下就已經可以進行最基本的程式設計了,比如:
寫程式的話還是需要儲存成檔案再執行,比如我們寫下面語句,並且儲存為helloworld.py:
print(“Hello world!”)
然後在終端裡執行:
安裝更多的python庫一般有兩種方法,第一是用系統的軟體包管理,以Ubuntu 16.04 LTS為例,比如想要安裝numpy庫(後面會介紹這個庫),軟體包的名字就是python-numpy,所以在終端中輸入:
>> sudo apt install python-numpy
Python自己也帶了包管理器,叫做pip,使用如下:
>> pip install numpy
安裝和深度學習相關的框架時,一般來說推薦使用系統自帶的包管理,出現版本錯誤的可能性低一些。另外也可以使用一些提前配置好很多第三方庫的Python包,這些包通常已經包含了深度學習框架中絕大多數的依賴庫,比如最常用的是Anaconda:
Windows下的Python安裝簡單一些,從官方網站下載相應的安裝程式就可以了,當然也有更方便的已經包含了很全的第三方庫的選擇,WinPython:
並且是綠色的,直接執行就可以用了。
5.2 Python基本語法
There should be one– and preferably only one –obvious way to do it.
對於一個特定的問題,應該只用最好的一種方法來解決。
— Tim Peters
5.2.1 基本資料型別和運算
基本資料型別
Python中最基本的資料型別包括整型,浮點數,布林值和字串。型別是不需要宣告的,比如:
1 2 3 4 5 |
a = 1 # 整數 b = 1.2 # 浮點數 c = True # 布林型別 d = "False" # 字串 e = None # NoneType |
其中#是行內註釋的意思。最後一個None是NoneType,注意不是0,在Python中利用type函式可以檢視一個變數的型別:
1 2 3 4 5 |
type(a) # <type 'int'> type(b) # <type 'float'> type(c) # <type 'bool'> type(d) # <type 'str'> type(e) # <type 'NoneType'> |
註釋中是執行type()函式後的輸出結果,可以看到None是單獨的一種型別NoneType。在很多API中,如果執行失敗就會返回None。
變數和引用
Python中基本變數的賦值一般建立的是個引用,比如下面的語句:
1 2 3 |
a = 1 b = a c = 1 |
a賦值為1後,b=a執行時並不會將a的值複製一遍,然後賦給b,而是簡單地為a所指的值,也就是1建立了一個引用,相當於a和b都是指向包含1這個值的這塊記憶體的指標。所以c=1執行的也是個引用建立,這三個變數其實是三個引用,指向同一個值。這個邏輯雖然簡單,不過也還是常常容易弄混,這沒關係,Python內建了id函式,可以返回一個物件的地址,用id函式可以讓我們知道每個變數指向的是不是同一個值:
1 2 3 |
id(a) # 35556792L id(b) # 35556792L id(c) # 35556792L |
註釋中表示的仍是執行後的結果。如果這時候我們接下面兩個語句:
1 2 |
b = 2 # b的引用到新的一個變數上 id(b) # 35556768L |
可以看到b引用到了另一個變數上。
運算子
Python中的數值的基本運算和C差不多,字串的運算更方便,下面是常見的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
a = 2 b = 2.3 c = 3 a + b # 2 + 2.3 = 4.3 c – a # 3 - 2 = 1 a / b # 整數除以浮點數,運算以浮點數為準,2 / 2.3 = 0.8695652173913044 a / c # Python2中,整數除法,向下取整 2 / 3 = 0 a ** c # a的c次方,結果為8 a += 1 # Python中沒有i++的用法,自增用+= c -= 3 # c變成0了 d = 'Hello' d + ' world!' # 相當於字串拼接,結果為'Hello world!' d += ' "world"!'# 相當於把字串接在當前字串尾,d變為'Hello "world"!' e = r'\n\t\\' print(e) # '\\n\\t\\\\' |
需要提一下的幾點:1)字串用雙引號和單引號都可以,區別主要是單引號字串中如果出現單引號字元則需要用轉義符,雙引號也是一樣,所以在單引號字串中使用雙引號,或者雙引號字串中使用單引號就會比較方便。另外三個雙引號或者三個單引號圍起來的也是字串,因為換行方便,更多用於文件。2)Python2中兩個數值相除會根據數值型別判斷是否整數除法,Python3種則都按照浮點數。想要在Python2種也執行Python3中的除法只要執行下面語句:
1 2 |
from __future__ import division # 使用Python3中的除法 1 / 2 |
3)字串前加r表示字串內容嚴格按照輸入的樣子,好處是不用轉義符了,非常方便。
Python中的布林值和邏輯的運算非常直接,下面是例子:
1 2 3 4 5 |
a = True b = False a and b # False a or b # True not a # False |
基本上就是英語,操作符優先順序之類的和其他語言類似。Python中也有位操作:
1 2 3 4 5 6 |
~8 # 按位翻轉,1000 --> -(1000+1) 8 >> 3 # 右移3位,1000 --> 0001 1 << 3 # 左移3位,0001 --> 1000 5 & 2 # 按位與,101 & 010 = 000 5 | 2 # 按位或,101 | 010 = 111 4 ^ 1 # 按位異或,100 ^ 001 = 101 |
==, !=和is
判斷是否相等或者不等的語法和C也一樣,另外在Python中也常常見到is操作符,這兩者的區別在於==和!=比較引用指向的記憶體中的內容,而is判斷兩個變數是否指向一個地址,看下面的程式碼例子:
1 2 3 4 5 6 |
a = 1 b = 1.0 c = 1 a == b # True,值相等 a is b # False,指向的不是一個物件,這個語句等效於 id(a) == id(b) a is c # True,指向的都是整型值1 |
所以一定要分清要比較的物件應該用那種方式,對於一些特殊的情況,比如None,本著Pythonic的原則,最好用is None。
注意關鍵字
Python中,萬物皆物件。不過這並不是這裡要探討的話題,想說的是一定要注意關鍵字,因為所有東西都是物件,所以一個簡簡單單的賦值操作就可以把系統內建的函式給變成一個普通變數,來看下邊例子:
1 2 3 4 5 6 |
id(type) # 506070640L type = 1 # type成了指向1的變數 id(type) # 35556792L id = 2 # id成了指向2的變數 from __future__ import print_function print = 3 # print成了指向3的變數 |
注意print是個很特殊的存在,在Python3中是按照函式用,在Python2中卻是個命令式的語句,最早print的用法其實是下邊這樣:
1 |
print "Hello world!" |
這麼用主要是受到ABC語法的影響,但這個用法並不Pythonic,後來加入了print函式,為了相容允許兩種用法並存。所以單純給print賦值是不靈的,在Python2中使用Python3中的一些特性都是用from __future__ import來實現。
模組匯入
因為提到了物件名覆蓋和import,所以簡單講一下。import是利用Python中各種強大庫的基礎,比如要計算cos(π)的值,可以有下面4種方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 直接匯入Python的內建基礎數學庫 import math print(math.cos(math.pi)) # 從math中匯入cos函式和pi變數 from math import cos, pi print(cos(pi)) # 如果是個模組,在匯入的時候可以起個別名,避免名字衝突或是方便懶得打字的人使用 import math as m print(m.cos(m.pi)) # 從math中匯入所有東西 from math import * print(cos(pi)) |
一般來說最後一種方式不是很推薦,因為不知道import匯入的名字裡是否和現有物件名已經有衝突,很可能會不知不覺覆蓋了現有的物件。
5.2.2 容器
列表
Python中的容器是異常好用且異常有用的結構。這節主要介紹列表(list),元組(tuple),字典(dict)和集合(set)。這些結構和其他語言中的類似結構並無本質不同,來看例子瞭解下使用:
1 2 3 4 5 6 7 8 9 10 11 |
a = [1, 2, 3, 4] b = [1] c = [1] d = b e = [1, "Hello world!", c, False] print(id(b), id(c)) # (194100040L, 194100552L) print(id(b), id(d)) # (194100040L, 194100040L) print(b == c) # True f = list("abcd") print(f) # ['a', 'b', 'c', 'd'] g = [0]*3 + [1]*4 + [2]*2 # [0, 0, 0, 1, 1, 1, 1, 2, 2] |
因為變數其實是個引用,所以對列表而言也沒什麼不同,所以列表對型別沒什麼限制。也正因為如此,和變數不同的是,即使用相同的語句賦值,列表的地址也是不同的,在這個例子中體現在id(b)和id(c)不相等,而內容相等。列表也可以用list()初始化,輸入引數需要是一個可以遍歷的結構,其中每一個元素會作為列表的一項。“*”操作符對於列表而言是複製,最後一個語句用這種辦法生成了分段的列表。
列表的基本操作有訪問,增加,刪除,和拼接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
a.pop() # 把最後一個值4從列表中移除並作為pop的返回值 a.append(5) # 末尾插入值,[1, 2, 3, 5] a.index(2) # 找到第一個2所在的位置,也就是1 a[2] # 取下標,也就是位置在2的值,也就是第三個值3 a += [4, 3, 2] # 拼接,[1, 2, 3, 5, 4, 3, 2] a.insert(1, 0) # 在下標為1處插入元素0,[1, 0, 2, 3, 5, 4, 3, 2] a.remove(2) # 移除第一個2,[1, 0, 3, 5, 4, 3, 2] a.reverse() # 倒序,a變為[2, 3, 4, 5, 3, 0, 1] a[3] = 9 # 指定下標處賦值,[2, 3, 4, 9, 3, 0, 1] b = a[2:5] # 取下標2開始到5之前的子序列,[4, 9, 3] c = a[2:-2] # 下標也可以倒著數,方便算不過來的人,[4, 9, 3] d = a[2:] # 取下標2開始到結尾的子序列,[4, 9, 3, 0, 1] e = a[:5] # 取開始到下標5之前的子序列,[2, 3, 4, 9, 3] f = a[:] # 取從開頭到最後的整個子序列,相當於值拷貝,[2, 3, 4, 9, 3, 0, 1] a[2:-2] = [1, 2, 3] # 賦值也可以按照一段來,[2, 3, 1, 2, 3, 0, 1] g = a[::-1] # 也是倒序,通過slicing實現並賦值,效率略低於reverse() a.sort() print(a) # 列表內排序,a變為[0, 1, 1, 2, 2, 3, 3] |
因為列表是有順序的,所以和順序相關的操作是列表中最常見的,首先我們來打亂一個列表的順序,然後再對這個列表排序:
1 2 3 4 5 6 7 8 9 |
import random a = range(10) # 生成一個列表,從0開始+1遞增到9 print(a) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] random.shuffle(a) # shuffle函式可以對可遍歷且可變結構打亂順序 print(a) # [4, 3, 8, 9, 0, 6, 2, 7, 5, 1] b = sorted(a) print(b) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] c = sorted(a, reverse=True) print(c) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] |
元組
元組和列表有很多相似的地方,最大的區別在於不可變,還有如果初始化只包含一個元素的tuple和列表不一樣,因為語法必須明確,所以必須在元素後加上逗號。另外直接用逗號分隔多個元素賦值預設是個tuple,這在函式多返回值的時候很好用:
1 2 3 4 5 6 7 8 |
a = (1, 2) b = tuple(['3', 4]) # 也可以從列表初始化 c = (5,) print(c) # (5,) d = (6) print(d) # 6 e = 3, 4, 5 print(e) # (3, 4, 5) |
集合
集合是一種很有用的數學操作,比如列表去重,或是理清兩組資料之間的關係,集合的操作符和位操作符有交集,注意不要弄混:
1 2 3 4 5 6 7 8 9 |
A = set([1, 2, 3, 4]) B = {3, 4, 5, 6} C = set([1, 1, 2, 2, 2, 3, 3, 3, 3]) print(C) # 集合的去重效果,set([1, 2, 3]) print(A | B) # 求並集,set([1, 2, 3, 4, 5, 6]) print(A & B) # 求交集,set([3, 4]) print(A - B) # 求差集,屬於A但不屬於B的,set([1, 2]) print(B - A) # 求差集,屬於B但不屬於A的,set([5, 6]) print(A ^ B) # 求對稱差集,相當於(A-B)|(B-A),set([1, 2, 5, 6]) |
字典
字典是一種非常常見的“鍵-值”(key-value)對映結構,鍵無重複,一個鍵不能對應多個值,不過多個鍵可以指向一個值。還是通過例子來了解,構建一個名字->年齡的字典,並執行一些常見操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
a = {'Tom': 8, 'Jerry': 7} print(a['Tom']) # 8 b = dict(Tom=8, Jerry=7) # 一種字串作為鍵更方便的初始化方式 print(b['Tom']) # 8 if 'Jerry' in a: # 判斷'Jerry'是否在keys裡面 print(a['Jerry']) # 7 print(a.get('Spike')) # None,通過get獲得值,即使鍵不存在也不會報異常 a['Spike'] = 10 a['Tyke'] = 3 a.update({'Tuffy': 2, 'Mammy Two Shoes': 42}) print(a.values()) # dict_values([8, 2, 3, 7, 10, 42]) print(a.pop('Mammy Two Shoes')) # 移除'Mammy Two Shoes'的鍵值對,並返回42 print(a.keys()) # dict_keys(['Tom', 'Tuffy', 'Tyke', 'Jerry', 'Spike']) |
注意到初始化字典和集合很像,的確如此,集合就像是沒有值只有鍵的字典。既然有了人名到年齡的對映,也許你立馬想到是否可以給字典排序?在Python3.6之前,這個問題是錯誤的,字典是一種對映關係,沒有順序。當然了,如果要把(鍵, 值)的這種對進行排序,是沒有問題的,前提是先把字典轉化成可排序的結構,items()或者iteritems()可以做到這件事,接上段程式碼繼續:
1 2 3 4 5 6 7 8 9 |
b = a.items() print(b) # [('Tuffy', 2), ('Spike', 10), ('Tom', 8), ('Tyke', 3), ('Jerry', 7)] from operator import itemgetter c = sorted(a.items(), key=itemgetter(1)) print(c) # [('Tuffy', 2), ('Tyke', 3), ('Jerry', 7), ('Tom', 8), ('Spike', 10)] d = sorted(a.iteritems(), key=itemgetter(1)) print(d) # [('Tuffy', 2), ('Tyke', 3), ('Jerry', 7), ('Tom', 8), ('Spike', 10)] e = sorted(a) print(e) # 只對鍵排序,['Jerry', 'Spike', 'Tom', 'Tuffy', 'Tyke'] |
items()可以把字典中的鍵值對轉化成一個列表,其中每個元素是一個tuple,tuple的第一個元素是鍵,第二個元素是值。變數c是按照值排序,所以需要一個操作符itemgetter,去位置為1的元素作為排序參考,如果直接對字典排序,則其實相當於只是對鍵排序。字典被當作一個普通的可遍歷結構使用時,都相當於遍歷字典的鍵。如果覺得字典沒有順序不方便,可以考慮使用OrderedDict,使用方式如下:
1 2 3 4 5 |
from collections import OrderedDict a = {1: 2, 3: 4, 5: 6, 7: 8, 9: 10} b = OrderedDict({1: 2, 3: 4, 5: 6, 7: 8, 9: 10}) print(a) # {1: 2, 3: 4, 9: 10, 5: 6, 7: 8} print(b) # OrderedDict([(1, 2), (3, 4), (9, 10), (5, 6), (7, 8)]) |
這樣初始化時的順序就保留了,除了有序的特性以外,用法上和字典沒有區別。2016年9月,Guido宣佈在Python3.6中,字典將預設有序,這樣就不用糾結了。另外需要注意的一點是字典是通過雜湊表實現的,所以鍵必須是可雜湊的, list不能被雜湊,所以也不能作為字典的鍵,而tuple就可以。
因為上上段程式碼中用到了iteritems(),所以這裡順帶提一下迭代器(iterator),迭代器相當於一個函式,每次呼叫都返回下一個元素,從遍歷的角度來看就和列表沒有區別了。iteritems()就是一個迭代器,所以效果一樣,區別是迭代器佔用更少記憶體,因為不需要一上來就生成整個列表。一般來說,如果只需要遍歷一次,用迭代器是更好的選擇,若是要多次頻繁從一個可遍歷結構中取值,且記憶體夠,則直接生成整個列表會更好。當然,用迭代器生成一個完整列表並不麻煩,所以有個趨勢是把迭代器作為預設的可遍歷方式,比如前面我們使用過用來生成等差數列列表的range(),在Python2中對應的迭代器形式是xrange()。在Python3中,range()就不再產生一個列表了,而是作為迭代器,xrange()直接沒了。
5.2.3 分支和迴圈
從這節開始,程式碼就未必適合在Python終端中輸入了,選個順手的編輯器或者IDE。作者良心推薦PyCharm,雖然慢,但好用,社群版免費:
for迴圈
上面提到的4種容器型別都是可遍歷的,所以該講講用來遍歷的for迴圈了。for迴圈的語法也是簡單的英語:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
a = ['This', 'is', 'a', 'list', '!'] b = ['This', 'is', 'a', 'tuple', '!'] c = {'This': 'is', 'an': 'unordered', 'dict': '!'} # 依次輸出:'This', 'is', 'a', 'list', '!' for x in a: print(x) # 依次輸出:'This', 'is', 'a', 'tuple', '!' for x in b: print(x) # 鍵的遍歷。不依次輸出:'This', 'dict', 'an' for key in c: print(key) # 依次輸出0到9 for i in range(10): print(i) |
注意到每個for迴圈中,print都有縮排,這是Python中一個讓人愛恨交織的特點:強行縮排來表明成塊的程式碼。這樣做的好處是程式碼十分清晰工整,還有助於防止寫出過長的函式或者過深的巢狀,壞處是有時候不知為什麼tab和空格就一起出現了,又或是多重if-else不知怎得就沒對齊,還是挺麻煩的。
回到for迴圈上,這種把每個元素拿出來的遍歷方式叫做for_each風格,熟悉Java的話就不會陌生,C++11中也開始支援這種for迴圈方式。不過如果還是需要下標呢?比如遍歷一個list的時候,希望把對應下標也列印出來,這時可以用enumerate:
1 2 3 4 5 |
names = ["Rick", "Daryl", "Glenn"] # 依次輸出下標和名字 for i, name in enumerate(names): print(i, name) |
需要注意的是,通過取下標遍歷當然是可行的,比如用len()函式獲得列表長度,然後用range()/xrange()函式獲得下標,但是並不推薦這樣做:
1 2 3 4 5 |
words = ["This", "is", "not", "recommended"] # not pythonic :( for i in xrange(len(words)): print(words[i]) |
在使用for迴圈時,有時會遇到這樣一種場景:我們需要對遍歷的每個元素進行某種判斷,如果符合這種判斷的情況沒有發生,則執行一個操作。舉個例子某神祕部門要稽核一個字串列表,如果沒有發現不和諧的字眼,則將內容放心通過,一種解決辦法是下面這樣:
1 2 3 4 5 6 7 8 9 10 11 |
wusuowei = ["I", "don't", "give", "a", "shit"] # 無所謂 hexie = True # 預設和諧社會 for x in wusuowei: if x == "f**k": print("What the f**k!") # 發現了不該出現的東西,WTF! hexie = False # 不和諧了 break # 趕緊停下!不能再唱了 if hexie: # 未發現不和諧元素! print("Harmonious society!") # 和諧社會! |
這樣需要設定一個標記是否發現不和諧因素的狀態變數hexie,迴圈結束後再根據這個變數判斷內容是否可以放心通過。一種更簡潔不過有些小眾的做法是直接和else一起,如果for迴圈中的if塊內的語句沒有被觸發,則通過else執行指定操作:
1 2 3 4 5 6 7 8 9 |
wusuowei = ["I", "don't", "give", "a", "shit"] for x in wusuowei: if x == "f**k": print("What the f**k!") hexie = False break else: # for迴圈中if內語句未被觸發 print("Harmonious society!") # 和諧社會! |
這樣不需要一個標記是否和諧的狀態變數,語句簡潔了很多。
if和分支結構
上一個例子中已經出現if語句了,所以這部分講講if。Python的條件控制主要是三個關鍵字:if-elif-else,其中elif就是else if的意思。還是看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
pets =['dog', 'cat', 'droid', 'fly'] for pet in pets: if pet == 'dog': # 狗糧 food = 'steak' # 牛排 elif pet == 'cat': # 貓糧 food = 'milk' # 牛奶 elif pet == 'droid': # 機器人 food = 'oil' # 機油 elif pet == 'fly': # 蒼蠅 food = 'sh*t' # else: pass print(food) |
需要提一下的是pass,這就是個空語句,什麼也不做,佔位用。Python並沒有switch-case的語法,等效的用法要麼是像上面一樣用if-elif-else的組合,要麼可以考慮字典:
1 2 3 4 5 6 7 8 9 10 11 |
pets = ['dog', 'cat', 'droid', 'fly'] food_for_pet = { 'dog': 'steak', 'cat': 'milk', 'droid': 'oil', 'fly': 'sh*t' } for pet in pets: food = food_for_pet[pet] if pet in food_for_pet else None print(food) |
這裡還用到了一個if-else常見的行內應用,就是代替三元操作符,如果鍵在字典中,則food取字典的對應值,否則為None。
if表示式中的小技巧
通過鏈式比較讓語句簡潔:
1 2 |
if -1 < x < 1: # 相較於 if x > -1 and x < 1: print('The absolute value of x is < 1') |
判斷一個值是不是等於多個可能性中的一個:
1 2 |
if x in ['piano', 'violin', 'drum']: # 相較於 if x == 'piano' or x == 'violin' or x =='drum': print("It's an instrument!") |
Python中的物件都會關聯一個真值,所以在if表示式中判斷是否為False或者是否為空的時候,是無需寫出明確的表示式的:
1 2 3 4 5 6 7 8 9 10 11 12 |
a = True if a: # 判斷是否為真,相較於 a is True print('a is True') if 'sky': # 判斷是否空字串,相較於 len('sky') > 0 print('birds') if '': # 判斷是否空字串,同上 print('Nothing!') if {}: # 判斷是否空的容器(字典),相較於len({}) > 0 print('Nothing!') |
隱式表示式為False的是如下狀況:
– None
– False
– 數值0
– 空的容器或序列(字串也是一種序列)
– 使用者自定義類中,如果定義了__len__()或者__nonzero__(),並且被呼叫後返回0或者False
while迴圈
while的就是迴圈和if的綜合體,是一種單純的基於條件的迴圈,本身沒有遍歷的意思,這是和for_each的本質差別,這種區別比起C/C++中要明確得多,用法如下:
1 2 3 4 5 6 |
i = 0 while i < 100: # 笑100遍 print("ha") while True: # 一直笑 print("ha") |
5.2.4 函式、生成器和類
還是從幾個例子看起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
def say_hello(): print('Hello!') def greetings(x='Good morning!'): print(x) say_hello() # Hello! greetings() # Good morning! greetings("What's up!") # What's up! a = greetings() # 返回值是None def create_a_list(x, y=2, z=3): # 預設引數項必須放後面 return [x, y, z] b = create_a_list(1) # [1, 2, 3] c = create_a_list(3, 3) # [3, 3, 3] d = create_a_list(6, 7, 8) # [6, 7, 8] def traverse_args(*args): for arg in args: print(arg) traverse_args(1, 2, 3) # 依次列印1, 2, 3 traverse_args('A', 'B', 'C', 'D') # 依次列印A, B, C, D def traverse_kargs(**kwargs): for k, v in kwargs.items(): print(k, v) traverse_kargs(x=3, y=4, z=5) # 依次列印('x', 3), ('y', 4), ('z', 5) traverse_kargs(fighter1='Fedor', fighter2='Randleman') def foo(x, y, *args, **kwargs): print(x, y) print(args) print(kwargs) # 第一個pring輸出(1, 2) # 第二個print輸出(3, 4, 5) # 第三個print輸出{'a': 3, 'b': 'bar'} foo(1, 2, 3, 4, 5, a=6, b='bar') |
其實和很多語言差不多,括號裡面定義引數,引數可以有預設值,且預設值不能在無預設值引數之前。Python中的返回值用return定義,如果沒有定義返回值,預設返回值是None。引數的定義可以非常靈活,可以有定義好的固定引數,也可以有可變長的引數(args: arguments)和關鍵字引數(kargs: keyword arguments)。如果要把這些引數都混用,則固定引數在最前,關鍵字引數在最後。
Python中萬物皆物件,所以一些情況下函式也可以當成一個變數似的使用。比如前面小節中提到的用字典代替switch-case的用法,有的時候我們要執行的不是通過條件判斷得到對應的變數,而是執行某個動作,比如有個小機器人在座標(0, 0)處,我們用不同的動作控制小機器人移動:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
moves = ['up', 'left', 'down', 'right'] coord = [0, 0] for move in moves: if move == 'up': # 向上,縱座標+1 coord[1] += 1 elif move == 'down': # 向下,縱座標-1 coord[1] -= 1 elif move == 'left': # 向左,橫座標-1 coord[0] -= 1 elif move == 'right': # 向右,橫座標+1 coord[0] += 1 else: pass print(coord) |
不同條件下對應的是對座標這個列表中的值的操作,單純的從字典取值就辦不到了,所以就把函式作為字典的值,然後用這個得到的值執行相應動作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
moves = ['up', 'left', 'down', 'right'] def move_up(x): # 定義向上的操作 x[1] += 1 def move_down(x): # 定義向下的操作 x[1] -= 1 def move_left(x): # 定義向左的操作 x[0] -= 1 def move_right(x): # 定義向右的操作 x[0] += 1 # 動作和執行的函式關聯起來,函式作為鍵對應的值 actions = { 'up': move_up, 'down': move_down, 'left': move_left, 'right': move_right } coord = [0, 0] for move in moves: actions[move](coord) print(coord) |
把函式作為值取到後,直接加一括號就能使了,這樣做之後起碼在迴圈部分看上去很簡潔。有點C裡邊函式指標的意思,只不過更簡單。其實這種用法在之前講排序的時候我們已經見過了,就是operator中的itemgetter。itemgetter(1)得到的是一個可呼叫物件(callable object),和返回下標為1的元素的函式用起來是一樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def get_val_at_pos_1(x): return x[1] heros = [ ('Superman', 99), ('Batman', 100), ('Joker', 85) ] sorted_pairs0 = sorted(heros, key=get_val_at_pos_1) sorted_pairs1 = sorted(heros, key=lambda x: x[1]) print(sorted_pairs0) print(sorted_pairs1) |
在這個例子中我們用到了一種特殊的函式:lambda表示式。Lambda表示式在Python中是一種匿名函式,lambda關鍵字後面跟輸入引數,然後冒號後面是返回值(的表示式),比如上邊例子中就是一個取下標1元素的函式。當然,還是那句話,萬物皆物件,給lambda表示式取名字也是一點問題沒有的:
1 2 |
some_ops = lambda x, y: x + y + x*y + x**y some_ops(2, 3) # 2 + 3 + 2*3 + 2^3 = 19 |
生成器(Generator)
生成器是迭代器的一種,形式上看和函式很像,只是把return換成了yield,在每次呼叫的時候,都會執行到yield並返回值,同時將當前狀態儲存,等待下次執行到yield再繼續:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 從10倒數到0 def countdown(x): while x >= 0: yield x x -= 1 for i in countdown(10): print(i) # 列印小於100的斐波那契數 def fibonacci(n): a = 0 b = 1 while b < n: yield b a, b = b, a + b for x in fibonacci(100): print(x) |
生成器和所有可迭代結構一樣,可以通過next()函式返回下一個值,如果迭代結束了則丟擲StopIteration異常:
1 2 3 4 5 |
a = fibonacci(3) print(next(a)) # 1 print(next(a)) # 1 print(next(a)) # 2 print(next(a)) # 丟擲StopIteration異常 |
Python3.3以上可以允許yield和return同時使用,return的是異常的說明資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Python3.3以上可以return返回異常的說明 def another_fibonacci(n): a = 0 b = 1 while b < n: yield b a, b = b, a + b return "No more ..." a = another_fibonacci(3) print(next(a)) # 1 print(next(a)) # 1 print(next(a)) # 2 print(next(a)) # 丟擲StopIteration異常並列印No more訊息 |
類(Class)
Python中的類的概念和其他語言相比沒什麼不同,比較特殊的是protected和private在Python中是沒有明確限制的,一個慣例是用單下劃線開頭的表示protected,用雙下劃線開頭的表示private:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class A: """Class A""" def __init__(self, x, y, name): self.x = x self.y = y self._name = name def introduce(self): print(self._name) def greeting(self): print("What's up!") def __l2norm(self): return self.x**2 + self.y**2 def cal_l2norm(self): return self.__l2norm() a = A(11, 11, 'Leonardo') print(A.__doc__) # "Class A" a.introduce() # "Leonardo" a.greeting() # "What's up!" print(a._name) # 可以正常訪問 print(a.cal_l2norm()) # 輸出11*11+11*11=242 print(a._A__l2norm()) # 仍然可以訪問,只是名字不一樣 print(a.__l2norm()) # 報錯: 'A' object has no attribute '__l2norm' |
類的初始化使用的是__init__(self,),所有成員變數都是self的,所以以self.開頭。可以看到,單下劃線開頭的變數是可以直接訪問的,而雙下劃線開頭的變數則觸發了Python中一種叫做name mangling的機制,其實就是名字變了下,仍然可以通過前邊加上“_類名”的方式訪問。也就是說Python中變數的訪問許可權都是靠自覺的。類定義中緊跟著類名字下一行的字串叫做docstring,可以寫一些用於描述類的介紹,如果有定義則通過“類名.__doc__”訪問。這種前後都加雙下劃線訪問的是特殊的變數/方法,除了__doc__和__init__還有很多,這裡就不展開講了。
Python中的繼承也非常簡單,最基本的繼承方式就是定義類的時候把父類往括號裡一放就行了:
1 2 3 4 5 6 7 8 9 10 |
class B(A): """Class B inheritenced from A""" def greeting(self): print("How's going!") b = B(12, 12, 'Flaubert') b.introduce() # Flaubert b.greeting() # How's going! print(b._name()) # Flaubert print(b._A__l2norm()) # “私有”方法,必須通過_A__l2norm訪問 |
5.2.5 map, reduce和filter
map可以用於對可遍歷結構的每個元素執行同樣的操作,批量操作:
1 2 3 |
map(lambda x: x**2, [1, 2, 3, 4]) # [1, 4, 9, 16] map(lambda x, y: x + y, [1, 2, 3], [5, 6, 7]) # [6, 8, 10] |
reduce則是對可遍歷結構的元素按順序進行兩個輸入引數的操作,並且每次的結果儲存作為下次操作的第一個輸入引數,還沒有遍歷的元素作為第二個輸入引數。這樣的結果就是把一串可遍歷的值,減少(reduce)成一個物件:
1 |
reduce(lambda x, y: x + y, [1, 2, 3, 4]) # ((1+2)+3)+4=10 |
filter顧名思義,根據條件對可遍歷結構進行篩選:
1 |
filter(lambda x: x % 2, [1, 2, 3, 4, 5]) # 篩選奇數,[1, 3, 5] |
需要注意的是,對於filter和map,在Python2中返回結果是列表,Python3中是生成器。
5.2.6 列表生成(list comprehension)
列表生成是Python2.0中加入的一種語法,可以非常方便地用來生成列表和迭代器,比如上節中map的兩個例子和filter的一個例子可以用列表生成重寫為:
1 2 3 4 5 |
[x**2 for x in [1, 2, 3, 4]] # [1, 4, 9 16] [sum(x) for x in zip([1, 2, 3], [5, 6, 7])] # [6, 8, 10] [x for x in [1, 2, 3, 4, 5] if x % 2] # [1, 3, 5] |
zip()函式可以把多個列表關聯起來,這個例子中,通過zip()可以按順序同時輸出兩個列表對應位置的元素對。有一點需要注意的是,zip()不會自動幫助判斷兩個列表是否長度一樣,所以最終的結果會以短的列表為準,想要以長的列表為準的話可以考慮itertools模組中的izip_longest()。如果要生成迭代器只需要把方括號換成括號,生成字典也非常容易:
1 2 3 4 5 |
iter_odd = (x for x in [1, 2, 3, 4, 5] if x % 2) print(type(iter_odd)) # <type 'generator'> square_dict = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} |
至於列表生成和map/filter應該優先用哪種,這個問題很難回答,不過Python創始人Guido似乎不喜歡map/filter/reduce,他曾在表示過一些從函數語言程式設計裡拿來的特性是個錯誤。
5.2.7 字串
Python中字串相關的處理都非常方便,來看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
a = 'Life is short, you need Python' a.lower() # 'life is short, you need Python' a.upper() # 'LIFE IS SHORT, YOU NEED PYTHON' a.count('i') # 2 a.find('e') # 從左向右查詢'e',3 a.rfind('need') # 從右向左查詢'need',19 a.replace('you', 'I') # 'Life is short, I need Python' tokens = a.split() # ['Life', 'is', 'short,', 'you', 'need', 'Python'] b = ' '.join(tokens) # 用指定分隔符按順序把字串列表組合成新字串 c = a + '\n' # 加了換行符,注意+用法是字串作為序列的用法 c.rstrip() # 右側去除換行符 [x for x in a] # 遍歷每個字元並生成由所有字元按順序構成的列表 'Python' in a # True |
Python2.6中引入了format進行字串格式化,相比在字串中用%的類似C的方式,更加強大方便:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
a = 'I’m like a {} chasing {}.' # 按順序格式化字串,'I’m like a dog chasing cars.' a.format('dog', 'cars') # 在大括號中指定引數所在位置 b = 'I prefer {1} {0} to {2} {0}' b.format('food', 'Chinese', 'American') # >代表右對齊,>前是要填充的字元,依次輸出: # 000001 # 000019 # 000256 for i in [1, 19, 256]: print('The index is {:0>6d}'.format(i)) # <代表左對齊,依次輸出: # *--------- # ****------ # *******--- for x in ['*', '****', '*******']: progress_bar = '{:-<10}'.format(x) print(progress_bar) for x in [0.0001, 1e17, 3e-18]: print('{:.6f}'.format(x)) # 按照小數點後6位的浮點數格式 print('{:.1e}'.format(x)) # 按照小數點後1位的科學記數法格式 print ('{:g}'.format(x)) # 系統自動選擇最合適的格式 template = '{name} is {age} years old.' c = template.format(name='Tom', age=8)) # Tom is 8 years old. d = template.format(age=7, name='Jerry')# Jerry is 7 years old. |
format在生成字串和文件的時候非常有用,更多更詳細的用法可以參考Python官網:
7.1. string – Common string operations – Python 2.7.13 documentation
5.2.8 檔案操作和pickle
在Python中,推薦用上下文管理器(with-as)來開啟檔案,IO資源的管理更加安全,而且不用老惦記著給檔案執行close()函式。還是舉例子來說明,考慮有個檔案name_age.txt,裡面儲存著名字和年齡的關係,格式如下:
1 2 3 4 |
Tom,8 Jerry,7 Tyke,3 ... |
讀取檔案內容並全部顯示:
1 2 3 4 5 |
with open('name_age.txt', 'r') as f: # 開啟檔案,讀取模式 lines = f.readlines() # 一次讀取所有行 for line in lines: # 按行格式化並顯示資訊 name, age = line.rstrip().split(',') print('{} is {} years old.'.format(name, age)) |
open()的第一個引數是檔名,第二個引數是模式。檔案的模式一般有四種,讀取(r),寫入(w),追加(a)和讀寫(r+)。如果希望按照二進位制資料讀取,則將檔案模式和b一起使用(wb, r+b…)。
再考慮一個場景,要讀取檔案內容,並把年齡和名字的順序交換存成新檔案age_name.txt,這時可以同時開啟兩個檔案:
1 2 3 4 5 6 |
with open('name_age.txt', 'r') as fread, open('age_name.txt', 'w') as fwrite: line = fread.readline() while line: name, age = line.rstrip().split(',') fwrite.write('{},{}\n'.format(age, name)) line = fread.readline() |
有的時候我們進行檔案操作是希望把物件進行序列化,那麼可以考慮用pickle模組:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import pickle lines = [ "I'm like a dog chasing cars.", "I wouldn't know what to do if I caught one...", "I'd just do things." ] with open('lines.pkl', 'wb') as f: # 序列化並儲存成檔案 pickle.dump(lines, f) with open('lines.pkl', 'rb') as f: # 從檔案讀取並反序列化 lines_back = pickle.load(f) print(lines_back) # 和lines一樣 |
注意到,序列化的時候就得使用b模式了。Python2中有個效率更高的pickle叫cPickle,用法和pickle一樣,在Python3中就只有一個pickle。
5.2.9 異常
相比起其他一些語言,在Python中我們可以更大膽地使用異常,因為異常在Python中是非常常見的存在,比如下面這種簡單的遍歷:
1 2 3 4 |
a = ['Why', 'so', 'serious', '?'] for x in a: print(x) |
當用for進行遍歷時,會對要遍歷的物件呼叫iter()。這需要給物件建立一個迭代器用來依次返回物件中的內容。為了能成功呼叫iter(),該物件要麼得支援迭代協議(定義__iter__()),要麼得支援序列協議(定義__getitem__())。當遍歷結束時,__iter__()或者__getitem__()都需要丟擲一個異常。__iter__()會丟擲StopIteration,而__getitem__()會丟擲IndexError,於是遍歷就會停止。
在深度學習中,尤其是資料準備階段,常常遇到IO操作。這時候遇到異常的可能性很高,採用異常處理可以保證資料處理的過程不被中斷,並對有異常的情況進行記錄或其他動作:
1 2 3 4 5 6 7 8 9 10 11 |
for filepath in filelist: # filelist中是檔案路徑的列表 try: with open(filepath, 'r') as f: # 執行資料處理的相關工作 ... print('{} is processed!'.format(filepath)) except IOError: print('{} with IOError!'.format(filepath)) # 異常的相應處理 ... |
5.2.10 多程式(multiprocessing)
深度學習中對資料高效處理常常會需要並行,這時多程式就派上了用場。考慮這樣一個場景,在資料準備階段,有很多檔案需要執行一定的預處理,正好有臺多核伺服器,我們希望把這些檔案分成32份,並行處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
from multiprocessing import Process#, freeze_support def process_data(filelist): for filepath in filelist: print('Processing {} ...'.format(filepath)) # 處理資料 ... if __name__ == '__main__': # 如果是在Windows下,還需要加上freeze_support() #freeze_support() # full_list包含了要處理的全部檔案列表 ... n_total = len(full_list) # 一個遠大於32的數 n_processes = 32 # 每段子列表的平均長度 length = float(n_total) / float(n_processes) # 計算下標,儘可能均勻地劃分輸入檔案列表 indices = [int(round(i*length)) for i in range(n_processes+1)] # 生成每個程式要處理的子檔案列表 sublists = [full_list[indices[i]:indices[i+1]] for i in range(n_processes)] # 生成程式 processes = [Process(target=process_data, args=(x,)) for x in sublists] # 並行處理 for p in processes: p.start() for p in processes: p.join() |
其中if __name__ == ‘__main__’用來標明在import時不包含,但是作為檔案執行時執行的語句塊。為什麼不用多執行緒呢?簡單說就是Python中執行緒的併發無法有效利用多核,如果有興趣的讀者可以從下面這個連結看起:
GlobalInterpreterLock – Python Wiki
5.2.11 os模組
深度學習中的資料多是檔案,所以資料處理階段和檔案相關的操作就非常重要。除了檔案IO,Python中一些作業系統的相關功能也能夠非常方便地幫助資料處理。想象一下我們有一個資料夾叫做data,下邊有3個子資料夾叫做cat,dog和bat,裡面分別是貓,狗和蝙蝠的照片。為了訓練一個三分類模型,我們先要生成一個檔案,裡面每一行是檔案的路徑和對應的標籤。定義cat是0,dog是1,bat是2,則可以通過如下指令碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import os # 定義資料夾名稱和標籤的對應關係 label_map = { 'cat': 0, 'dog': 1, 'bat': 2 } with open('data.txt', 'w') as f: # 遍歷所有檔案,root為當前資料夾,dirs是所有子資料夾名,files是所有檔名 for root, dirs, files in os.walk('data'): for filename in files: filepath = os.sep.join([root, filename]) # 獲得檔案完整路徑 dirname = root.split(os.sep)[-1] # 獲取當前資料夾名稱 label = label_map[dirname] # 得到標籤 line = '{},{}\n'.format(filepath, label) f.write(line) |
其中,os.sep是當前作業系統的路徑分隔符,在Unix/Linux中是’/’,Windows中是’\\’。有的時候我們已經有了所有的檔案在一個資料夾data下,希望獲取所有檔案的名稱,則可以用os.listdir():
1 |
filenames = os.listdir('data') |
os也提供了諸如拷貝,移動和修改檔名等操作。同時因為大部分深度學習框架最常見的都是在Unix/Linux下使用,並且Unix/Linux的shell已經非常強大(比Windows好用太多),所以只需要用字串格式化等方式生成shell命令的字串,然後通過os.system()就能方便實現很多功能,有時比os,還有Python中另一個作業系統相關模組shutil還要方便:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import os, shutil filepath0 = 'data/bat/IMG_000001.jpg' filepath1 = 'data/bat/IMG_000000.jpg' # 修改檔名 os.system('mv {} {}'.format(filepath0, filepath1)) #os.rename(filepath0, filepath1) # 建立資料夾 dirname = 'data_samples' os.system('mkdir -p {}'.format(dirname)) #if not os.path.exists(dirname): # os.mkdir(dirname) # 拷貝檔案 os.system('cp {} {}'.format(filepath1, dirname)) #shutil.copy(filepath1, dirname) |