【翻譯】《利用Python進行數據分析·第2版》第3章(下)Python的數據結構、函數和文件

作者:SeanCheney Python愛好者社區專欄作者

簡書專欄:jianshu.com/u/130f76596

前文傳送門:

【翻譯】《利用Python進行數據分析·第2版》第1章 準備工作

【翻譯】《利用Python進行數據分析·第2版》第2章(上)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(中)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(下)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第3章(上)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第3章(中)Python的數據結構、函數和文件

3.2 函數

函數是Python中最主要也是最重要的代碼組織和復用手段。作為最重要的原則,如果你要重複使用相同或非常類似的代碼,就需要寫一個函數。通過給函數起一個名字,還可以提高代碼的可讀性。

函數使用def關鍵字聲明,用return關鍵字返回值:

def my_function(x, y, z=1.5):

if z > 1:

return z * (x + y)

else:

return z / (x + y)

同時擁有多條return語句也是可以的。如果到達函數末尾時沒有遇到任何一條return語句,則返回None。

函數可以有一些位置參數(positional)和一些關鍵字參數(keyword)。關鍵字參數通常用於指定默認值或可選參數。在上面的函數中,x和y是位置參數,而z則是關鍵字參數。也就是說,該函數可以下面這兩種方式進行調用:

my_function(5, 6, z=0.7)

my_function(3.14, 7, 3.5)

my_function(10, 20)

函數參數的主要限制在於:關鍵字參數必須位於位置參數(如果有的話)之後。你可以任何順序指定關鍵字參數。也就是說,你不用死記硬背函數參數的順序,只要記得它們的名字就可以了。

筆記:也可以用關鍵字傳遞位置參數。前面的例子,也可以寫為:

my_function(x=5, y=6, z=7)

my_function(y=6, x=5, z=7)

這種寫法可以提高可讀性。

命名空間、作用域,和局部函數

函數可以訪問兩種不同作用域中的變數:全局(global)和局部(local)。Python有一種更科學的用於描述變數作用域的名稱,即命名空間(namespace)。任何在函數中賦值的變數默認都是被分配到局部命名空間(local namespace)中的。局部命名空間是在函數被調用時創建的,函數參數會立即填入該命名空間。在函數執行完畢之後,局部命名空間就會被銷毀(會有一些例外的情況,具體請參見後面介紹閉包的那一節)。看看下面這個函數:

def func():

a = []

for i in range(5):

a.append(i)

調用func()之後,首先會創建出空列表a,然後添加5個元素,最後a會在該函數退出的時候被銷毀。假如我們像下面這樣定義a:

a = []

def func():

for i in range(5):

a.append(i)

雖然可以在函數中對全局變數進行賦值操作,但是那些變數必須用global關鍵字聲明成全局的才行:

In [168]: a = None

In [169]: def bind_a_variable():

.....: global a

.....: a = []

.....: bind_a_variable()

.....:

In [170]: print(a)

[]

注意:我常常建議人們不要頻繁使用global關鍵字。因為全局變數一般是用於存放系統的某些狀態的。如果你發現自己用了很多,那可能就說明得要來點兒面向對象編程了(即使用類)。

返回多個值

在我第一次用Python編程時(之前已經習慣了Java和C++),最喜歡的一個功能是:函數可以返回多個值。下面是一個簡單的例子:

def f():

a = 5

b = 6

c = 7

return a, b, c

a, b, c = f()

在數據分析和其他科學計算應用中,你會發現自己常常這麼干。該函數其實只返回了一個對象,也就是一個元組,最後該元組會被拆包到各個結果變數中。在上面的例子中,我們還可以這樣寫:

return_value = f()

這裡的return_value將會是一個含有3個返回值的三元元組。此外,還有一種非常具有吸引力的多值返回方式——返回字典:

def f():

a = 5

b = 6

c = 7

return {a : a, b : b, c : c}

取決於工作內容,第二種方法可能很有用。

函數也是對象

由於Python函數都是對象,因此,在其他語言中較難表達的一些設計思想在Python中就要簡單很多了。假設我們有下面這樣一個字元串數組,希望對其進行一些數據清理工作並執行一堆轉換:

In [171]: states = [ Alabama , Georgia!, Georgia, georgia, FlOrIda,

.....: south carolina##, West virginia?

不管是誰,只要處理過由用戶提交的調查數據,就能明白這種亂七八糟的數據是怎麼一回事。為了得到一組能用於分析工作的格式統一的字元串,需要做很多事情:去除空白符、刪除各種標點符號、正確的大寫格式等。做法之一是使用內建的字元串方法和正則表達式re模塊:

import re

def clean_strings(strings):

result = []

for value in strings:

value = value.strip()

value = re.sub([!#?], , value)

value = value.title()

result.append(value)

return result

結果如下所示:

In [173]: clean_strings(states)

Out[173]:

[Alabama,

Georgia,

Georgia,

Georgia,

Florida,

South Carolina,

West Virginia]

其實還有另外一種不錯的辦法:將需要在一組給定字元串上執行的所有運算做成一個列表:

def remove_punctuation(value):

return re.sub([!#?], , value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):

result = []

for value in strings:

for function in ops:

value = function(value)

result.append(value)

return result

然後我們就有了:

In [175]: clean_strings(states, clean_ops)

Out[175]:

[Alabama,

Georgia,

Georgia,

Georgia,

Florida,

South Carolina,

West Virginia]

這種多函數模式使你能在很高的層次上輕鬆修改字元串的轉換方式。此時的clean_strings也更具可復用性!

還可以將函數用作其他函數的參數,比如內置的map函數,它用於在一組數據上應用一個函數:

In [176]: for x in map(remove_punctuation, states):

.....: print(x)

Alabama

Georgia

Georgia

georgia

FlOrIda

south carolina

West virginia

匿名(lambda)函數

Python支持一種被稱為匿名的、或lambda函數。它僅由單條語句組成,該語句的結果就是返回值。它是通過lambda關鍵字定義的,這個關鍵字沒有別的含義,僅僅是說「我們正在聲明的是一個匿名函數」。

def short_function(x):

return x * 2

equiv_anon = lambda x: x * 2

本書其餘部分一般將其稱為lambda函數。它們在數據分析工作中非常方便,因為你會發現很多數據轉換函數都以函數作為參數的。直接傳入lambda函數比編寫完整函數聲明要少輸入很多字(也更清晰),甚至比將lambda函數賦值給一個變數還要少輸入很多字。看看下面這個簡單得有些傻的例子:

def apply_to_list(some_list, f):

return [f(x) for x in some_list]

ints = [4, 0, 1, 5, 6]

apply_to_list(ints, lambda x: x * 2)

雖然你可以直接編寫[x *2for x in ints],但是這裡我們可以非常輕鬆地傳入一個自定義運算給apply_to_list函數。

再來看另外一個例子。假設有一組字元串,你想要根據各字元串不同字母的數量對其進行排序:

In [177]: strings = [foo, card, bar, aaaa, abab]

這裡,我們可以傳入一個lambda函數到列表的sort方法:

In [178]: strings.sort(key=lambda x: len(set(list(x))))

In [179]: strings

Out[179]: [aaaa, foo, abab, bar, card]

筆記:lambda函數之所以會被稱為匿名函數,與def聲明的函數不同,原因之一就是這種函數對象本身是沒有提供名稱name屬性。

柯里化:部分參數應用

柯里化(currying)是一個有趣的計算機科學術語,它指的是通過「部分參數應用」(partial argument application)從現有函數派生出新函數的技術。例如,假設我們有一個執行兩數相加的簡單函數:

def add_numbers(x, y):

return x + y

通過這個函數,我們可以派生出一個新的只有一個參數的函數——add_five,它用於對其參數加5:

add_five = lambda y: add_numbers(5, y)

add_numbers的第二個參數稱為「柯里化的」(curried)。這裡沒什麼特別花哨的東西,因為我們其實就只是定義了一個可以調用現有函數的新函數而已。內置的functools模塊可以用partial函數將此過程簡化:

from functools import partial

add_five = partial(add_numbers, 5)

生成器

能以一種一致的方式對序列進行迭代(比如列表中的對象或文件中的行)是Python的一個重要特點。這是通過一種叫做迭代器協議(iterator protocol,它是一種使對象可迭代的通用方式)的方式實現的,一個原生的使對象可迭代的方法。比如說,對字典進行迭代可以得到其所有的鍵:

In [180]: some_dict = {a: 1, b: 2, c: 3}

In [181]: for key in some_dict:

.....: print(key)

a

b

c

當你編寫for key in some_dict時,Python解釋器首先會嘗試從some_dict創建一個迭代器:

In [182]: dict_iterator = iter(some_dict)

In [183]: dict_iterator

Out[183]: <dict_keyiterator at 0x7fbbd5a9f908>

迭代器是一種特殊對象,它可以在諸如for循環之類的上下文中向Python解釋器輸送對象。大部分能接受列表之類的對象的方法也都可以接受任何可迭代對象。比如min、max、sum等內置方法以及list、tuple等類型構造器:

In [184]: list(dict_iterator)Out[184]: [a, b, c]

生成器(generator)是構造新的可迭代對象的一種簡單方式。一般的函數執行之後只會返回單個值,而生成器則是以延遲的方式返回一個值序列,即每返回一個值之後暫停,直到下一個值被請求時再繼續。要創建一個生成器,只需將函數中的return替換為yeild即可:

def squares(n=10):

print(Generating squares from 1 to {0}.format(n ** 2))

for i in range(1, n + 1):

yield i ** 2

調用該生成器時,沒有任何代碼會被立即執行:

In [186]: gen = squares()

In [187]: gen

Out[187]: <generator object squares at 0x7fbbd5ab4570>

直到你從該生成器中請求元素時,它才會開始執行其代碼:

In [188]: for x in gen:

.....: print(x, end= )

Generating squares from 1 to 100

1 4 9 16 25 36 49 64 81 100

生成器表達式

另一種更簡潔的構造生成器的方法是使用生成器表達式(generator expression)。這是一種類似於列表、字典、集合推導式的生成器。其創建方式為,把列表推導式兩端的方括弧改成圓括弧:

In [189]: gen = (x ** 2 for x in range(100))

In [190]: gen

Out[190]: <generator object <genexpr> at 0x7fbbd5ab29e8>

它跟下面這個冗長得多的生成器是完全等價的:

def _make_gen():

for x in range(100):

yield x ** 2

gen = _make_gen()

生成器表達式也可以取代列表推導式,作為函數參數:

In [191]: sum(x ** 2 for x in range(100))

Out[191]: 328350

In [192]: dict((i, i **2) for i in range(5))

Out[192]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

itertools模塊

標準庫itertools模塊中有一組用於許多常見數據演算法的生成器。例如,groupby可以接受任何序列和一個函數。它根據函數的返回值對序列中的連續元素進行分組。下面是一個例子:

In [193]: import itertoolsIn [194]: first_letter = lambda x: x[0]In [195]: names = [Alan, Adam, Wes, Will, Albert, Steven]In [196]: for letter, names in itertools.groupby(names, first_letter): .....: print(letter, list(names)) # names is a generatorA [Alan, Adam]W [Wes, Will]A [Albert]S [Steven]

表3-2中列出了一些我經常用到的itertools函數。建議參閱Python官方文檔,進一步學習。

表3-2 一些有用的itertools函數

錯誤和異常處理

優雅地處理Python的錯誤和異常是構建健壯程序的重要部分。在數據分析中,許多函數函數只用於部分輸入。例如,Python的float函數可以將字元串轉換成浮點數,但輸入有誤時,有ValueError錯誤:

In [197]: float(1.2345)

Out[197]: 1.2345

In [198]: float(something)

---------------------------------------------------------------------------

ValueError Traceback (most recent call last)

<ipython-input-198-439904410854> in <module>()

----> 1 float(something)

ValueError: could not convert string to float: something

假如想優雅地處理float的錯誤,讓它返回輸入值。我們可以寫一個函數,在try/except中調用float:

def attempt_float(x):

try:

return float(x)

except:

return x

當float(x)拋出異常時,才會執行except的部分:

In [200]: attempt_float(1.2345)

Out[200]: 1.2345

In [201]: attempt_float(something)

Out[201]: something

你可能注意到float拋出的異常不僅是ValueError:

In [202]: float((1, 2))

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-202-842079ebb635> in <module>()

----> 1 float((1, 2))

TypeError: float() argument must be a string or a number, not tuple

你可能只想處理ValueError,TypeError錯誤(輸入不是字元串或數值)可能是合理的bug。可以寫一個異常類型:

def attempt_float(x):

try:

return float(x)

except ValueError:

return x

然後有:

In [204]: attempt_float((1, 2))

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-204-9bdfd730cead> in <module>()

----> 1 attempt_float((1, 2))

<ipython-input-203-3e06b8379b6b> in attempt_float(x)

1 def attempt_float(x):

2 try:

----> 3 return float(x)

4 except ValueError:

5 return x

TypeError: float() argument must be a string or a number, not tuple

可以用元組包含多個異常:

def attempt_float(x):

try:

return float(x)

except (TypeError, ValueError):

return x

某些情況下,你可能不想抑制異常,你想無論try部分的代碼是否成功,都執行一段代碼。可以使用finally:

f = open(path, w)

try:

write_to_file(f)

finally:

f.close()

這裡,文件處理f總會被關閉。相似的,你可以用else讓只在try部分成功的情況下,才執行代碼:

f = open(path, w)

try:

write_to_file(f)

except:

print(Failed)

else:

print(Succeeded)

finally:

f.close()

IPython的異常

如果是在%run一個腳本或一條語句時拋出異常,IPython默認會列印完整的調用棧(traceback),在棧的每個點都會有幾行上下文:

In [10]: %run examples/ipython_bug.py

---------------------------------------------------------------------------

AssertionError Traceback (most recent call last)

/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()

13 throws_an_exception()

14

---> 15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()

11 def calling_things():

12 works_fine()

---> 13 throws_an_exception()

14

15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()

7 a = 5

8 b = 6

----> 9 assert(a + b == 10)

10

11 def calling_things():

AssertionError:

自身就帶有文本是相對於Python標準解釋器的極大優點。你可以用魔術命令%xmode,從Plain(與Python標準解釋器相同)到Verbose(帶有函數的參數值)控制文本顯示的數量。後面可以看到,發生錯誤之後,(用%debug或%pdb magics)可以進入stack進行事後調試。

3.3 文件和操作系統

本書的代碼示例大多使用諸如pandas.read_csv之類的高級工具將磁碟上的數據文件讀入Python數據結構。但我們還是需要了解一些有關Python文件處理方面的基礎知識。好在它本來就很簡單,這也是Python在文本和文件處理方面的如此流行的原因之一。

為了打開一個文件以便讀寫,可以使用內置的open函數以及一個相對或絕對的文件路徑:

In [207]: path = examples/segismundo.txt

In [208]: f = open(path)

默認情況下,文件是以只讀模式(r)打開的。然後,我們就可以像處理列表那樣來處理這個文件句柄f了,比如對行進行迭代:

for line in f:

pass

從文件中取出的行都帶有完整的行結束符(EOL),因此你常常會看到下面這樣的代碼(得到一組沒有EOL的行):

In [209]: lines = [x.rstrip() for x in open(path)]

In [210]: lines

Out[210]:

[Sue?a el rico en su riqueza,,

que más cuidados le ofrece;,

,

sue?a el pobre que padece,

su miseria y su pobreza;,

,

sue?a el que a medrar empieza,,

sue?a el que afana y pretende,,

sue?a el que agravia y ofende,,

,

y en el mundo, en conclusión,,

todos sue?an lo que son,,

aunque ninguno lo entiende.,

]

如果使用open創建文件對象,一定要用close關閉它。關閉文件可以返回操作系統資源:

In [211]: f.close()

用with語句可以可以更容易地清理打開的文件:

In [212]: with open(path) as f:

.....: lines = [x.rstrip() for x in f]

這樣可以在退出代碼塊時,自動關閉文件。

如果輸入f =open(path,w),就會有一個新文件被創建在examples/segismundo.txt,並覆蓋掉該位置原來的任何數據。另外有一個x文件模式,它可以創建可寫的文件,但是如果文件路徑存在,就無法創建。表3-3列出了所有的讀/寫模式。

表3-3 Python的文件模式

對於可讀文件,一些常用的方法是read、seek和tell。read會從文件返回字元。字元的內容是由文件的編碼決定的(如UTF-8),如果是二進位模式打開的就是原始位元組:

In [213]: f = open(path)

In [214]: f.read(10)

Out[214]: Sue?a el r

In [215]: f2 = open(path, rb) # Binary mode

In [216]: f2.read(10)

Out[216]: bSuexc3xb1a el

read模式會將文件句柄的位置提前,提前的數量是讀取的位元組數。tell可以給出當前的位置:

In [217]: f.tell()

Out[217]: 11

In [218]: f2.tell()

Out[218]: 10

儘管我們從文件讀取了10個字元,位置卻是11,這是因為用默認的編碼用了這麼多位元組才解碼了這10個字元。你可以用sys模塊檢查默認的編碼:

In [219]: import sys

In [220]: sys.getdefaultencoding()

Out[220]: utf-8

seek將文件位置更改為文件中的指定位元組:

In [221]: f.seek(3)

Out[221]: 3

In [222]: f.read(1)

Out[222]: ?

最後,關閉文件:

In [223]: f.close()

In [224]: f2.close()

向文件寫入,可以使用文件的write或writelines方法。例如,我們可以創建一個無空行版的prof_mod.py:

In [225]: with open(tmp.txt, w) as handle:

.....: handle.writelines(x for x in open(path) if len(x) > 1)

In [226]: with open(tmp.txt) as f:

.....: lines = f.readlines()

In [227]: lines

Out[227]:

[Sue?a el rico en su riqueza,
,

que más cuidados le ofrece;
,

sue?a el pobre que padece
,

su miseria y su pobreza;
,

sue?a el que a medrar empieza,
,

sue?a el que afana y pretende,
,

sue?a el que agravia y ofende,
,

y en el mundo, en conclusión,
,

todos sue?an lo que son,
,

aunque ninguno lo entiende.
]

表3-4列出了一些最常用的文件方法。

表3-4 Python重要的文件方法或屬性

文件的位元組和Unicode

Python文件的默認操作是「文本模式」,也就是說,你需要處理Python的字元串(即Unicode)。它與「二進位模式」相對,文件模式加一個b。我們來看上一節的文件(UTF-8編碼、包含非ASCII字元):

In [230]: with open(path) as f:

.....: chars = f.read(10)

In [231]: chars

Out[231]: Sue?a el r

UTF-8是長度可變的Unicode編碼,所以當我從文件請求一定數量的字元時,Python會從文件讀取足夠多(可能少至10或多至40位元組)的位元組進行解碼。如果以「rb」模式打開文件,則讀取確切的請求位元組數:

In [232]: with open(path, rb) as f:

.....: data = f.read(10)

In [233]: data

Out[233]: bSuexc3xb1a el

取決於文本的編碼,你可以將位元組解碼為str對象,但只有當每個編碼的Unicode字元都完全成形時才能這麼做:

In [234]: data.decode(utf8)

Out[234]: Sue?a el

In [235]: data[:4].decode(utf8)

---------------------------------------------------------------------------

UnicodeDecodeError Traceback (most recent call last)

<ipython-input-235-300e0af10bb7> in <module>()

----> 1 data[:4].decode(utf8)

UnicodeDecodeError: utf-8 codec cant decode byte 0xc3 in position 3: unexpecte

d end of data

文本模式結合了open的編碼選項,提供了一種更方便的方法將Unicode轉換為另一種編碼:

In [236]: sink_path = sink.txt

In [237]: with open(path) as source:

.....: with open(sink_path, xt, encoding=iso-8859-1) as sink:

.....: sink.write(source.read())

In [238]: with open(sink_path, encoding=iso-8859-1) as f:

.....: print(f.read(10))

Sue?a el r

注意,不要在二進位模式中使用seek。如果文件位置位於定義Unicode字元的位元組的中間位置,讀取後面會產生錯誤:

In [240]: f = open(path)

In [241]: f.read(5)

Out[241]: Sue?a

In [242]: f.seek(4)

Out[242]: 4

In [243]: f.read(1)

---------------------------------------------------------------------------

UnicodeDecodeError Traceback (most recent call last)

<ipython-input-243-7841103e33f5> in <module>()

----> 1 f.read(1)

/miniconda/envs/book-env/lib/python3.6/codecs.py in decode(self, input, final)

319 # decode input (taking the buffer into account)

320 data = self.buffer + input

--> 321 (result, consumed) = self._buffer_decode(data, self.errors, final

)

322 # keep undecoded input until the next call

323 self.buffer = data[consumed:]

UnicodeDecodeError: utf-8 codec cant decode byte 0xb1 in position 0: invalid s

tart byte

In [244]: f.close()

如果你經常要對非ASCII字元文本進行數據分析,通曉Python的Unicode功能是非常重要的。更多內容,參閱Python官方文檔。

3.4 結論

我們已經學過了Python的基礎、環境和語法,接下來學習NumPy和Python的面向數組計算。


推薦閱讀:

就是它了-結合自己興趣與事業發展的新方向
小白python之路的開啟
數據之美
吳恩達deep learning中實現CNN的小作業
誰在被取代

TAG:Python | Python教程 | 數據分析 |