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)
|
【. . . . . .本博客僅作個人生活、工作、學習等的日常記錄。說明: (1) 內容有參考其他博主、網頁等,有因“懶”直接粘貼來,會備注出處。若遇雷同,或忘備注,並無故意抄襲之意,請諸“原主”諒解,很感謝您的辛勤"筆記"可供本人參考學習。 (2) 如遇同行,有參考學習者,因個人學識有限,不保證所寫內容完全正確。您對本博文有任何的意見或建議,歡迎留言,感謝指正。 (3) 若您認為本主的全博客還不錯,可以點擊關注,便於互相學習。 (4) 感謝您的閱讀,希望對您有一定的幫助。歡迎轉載或分享,但請注明出處,謝謝。. . . . .
給深度學習入門者的Python快速教程 - 基礎篇
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) 感謝您的閱讀,希望對您有一定的幫助。歡迎轉載或分享,但請注明出處,謝謝。. . . . . .】
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)
|