導讀:函式是Python中最重要、最基礎的程式碼組織和程式碼復用方式。根據經驗,如果你需要多次重覆相同或類似的程式碼,就非常值得寫一個可復用的函式。透過給一組Python陳述句一個函式名,形成的函式可以幫助你的程式碼更加可讀。
函式宣告時使用def關鍵字,傳回時使用return關鍵字:
def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)
有多條傳回陳述句是沒有問題的。如果Python達到函式的尾部時仍然沒有遇到return陳述句,就會自動傳回None。
每個函式都可以有位置引數和關鍵字引數。關鍵字引數最常用於指定預設值或可選引數。在前面的函式中,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)
在部分場景中,這樣做有助於程式碼可讀性
01 名稱空間、作用域和本地函式
函式有兩種連線變數的方式:全域性、本地。在Python中另一種更貼切地描述變數作用域的名稱是名稱空間。在函式內部,任意變數都是預設分配到本地名稱空間的。本地名稱空間是在函式被呼叫時生成的,並立即由函式的引數填充。當函式執行結束後,本地名稱空間就會被銷毀(除了一些特殊情況)。考慮以下函式:
def func():
a = []
for i in range(5):
a.append(i)
當func()呼叫時,空的串列會被建立,五個元素被新增到串列,之後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關鍵字的用法。通常全域性變數用來儲存系統中的某些狀態。如果你發現你大量使用了全域性變數,可能表明你需要面向物件程式設計(使用類)
02 傳回多個值
當我在使用Java和C++程式設計後第一次使用Python程式設計時,我最喜歡的特性就是使用簡單語法就可以從函式中傳回多個值。以下是程式碼:
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}
具體用哪種技術取決於你需要做什麼的事。
03 函式是物件
由於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
04 匿名(Lambda)函式
Python支援所謂的匿名或lambda函式。匿名函式是一種透過單個陳述句生成函式的方式,其結果是傳回值。匿名函式使用lambda關鍵字定義,該關鍵字僅表達“我們宣告一個匿名函式”的意思:
def short_function(x):
return x * 2
equiv_anon = lambda x: x * 2
匿名函式在資料分析中非常方便,因為在很多案例中資料變形函式都可以作為函式的引數。匿名函式程式碼量小(也更為清晰),將它作為引數進行傳值,比寫一個完整的函式或者將匿名函式賦值給區域性變數更好。舉個例子,考慮下麵的不佳示例:
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 * 2 for x in ints] ,但是在這裡我們能夠簡單地將一個自定義運運算元傳遞給apply_to_list函式。
另一個例子,假設你想要根據字串中不同字母的數量對一個字串集合進行排序:
In [177]: strings = [‘foo’, ‘card’, ‘bar’, ‘aaaa’, ‘abab’]
這裡我們可以將一個匿名函式傳給串列的sort方法:
In [178]: strings.sort(key=lambda x: len(set(list(x))))
In [179]: strings
Out[179]: [‘aaaa’, ‘foo’, ‘abab’, ‘bar’, ‘card’]
和def關鍵字宣告的函式不同,匿名函式物件自身並沒有一個顯式的__name__ 屬性,這是lambda函式被稱為匿名函式的一個原因。
05 柯里化:部分函式應用
柯里化是電腦科學術語(以數學家Haskell Curry命名),它表示透過部分引數應用的方式從已有的函式中衍生出新的函式。例如,假設我們有一個不重要的函式,其功能是將兩個數加一起:
def add_numbers(x, y):
return x + y
使用這個函式,我們可以衍生出一個只有一個變數的新函式,add_five,可以給引數加上5:
add_five = lambda y: add_numbers(5, y)
第二個引數對於函式add_numers就是柯里化了。這裡並沒有什麼神奇的地方,我們真正做的事只是定義了一個新函式,這個新函式呼叫了已經存在的函式。內建的functools模組可以使用pratial函式簡化這種處理:
from functools import partial
add_five = partial(add_numbers, 5)
06 生成器
透過一致的方式遍歷序列,例如串列中的物件或者檔案中的一行行內容,這是Python的一個重要特性。這個特性是透過迭代器協議來實現的,迭代器協議是一種令物件可遍歷的通用方式。例如,遍歷一個字典,獲得字典的鍵:
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]:
迭代器就是一種用於在背景關係中(比如for迴圈)向Python直譯器生成物件的物件。大部分以串列或串列型物件為引數的方法都可以接收任意的迭代器物件。包括內建方法比如min、max和sum,以及型別建構式比如list和tuple:
In [184]: list(dict_iterator)
Out[184]: [‘a’, ‘b’, ‘c’]
用迭代器構造新的可遍歷物件是一種非常簡潔的方式。普通函式執行並一次傳回單個結果,而生成器則“惰性”地傳回一個多結果序列,在每一個元素產生之後暫停,直到下一個請求。如需建立一個生成器,只需要在函式中將傳回關鍵字return替換為yield關鍵字:
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]:
直到你請求生成器中的元素時,它才會執行它的程式碼:
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
1. 生成器運算式
生成器運算式來建立生成器更為簡單。生成器運算式與串列、字典、集合的推導式很類似,建立一個生成器運算式,只需要將串列推導式的中括號替換為小括號即可:
In [189]: gen = (x ** 2 for x in range(100))
In [190]: gen
Out[190]:
上面的程式碼與下麵更為複雜的生成器是等價的:
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}
2. itertools模組
標準庫中的itertools模組是適用於大多數資料演演算法的生成器集合。例如,groupby可以根據任意的序列和一個函式,透過函式的傳回值對序列中連續的元素進行分組,參見下麵的例子:
In [193]: import itertools
In [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是一個生成器
A [‘Alan’, ‘Adam’]
W [‘Wes’, ‘Will’]
A [‘Albert’]
S [‘Steven’]
下表是一些我認為經常用到的itertools函式的串列。你可以透過查詢Python官方檔案來獲得更多關於內建工具庫的資訊。
函式 |
描述 |
combinations(iterable, k) |
根據iterable引數中的所有元素生成一個包含所有可能K元組的序列,忽略元素的順序,也不進行替代(需要替代請參考函式 combinations_with_replacement ) |
permutations(iterable, k) |
根據itrable引數中的按順序生成包含所有可能K元組的序列 |
groupby(iterable[, keyfunc]) |
根據每一個獨一的Key生成 (key, sub-iterator) 元組 |
product(*iterables, repeat=1) |
以元組的形式,根據輸入的可遍歷物件們生成笛卡爾積,與巢狀的for迴圈類似 |
07 錯誤和異常處理
優雅地處理Python的錯誤或異常是構建穩定程式的重要組成部分。在資料分析應用中,很多函式只能處理特定的輸入。例如,Python的float函式可以將字串轉換為浮點數字,但是對不正確的輸入會產生ValueError:
In [197]: float(‘1.2345’)
Out[197]: 1.2345
In [198]: float(‘something’)
—————————————————————————
ValueError Traceback (most recent call last)
—-> 1 float(‘something’)
ValueError: could not convert string to float: ‘something’
假設我們想要在float函式執行失敗時可以優雅地傳回輸入引數。我們可以透過將float函式寫入一個try/except程式碼段來實現:
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
你可能會註意到,除了ValueError,float函式還會丟擲其他的異常:
In [202]: float((1, 2))
—————————————————————————
TypeError Traceback (most recent call last)
—-> 1 float((1, 2))
TypeError: float() argument must be a string or a number, not ‘tuple‘
你可能只想處理ValueError,因為TypeError(輸入的不是字串或數值)可能表明你的程式中有個合乎語法的錯誤。為了實現這個目的,在except後面寫下異常型別:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
然後我們可以得到:
In [204]: attempt_float((1, 2))
—————————————————————————
TypeError Traceback (most recent call last)
—-> 1 attempt_float((1, 2))
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將會預設打印出完整的呼叫堆疊跟蹤(報錯追溯),會將堆疊中每個錯誤點附近的幾行背景關係程式碼打印出:
In [10]: %run examples/ipython_bug.py
—————————————————————————
AssertionError Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in
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直譯器提供更多額外的背景關係是IPython的一大進步(標準Python直譯器不提供任何額外的背景關係)。你可以使用%xmode命令來控制背景關係的數量,可以從Plain(普通)樣式(與標準Python直譯器一致)切換到Verbose(複雜)樣式(可以顯示函式的引數值以及更多有用資訊)。
關於作者:韋斯·麥金尼(Wes McKinney)是流行的Python開源資料分析庫pandas的創始人。他是一名活躍的演講者,也是Python資料社群和Apache軟體基金會的Python/C++開源開發者。目前他在紐約從事軟體架構師工作。
本文摘編自《利用Python進行資料分析》(原書第2版),經出版方授權釋出。
延伸閱讀《利用Python進行資料分析》
轉載請聯絡微信:togo-maruko
點選文末右下角“寫留言”發表你的觀點
推薦語:適合剛學Python的資料分析師或剛學資料科學以及科學計算的Python程式設計者。閱讀本書可以獲得一份關於在Python下操作、處理、清洗、規整資料集的完整說明。
更多精彩
在公眾號後臺對話方塊輸入以下關鍵詞
檢視更多優質內容!
PPT | 報告 | 讀書 | 書單 | 乾貨
Python | 機器學習 | 深度學習 | 神經網路
區塊鏈 | 揭秘 | 高考 | 福利
猜你想看
Q: 你都會用到哪些Python的函式?
歡迎留言與大家分享
覺得不錯,請把這篇文章分享給你的朋友
轉載 / 投稿請聯絡:baiyu@hzbook.com
更多精彩,請在後臺點選“歷史文章”檢視
點選閱讀原文,瞭解更多