【翻譯】《利用Python進行數據分析·第2版》第3章(下)Python的數據結構、函數和文件
作者:SeanCheney Python愛好者社區專欄作者
簡書專欄:https://www.jianshu.com/u/130f76596b02
前文傳送門:
【翻譯】《利用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, ca, 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 GeorgiaGeorgiageorgiaFlOrIdasouth carolinaWest virginia匿名(lambda)函數
Python支持一種被稱為匿名的、或lambda函數。它僅由單條語句組成,該語句的結果就是返回值。它是通過lambda關鍵字定義的,這個關鍵字沒有別的含義,僅僅是說「我們正在聲明的是一個匿名函數」。
def short_function(x):
return x * 2equiv_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]: stringsOut[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)abc當你編寫for key in some_dict時,Python解釋器首先會嘗試從some_dict創建一個迭代器:
In [182]: dict_iterator = iter(some_dict)
In [183]: dict_iteratorOut[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]: genOut[187]: <generator object squares at 0x7fbbd5ab4570>直到你從該生成器中請求元素時,它才會開始執行其代碼:
In [188]: for x in gen:
.....: print(x, end= )Generating squares from 1 to 1001 4 9 16 25 36 49 64 81 100生成器表達式
另一種更簡潔的構造生成器的方法是使用生成器表達式(generator expression)。這是一種類似於列表、字典、集合推導式的生成器。其創建方式為,把列表推導式兩端的方括弧改成圓括弧:
In [189]: gen = (x ** 2 for x in range(100))
In [190]: genOut[190]: <generator object <genexpr> at 0x7fbbd5ab29e8>它跟下面這個冗長得多的生成器是完全等價的:
def _make_gen():
for x in range(100): yield x ** 2gen = _make_gen()生成器表達式也可以取代列表推導式,作為函數參數:
In [191]: sum(x ** 2 for x in range(100))
Out[191]: 328350In [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.2345In [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.2345In [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 xTypeError: 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]: linesOut[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:
這樣可以在退出代碼塊時,自動關閉文件。
如果輸入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 rIn [215]: f2 = open(path, rb) # Binary modeIn [216]: f2.read(10)Out[216]: bSuexc3xb1a elread模式會將文件句柄的位置提前,提前的數量是讀取的位元組數。tell可以給出當前的位置:
In [217]: f.tell()
Out[217]: 11In [218]: f2.tell()Out[218]: 10儘管我們從文件讀取了10個字元,位置卻是11,這是因為用默認的編碼用了這麼多位元組才解碼了這10個字元。你可以用sys模塊檢查默認的編碼:
In [219]: import sys
In [220]: sys.getdefaultencoding()Out[220]: utf-8seek將文件位置更改為文件中的指定位元組:
In [221]: f.seek(3)
Out[221]: 3In [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]: linesOut[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]: charsOut[231]: Sue?a el rUTF-8是長度可變的Unicode編碼,所以當我從文件請求一定數量的字元時,Python會從文件讀取足夠多(可能少至10或多至40位元組)的位元組進行解碼。如果以「rb」模式打開文件,則讀取確切的請求位元組數:
In [232]: with open(path, rb) as f:
.....: data = f.read(10)In [233]: dataOut[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: unexpected 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?aIn [242]: f.seek(4)Out[242]: 4In [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 start byteIn [244]: f.close()如果你經常要對非ASCII字元文本進行數據分析,通曉Python的Unicode功能是非常重要的。更多內容,參閱Python官方文檔。
3.4 結論
我們已經學過了Python的基礎、環境和語法,接下來學習NumPy和Python的面向數組計算。
推薦閱讀:
※就是它了-結合自己興趣與事業發展的新方向
※小白python之路的開啟
※數據之美
※吳恩達deep learning中實現CNN的小作業
※誰在被取代