def my_function(x, y, z=1.5):
if z > 1:
return z * (x + y)
else:
return z / (x + y)
導讀:函數是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中另一種更貼切地描述變量作用域的名稱是命名空間。在函數內部,任意變量都是默認分配到本地命名空間的。本地命名空間是在函數被調用時生成的,并立即由函數的參數填充。當函數執(zhí)行結束后,本地命名空間就會被銷毀(除了一些特殊情況)??紤]以下函數: def func(): a = [] for i in range(5): a.append(i) 當func()調用時,空的列表會被創(chuàng)建,五個元素被添加到列表,之后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關鍵字的用法。通常全局變量用來存儲系統中的某些狀態(tài)。如果你發(fā)現你大量使用了全局變量,可能表明你需要面向對象編程(使用類) 02 返回多個值 當我在使用Java和C++編程后第一次使用Python編程時,我最喜歡的特性就是使用簡單語法就可以從函數中返回多個值。以下是代碼: def f(): a = 5 b = 6 c = 7 return a, b, c a, b, c = f() 在數據分析和其他科研應用,你可能會發(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]: <dict_keyiterator at 0x7fbbd5a9f908> 迭代器就是一種用于在上下文中(比如for循環(huán))向Python解釋器生成對象的對象。大部分以列表或列表型對象為參數的方法都可以接收任意的迭代器對象。包括內建方法比如min、max和sum,以及類型構造函數比如list和tuple: In [184]: list(dict_iterator) Out[184]: ['a', 'b', 'c'] 用迭代器構造新的可遍歷對象是一種非常簡潔的方式。普通函數執(zhí)行并一次返回單個結果,而生成器則“惰性”地返回一個多結果序列,在每一個元素產生之后暫停,直到下一個請求。如需創(chuàng)建一個生成器,只需要在函數中將返回關鍵字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 當你實際調用生成器時,代碼并不會立即執(zhí)行: In [186]: gen = squares() In [187]: gen Out[187]: <generator object squares at 0x7fbbd5ab4570> 直到你請求生成器中的元素時,它才會執(zhí)行它的代碼: 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. 生成器表達式 生成器表達式來創(chuàng)建生成器更為簡單。生成器表達式與列表、字典、集合的推導式很類似,創(chuàng)建一個生成器表達式,只需要將列表推導式的中括號替換為小括號即可: 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} 2. itertools模塊 標準庫中的itertools模塊是適用于大多數數據算法的生成器集合。例如,groupby可以根據任意的序列和一個函數,通過函數的返回值對序列中連續(xù)的元素進行分組,參見下面的例子: 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官方文檔來獲得更多關于內建工具庫的信息。
07 錯誤和異常處理 優(yōu)雅地處理Python的錯誤或異常是構建穩(wěn)定程序的重要組成部分。在數據分析應用中,很多函數只能處理特定的輸入。例如,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函數運行失敗時可以優(yōu)雅地返回輸入參數。我們可以通過將float函數寫入一個try/except代碼段來實現: def attempt_float(x): try: return float(x) except: return x 如果float(x)執(zhí)行時拋出了異常,則代碼段中的except部分代碼將會被執(zhí)行: 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) <ipython-input-202-842079ebb635> in <module>() ----> 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) <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代碼塊是否報錯都要執(zhí)行。為了實現這個目的,使用finally關鍵字: f = open(path, 'w') try: write_to_file(f) finally: f.close() 這樣,我們可以讓f在程序結束后總是關閉。類似的,你可以使用else來執(zhí)行當try代碼塊成功執(zhí)行時才會執(zhí)行的代碼: f = open(path, 'w') try: write_to_file(f) except: print('Failed') else: print('Succeeded') finally: f.close() IPython中的異常 如果當你正在%run一個腳本或執(zhí)行任何語句報錯時,IPython將會默認打印出完整的調用堆棧跟蹤(報錯追溯),會將堆棧中每個錯誤點附近的幾行上下文代碼打印出: 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解釋器提供更多額外的上下文是IPython的一大進步(標準Python解釋器不提供任何額外的上下文)。你可以使用%xmode命令來控制上下文的數量,可以從Plain(普通)模式(與標準Python解釋器一致)切換到Verbose(復雜)模式(可以顯示函數的參數值以及更多有用信息)。 關于作者:韋斯·麥金尼(Wes McKinney)是流行的Python開源數據分析庫pandas的創(chuàng)始人。他是一名活躍的演講者,也是Python數據社區(qū)和Apache軟件基金會的Python/C++開源開發(fā)者。目前他在紐約從事軟件架構師工作。 |
|
來自: Four兄 > 《Python筆記》