給深度學習入門者的Python快速教程 - 基礎篇


 

實在搞不定博客園的排版,排版更佳的版本在:

https://zhuanlan.zhihu.com/p/24162430 

 

 

Life is short, you need Python

人生苦短,我用Python

-- Bruce Eckel

5.1 Python簡介

本章將介紹Python的最基本語法,以及一些和深度學習還有計算機視覺最相關的基本使用。

5.1.1 Python簡史

Python是一門解釋型的高級編程語言,特點是簡單明確。Python作者是荷蘭人Guido van Rossum1982年他獲得數學和計算機碩士學位后,在荷蘭數學與計算科學研究所(Centrum Wiskunde & Informatica, CWI)謀了份差事。在CWI期間,Guido參與到了一門叫做ABC的語言開發工作中。ABC是一門教學語言,所以擁有簡單,可讀性好,語法更接近自然語言等特點。在那個C語言一統天下的年代,ABC就是一股簡單的清流。不過畢竟是門教學語言,最后沒有流行起來,不過這段經歷影響了Guido1989年的聖誕假期,閑得蛋疼的Guido決定設計一門簡單易用的新語言,要介於CShell之間,同時吸取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哲學的做法叫做很PythonPythonic),具體參見:

https://www.python.org/dev/peps/pep-0020/

或者在Python中執行:

>> import this

Python擁有很好的擴充性,可以非常輕松地用其他語言編寫模塊供調用,用Python編寫的模塊也可以通過各種方式輕松被其他語言調用。所以一種常見的Python使用方式是,底層復雜且對效率要求高的模塊用C/C++等語言實現,頂層調用的APIPython封裝,這樣可以通過簡單的語法實現頂層邏輯,故而Python又被稱為“膠水語言”。這種特性的好處是,無需花費很多時間在編程實現上,更多的時間可以專注於思考問題的邏輯。尤其是對做算法和深度學習的從業人員,這種方式是非常理想的,所以如今的深度學習框架中,除了MATLAB,或是Deeplearning4j這種擺明了給Java用的,其他框架基本上要么官方接口就是Python,要么支持Python接口。

5.1.2 安裝和使用Python

Python有兩個大版本,考慮到用戶群數量和庫的各種框架的兼容性,本書以Python22.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

https://www.continuum.io/downloads

Windows下的Python安裝簡單一些,從官方網站下載相應的安裝程序就可以了,當然也有更方便的已經包含了很全的第三方庫的選擇,WinPython

http://winpython.github.io/

並且是綠色的,直接執行就可以用了。

5.2 Python基本語法

There should be one-- and preferably only one --obvious way to do it.

對於一個特定的問題,應該只用最好的一種方法來解決。

-- Tim Peters

5.2.1 基本數據類型和運算

基本數據類型

Python中最基本的數據類型包括整型,浮點數,布爾值和字符串。類型是不需要聲明的,比如:

a = 1       # 整數

b = 1.2     # 浮點數

c = True    # 布爾類型

d = "False" # 字符串

e = None    # NoneType

其中#是行內注釋的意思。最后一個NoneNoneType,注意不是0,在Python中利用type函數可以查看一個變量的類型:

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中基本變量的賦值一般建立的是個引用,比如下面的語句:

a = 1

b = a

c = 1

a賦值為1后,b=a執行時並不會將a的值復制一遍,然后賦給b,而是簡單地為a所指的值,也就是1建立了一個引用,相當於ab都是指向包含1這個值的這塊內存的指針。所以c=1執行的也是個引用建立,這三個變量其實是三個引用,指向同一個值。這個邏輯雖然簡單,不過也還是常常容易弄混,這沒關系,Python內置了id函數,可以返回一個對象的地址,用id函數可以讓我們知道每個變量指向的是不是同一個值:

id(a)   # 35556792L

id(b)   # 35556792L

id(c)   # 35556792L

注釋中表示的仍是執行后的結果。如果這時候我們接下面兩個語句:

b = 2   # b的引用到新的一個變量上

id(b)   # 35556768L

可以看到b引用到了另一個變量上。

 

運算符

Python中的數值的基本運算和C差不多,字符串的運算更方便,下面是常見的例子:

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          # ac次方,結果為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)字符串用雙引號和單引號都可以,區別主要是單引號字符串中如果出現單引號字符則需要用轉義符,雙引號也是一樣,所以在單引號字符串中使用雙引號,或者雙引號字符串中使用單引號就會比較方便。另外三個雙引號或者三個單引號圍起來的也是字符串,因為換行方便,更多用於文檔。2Python2中兩個數值相除會根據數值類型判斷是否整數除法,Python3種則都按照浮點數。想要在Python2種也執行Python3中的除法只要執行下面語句:

from __future__ import division     # 使用Python3中的除法

1 / 2                               # 0.5

3)字符串前加r表示字符串內容嚴格按照輸入的樣子,好處是不用轉義符了,非常方便。

Python中的布爾值和邏輯的運算非常直接,下面是例子:

a = True

b = False

a and b     # False

a or b      # True

not a       # False

基本上就是英語,操作符優先級之類的和其他語言類似。Python中也有位操作:

~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判斷兩個變量是否指向一個地址,看下面的代碼例子:

a = 1

b = 1.0

c = 1

a ==# True,值相等

a is# False,指向的不是一個對象,這個語句等效於 id(a) == id(b)

a is# True,指向的都是整型值1

所以一定要分清要比較的對象應該用那種方式,對於一些特殊的情況,比如None,本着Pythonic的原則,最好用is None / is not None

 

注意關鍵字

Python中,萬物皆對象。不過這並不是這里要探討的話題,想說的是一定要注意關鍵字,因為所有東西都是對象,所以一個簡簡單單的賦值操作就可以把系統內置的函數給變成一個普通變量,來看下邊例子:

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的用法其實是下邊這樣:

print "Hello world!"

這么用主要是受到ABC語法的影響,但這個用法並不Pythonic,后來加入了print函數,為了兼容允許兩種用法並存。所以單純給print賦值是不靈的,在Python2中使用Python3中的一些特性都是用from __future__ import來實現。

 

模塊導入

因為提到了對象名覆蓋和import,所以簡單講一下。import是利用Python中各種強大庫的基礎,比如要計算cos(π)的值,可以有下面4種方式:

# 直接導入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)。這些結構和其他語言中的類似結構並無本質不同,來看例子了解下使用:

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")            # 利用list函數從任何可遍歷結構初始化

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()初始化,輸入參數需要是一個可以遍歷的結構,其中每一個元素會作為列表的一項。“*”操作符對於列表而言是復制,最后一個語句用這種辦法生成了分段的列表。

列表的基本操作有訪問,增加,刪除,拼接,子序列和倒序等:

a = [1, 2, 3, 4]

a.pop()             # 把最后一個值4從列表中移除並作為pop的返回值

4 in            # 判斷4是否在a中,False

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()         # 倒序,返回值為0a變為[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],此類方法叫做slicing

c = a[2:-2]         # 下標也可以很方便地倒着數, -1對應最后一個元素,[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)             # 列表內排序,返回值為Nonea變為[0, 1, 1, 2, 2, 3, 3]

因為列表是有順序的,所以和順序相關的操作是列表中最常見的,首先我們來打亂一個列表的順序,然后再對這個列表排序:

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]

其中range函數是一個可以生成等差數列的函數,比如代碼中生成了0-910個元素的列表。

 

元組

元組和列表有很多相似的地方,最大的區別在於不可變,還有如果初始化只包含一個元素的tuple和列表不一樣,因為語法必須明確,所以必須在元素后加上逗號。另外直接用逗號分隔多個元素賦值默認是個tuple,這在函數多返回值的時候很好用:

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)

 

集合

集合是一種很有用的數學操作,比如列表去重,或是理清兩組數據之間的關系,集合的操作符和位操作符有交集,注意不要弄混:

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)映射結構,鍵無重復,一個鍵不能對應多個值,不過多個鍵可以指向一個值。還是通過例子來了解,構建一個名字à年齡的字典,並執行一些常見操作:

a = {'Tom': 8, 'Jerry': 7}

print(a['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()可以做到這件事,接上段代碼繼續:

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()可以把字典中的鍵值對轉化成一個列表,其中每個元素是一個tupletuple的第一個元素是鍵,第二個元素是值。變量c是按照值排序,所以需要一個操作符itemgetter,去位置為1的元素作為排序參考,如果直接對字典排序,則其實相當於只是對鍵排序。字典被當作一個普通的可遍歷結構使用時,都相當於遍歷字典的鍵。如果覺得字典沒有順序不方便,可以考慮使用OrderedDict,使用方式如下:

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)])

這樣初始化時的順序就保留了,除了有序的特性以外,用法上和字典沒有區別。20169月,Guido宣布在Python3.6中,字典將默認有序,這樣就不用糾結了。另外需要注意的一點是字典是通過哈希表實現的,所以鍵必須是可哈希的, list不能被哈希,所以也不能作為字典的鍵,而tuple就可以。

因為上上段代碼中用到了iteritems(),所以這里順帶提一下迭代器(iterator),迭代器相當於一個函數,每次調用都返回下一個元素,從遍歷的角度來看就和列表沒有區別了。iteritems()就是一個迭代器,所以效果一樣,區別是迭代器占用更少內存,因為不需要一上來就生成整個列表。一般來說,如果只需要遍歷一次,用迭代器是更好的選擇,若是要多次頻繁從一個可遍歷結構中取值,且內存夠,則直接生成整個列表會更好。當然,用迭代器生成一個完整列表並不麻煩,所以有個趨勢是把迭代器作為默認的可遍歷方式,比如前面我們使用過用來生成等差數列列表的range(),在Python2中對應的迭代器形式是xrange()。在Python3中,range()就不再產生一個列表了,而是作為迭代器,xrange()直接沒了。

5.2.3 分支和循環

從這節開始,代碼就未必適合在Python終端中輸入了,選個順手的編輯器或者IDE。作者良心推薦PyCharm,雖然慢,但好用,社區版免費:

https://www.jetbrains.com/pycharm/

 

for循環

上面提到的4種容器類型都是可遍歷的,所以該講講用來遍歷的for循環了。for循環的語法也是簡單的英語:

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)

 

# 依次輸出09

for i in range(10):

    print(i)

注意到每個for循環中,print都有縮進,這是Python中一個讓人愛恨交織的特點:強行縮進來表明成塊的代碼。這樣做的好處是代碼十分清晰工整,還有助於防止寫出過長的函數或者過深的嵌套,壞處是有時候不知為什么tab和空格就一起出現了,又或是多重if-else不知怎得就沒對齊,還是挺麻煩的。

回到for循環上,這種把每個元素拿出來的遍歷方式叫做for_each風格,熟悉Java的話就不會陌生,C++11中也開始支持這種for循環方式。不過如果還是需要下標呢?比如遍歷一個list的時候,希望對應下標也打印出來,這時可以用enumerate

names = ["Rick", "Daryl", "Glenn"]

 

# 依次輸出下標和名字

for i, name in enumerate(names):

    print(i, name)

需要注意的是,通過取下標遍歷當然是可行的,比如用len()函數獲得列表長度,然后用range()/xrange()函數獲得下標,但是並不推薦這樣做:

words = ["This", "is", "not", "recommended"]

 

# not pythonic :(

for i in xrange(len(words)):

    print(words[i])

在使用for循環時,有時會遇到這樣一種場景:我們需要對遍歷的每個元素進行某種判斷,如果符合這種判斷的情況沒有發生,則執行一個操作。舉個例子某神秘部門要審核一個字符串列表,如果沒有發現不和諧的字眼,則將內容放心通過,一種解決辦法是下面這樣:

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執行指定操作:

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語句了,所以這部分先講講ifPython的條件控制主要是三個關鍵字:if-elif-else,其中elif就是else if的意思。還是看例子:

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的組合,要么可以考慮字典:

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表達式中的小技巧

通過鏈式比較讓語句簡潔:

if -1 < x < 1: # 相較於 if x > -1 and x < 1:

    print('The absolute value of x is < 1')

判斷一個值是不是等於多個可能性中的一個:

if x in ['piano', 'violin']:    # 相較於 if x == 'piano' or x == 'violin':

    print("It's an instrument! ")

Python中的對象都會關聯一個真值,所以在if表達式中判斷是否為False或者是否為空的時候,是無需寫出明確的表達式的:

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++中要明確得多,用法如下:

i = 0

while i < 100: # 100

    print("ha")

 

while True:     # 一直笑

    print("ha")

5.2.4 函數、生成器和類

函數

還是從幾個例子看起:

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, 2, 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)處,我們用不同的動作控制小機器人移動:

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)             # 打印當前位置坐標

不同條件下對應的是對坐標這個列表中的值的操作,單純的從字典取值就辦不到了,所以就把函數作為字典的值,然后用這個得到的值執行相應動作:

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中的itemgetteritemgetter(1)得到的是一個可調用對象(callable object),和返回下標為1的元素的函數用起來是一樣的:

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表達式取名字也是一點問題沒有的:

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再繼續:

# 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異常:

a = fibonacci(3)

print(next(a)) # 1

print(next(a)) # 1

print(next(a)) # 2

print(next(a)) # 拋出StopIteration異常

Python3.3以上可以允許yieldreturn同時使用,return的是異常的說明信息:

# 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中的類的概念和其他語言相比沒什么不同,比較特殊的是protectedprivatePython中是沒有明確限制的,一個慣例是用單下划線開頭的表示protected,用雙下划線開頭的表示private

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中的繼承也非常簡單,最基本的繼承方式就是定義類的時候把父類往括號里一放就行了:

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, reducefilter

map可以用於對可遍歷結構的每個元素執行同樣的操作,批量操作:

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)成一個對象:

reduce(lambda x, y: x + y, [1, 2, 3, 4])    # ((1+2)+3)+4=10

filter顧名思義,根據條件對可遍歷結構進行篩選:

filter(lambda x: x % 2, [1, 2, 3, 4, 5])    # 篩選奇數,[1, 3, 5]

需要注意的是,對於filtermap,在Python2中返回結果是列表,Python3中是生成器。

5.2.6 列表生成(list comprehension

列表生成是Python2.0中加入的一種語法,可以非常方便地用來生成列表和迭代器,比如上節中map的兩個例子和filter的一個例子可以用列表生成重寫為:

[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]

如果要生成迭代器只需要把方括號換成括號,生成字典也非常容易:

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中字符串相關的處理都非常方便,來看例子:

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的方式,更加強大方便:

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官網:

https://docs.python.org/2/library/string.html#format-specification-mini-language

5.2.8 文件操作和pickle

Python中,推薦用上下文管理器(with-as)來打開文件,IO資源的管理更加安全,而且不用老惦記着給文件執行close()函數。還是舉例子來說明,考慮有個文件name_age.txt,里面存儲着名字和年齡的關系,格式如下:

Tom,8

Jerry,7

Tyke,3

...

讀取文件內容並全部顯示:

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,這時可以同時打開兩個文件:

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模塊:

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中有個效率更高的picklecPickle,用法和pickle一樣,在Python3中就只有一個pickle

5.2.9 異常

相比起其他一些語言,在Python中我們可以更大膽地使用異常,因為異常在Python中是非常常見的存在,比如下面這種簡單的遍歷:

a = ['Why', 'so', 'serious', '?']

 

for x in a:

    print(x)

當用for進行遍歷時,會對要遍歷的對象調用iter()。這需要給對象創建一個迭代器用來依次返回對象中的內容。為了能成功調用iter(),該對象要么得支持迭代協議(定義__iter__()),要么得支持序列協議(定義__getitem__())。當遍歷結束時,__iter__()或者__getitem__()都需要拋出一個異常。__iter__()會拋出StopIteration,而__getitem__()會拋出IndexError,於是遍歷就會停止。

在深度學習中,尤其是數據准備階段,常常遇到IO操作。這時候遇到異常的可能性很高,采用異常處理可以保證數據處理的過程不被中斷,並對有異常的情況進行記錄或其他動作:

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份,並行處理:

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+1)*length)) for i in range(-1, n_processes)]

   

    # 生成每個進程要處理的子文件列表

    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中線程的並發無法有效利用多核,如果有興趣的讀者可以從下面這個鏈接看起:

https://wiki.python.org/moin/GlobalInterpreterLock

5.2.11 os模塊

深度學習中的數據多是文件,所以數據處理階段和文件相關的操作就非常重要。除了文件IOPython中一些操作系統的相關功能也能夠非常方便地幫助數據處理。想象一下我們有一個文件夾叫做data,下邊有3個子文件夾叫做catdogbat,里面分別是貓,狗和蝙蝠的照片。為了訓練一個三分類模型,我們先要生成一個文件,里面每一行是文件的路徑和對應的標簽。定義cat0dog1bat2,則可以通過如下腳本:

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()

filenames = os.listdir('data')

os也提供了諸如拷貝,移動和修改文件名等操作。同時因為大部分深度學習框架最常見的都是在Unix/Linux下使用,並且Unix/Linuxshell已經非常強大(比Windows好用太多),所以只需要用字符串格式化等方式生成shell命令的字符串,然后通過os.system()就能方便實現很多功能,有時比os,還有Python中另一個操作系統相關模塊shutil還要方便:

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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM