《Python入門》-函數(總結),這就是別人成爲大牛的原因!

《Python入門》-函數(總結),這就是別人成為大牛的原因!

現實世界的程序很快就會變得越來越大,越來越複雜。需要一些方法把它們分成較小的部分進行組織,這樣更易於編寫,也更容易閱讀。這樣有助於做代碼複用,減少冗餘代碼,讓結構清晰。

(私信小編007即可自動獲取Python視頻教程以及各類PDF資源!)

把程序分解成較小的部分,主要有3種方法。

  1. 函數(function)
  2. 對象(object)
  3. 模塊(module)

本節我們先學習函數。函數是帶名字的代碼塊,可以把多個邏輯封裝起來。這樣就可以在程序中可以不止一次的運行它。

函數的一般格式如下:

def (arg1, arg2, ..., argN):

return

看一個真實的函數:

def hello():
print('Hello World!')
return True

這就是一個函數,def語句生成一個函數對象並賦值一個函數名,函數名字叫做hello。括號內可以傳入一些參數內容,當然也可以不傳,就是一個空括號,表示它不需要任何其他額外信息就能完成工作。另外在函數定義的最後要加上冒號。冒號告訴 Python 接下來是一個代碼塊,這個代碼塊是一個縮進的函數體,每次函數調用就是執行這部分內容,

代碼塊包含2個語句,先打印hello world,最後一句return表示返回,也就是函數返回值為True

調用函數是指運行函數里的代碼。定義而不調用,那麼函數內的代碼塊永遠不會執行,調用函數時要使用函數名和一對括號。

In : hello()
Hello World!
Out: True


向函數傳遞參數

可以給函數傳遞參數,這樣調用函數時可以讓結果變得不一樣

In : def hello(name):
....: print(f'Hello, {name}!')
....:
In : hello('Amy')
Hello, Amy!
In : hello('Chris')
Hello, Chris!

調用時無論傳入什麼樣的名字,都會打印相應的輸出。

函數參數數量可以任意,完全看業務需要。

這裡要引入實參和形參。

形參:函數定義中在內部使用的參數,這是函數完成其工作所需的一項信息,在沒實際調用的時候,函數用形參來指代。 實參:是指調用函數時由調用者傳入的參數,這個時候形參指代的內容就是實參了。

上面的例子中name就是形參,amy和chris是實參

實參類型

調用函數時,可以指定兩種類型的參數:位置參數(positional argument)和關鍵字參數(keyword argument).

位置參數

位置參數又稱為非關鍵字參數(non-keyword argument),這種參數的指定方式有兩種:直接以值的形式和以*開頭的可迭代對象:

In : def hello(name):
....: print(f'Hell, {name}!')
....:

之前用的hello這個函數,name就是位置參數。一個*加上形參名的函數表示這個函數實參個數不定:

In : def hello(*names):
...: print(names)
...:
In : hello()
()
In : hello(1)
(1,)
In : hello(1, 2)
(1, 2)

如果不能肯定參數的個數,就可以使用這種變長元組參數。另外,位置參數的順序很重要,實參會直接對應形參位置。

強制關鍵字參數

Python 3.6 添加了一個新功能,就是強制關鍵字參數。使用強制關鍵字參數會比使用位置參數表意更加清晰,程序也更加具有可讀性,那麼可以讓這些參數強制使用關鍵字參數傳遞,可以將強制關鍵字參數放到某個參數或者單個後面就能達到這種效果:

In : def recv(maxsize, *, block):
....: pass
....:
In : recv(1024, True)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
----> 1 recv(1024, True)
TypeError: recv() takes 1 positional argument but 2 were given
In : recv(1024, block=True)

關鍵字參數

關鍵字參數的指定方式也有兩種:以 name=value (名稱=值)的方式和 以**開頭的字典。關鍵字參數可以讓函數更加清晰、容易使用,也無需考慮函數調用中的實參順序,因為Python知道各個值該存儲到哪個形參中。

In : def hello(name='World'):
....: print(f'Hello, {name}!')
....:
In : hello()
Hello, World!
In : hello('Amy')
Hello, Amy!
In : hello(name='Chris')
Hello, Chris!

也就是name有個默認值,如果不傳入name就使用默認的world。

**表示變長關鍵字參數。在常規參數和默認參數綁定完成後,如果還有額外(0個或多個)的關鍵字參數,則為變長關鍵字參數,將會把這些多餘的“關鍵字參數” 以字典的形式蒐集到一起。

混合使用

調用函數時,可以混合使用位置參數和關鍵字參數,但是位置參數必須位於關鍵字參數之前。

In : def hello(name, default='World'):
....: print(f'Hello, {name or default}!')
....:

要混合使用上述四種類型的形參,則這幾種形參類型的排列順序必須從左到右依次為:常規參數,默認參數,變長元組參數,變長關鍵字參數。

In : def func(a, b=0, *args, **kwargs):
....: print('a =', a, 'b =', b, 'args =', args, 'kwargs =', kwargs)
....:
In : func(1, 2)
a = 1 b = 2 args = () kwargs = {}
In : func(1, 2, d=4)
a = 1 b = 2 args = () kwargs = {'d': 4}
In : func(1, 2, 3)
a = 1 b = 2 args = (3,) kwargs = {}
In : func(1, 2, 3, d=4)
a = 1 b = 2 args = (3,) kwargs = {'d': 4}

返回值

之前的例子中都是打印輸出,實際開發中通常會讓函數在執行一系列邏輯之後,返回一個或一組值。函數返回的值被稱為返回值。在函數中,可使用return語句將值返回到調用函數的代碼行。之前看到的函數都沒有顯式的使用return,可以理解為返回值為None。

In : def add(a, b):
....: return a + b
....:
In : add(1, 2)
Out: 3

返回值就是函數的執行結果。返回值不僅可以是單個,也可以是一組:

In : def partition(string, sep):
....: return string.partition(sep)
....:
In : partition('/home/dongwm/bran', '/')
Out: ('', '/', 'home/dongwm/bran')

參數為函數:

函數實參除了可以是常見的數據結構。也可以是函數:

In : def hello(name):
print(f'Hello {name}!')
....:
In : def test(func, name='World'):
....: func(name)
....:
In : test(hello, 'Amy')
Hello Amy!

本地變量/全局變量

本地變量

在函數定義內聲明的變量就是本地變量,也叫局部變量,它們與函數外具有相同名稱的其他變量沒有任何關係,即變量只是在函數內可見的。我們看下面的例子

In : def run(name):
...: s = f'{name}'
...: for x in range(5):
...: if x == 3:
...: return
...: print(s)
...:
In : run('Test')

第二行的s是被賦值過的,所以s是一個本地變量, 第三行for循環將元素賦值給變量x, 所以x是一個本地變量, 第一行參數name也是通過賦值被傳入的,所以也是本地變量。

這些本地變量會在函數調用時出現,在函數退出時消失。

全局變量

全局變量有更大的作用域,它可以在程序的任何地方使用這個變量。

In : g = 0
In : def run():
...: print(g)
...:
In : run()
0
In : def run():
....: g = 2
....:
In : g
Out: 0

可以感受到g在函數內也可以訪問的到。另外函數內的修改沒有影響這個全局變量。

現在我演示一個初學者常見錯誤使用全局變量的例子:

In : g = 0
In : def run():
....: print(g)
....: g = 2
....: print(g)
....:
In : run()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
in ()
----> 1 run()
in run()
1 def run():
----> 2 print(g)
3 g = 2
4 print(g)
5
UnboundLocalError: local variable 'g' referenced before assignment

在函數內操作全局變量很常見,但現在Python拋出了一個異常,錯誤提示局部變量g在賦值前被應用,也就是該變量沒有定義就使用它,錯誤發生在第二行,也就是第一次print g的時候。但是一開始已經對g賦值了。但是為了print時候說是本地變量g沒有被定義呢?這是因為在函數內第二行,想把2賦值給g,如果函數內部的變量名第一次出現,且出現在=前面,即被視為定義一個局部變量,不管全局域中有沒有用到該變量名,函數中使用的將是局部變量。我們在感受下:

In : def run():
....: g += 2
....: print(g)
....:
In : run()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
in ()
----> 1 run()
in run()
1 def run():
----> 2 g += 2
3 print(g)
4
UnboundLocalError: local variable 'g' referenced before assignment

如果確實想這麼用,怎麼辦呢?可以使用global關鍵字:

In : def run():
....: global g
....: g += 2
....: print(g)
....:
In : run()
2
In : g
Out[19]: 2
In : run()
4
In : g
Out[21]: 4

但是請注意,global語句會讓函數內的賦值影響到全局,這種修改是很隱晦的,不具備可讀和可追溯性。global語句我是非常不推薦使用的,除非你很清楚確實需要使用它,而且我實際工作中總結,真的基本不需要用它,如果你不得不用它,往往是由於程序設計有問題,或者在濫用。

變量在全局域中有定義,而在局部沒有定義,則會使用全局變量,如果局部要定義,定義前不要使用這個變量。否則需要引入關鍵字global

作用域(scope)

上面說的local和global是不同的作用域。作用域簡單說就是一個變量的命名空間,這個空間裡面可以創建改變和查找變量名。所以變量賦值的地方決定了它的作用域。Python的變量作用域中分為四個級別,簡稱為:BGEL,作用域的級別依次升高,級別最高的是Local,如果該變量在Local中已經聲明並賦值,將優先使用Local中的變量對應的值:

  1. B:build-in 系統固定模塊裡面的變量,也叫系統變量,比如int,這些變量可以通過builtins模塊獲取
  2. G:global 全局變量,在單個程序文件裡面都可用, 它位於文件代碼的頂級
  3. E:enclosing 嵌套的父級函數的局部作用域,就是包含此函數的上層函數的局部作用域
  4. L:local 局部作用域,即為函數中定義的變量
In : import builtins 

In : ', '.join((i for i in dir(builtins) if i.islower() and '_' not in i))
Out[33]: 'abs, all, any, ascii, bin, bool, bytearray, bytes, callable, chr, classmethod, compile, complex, copyright, credits, delattr, dict, dir, divmod, dreload, enumerate, eval, exec, filter, float, format, frozenset, getattr, globals, hasattr, hash, help, hex, id, input, int, isinstance, issubclass, iter, len, license, list, locals, map, max, memoryview, min, next, object, oct, open, ord, pow, print, property, range, repr, reversed, round, set, setattr, slice, sorted, staticmethod, str, sum, super, tuple, type, vars, zip'

這個列表太長了,省略了一些,在這裡只展示一些比較常見的。builtins模塊裡面的這些函數和類等內容構成了內置作用域。這些函數和類可以直接使用,Python會從這裡找到他們

在Python2裡面builtins模塊叫做__builtin__,

>>> import __builtin__
>>> dir(__builtin__)
...

在Python3中雙下劃線的這種用法依然可以使用,而且不需要導入就能使用.

E和L是相對的,E中的變量相對上層來說也是L.

E嵌套作用域,在local中取值,但是local中沒有,就會去E裡面找。我們感受一個例子:

In : g = 0
In : def run():
...: g = 2
...: def run2():
...: print(g)
...: return run2
...:
In : f = run()
In : f()
2

這個例子值得回味,第一函數內嵌套了函數run2, run2內使用變量g,但是run2裡面沒有,所以它從內向外找,就找到了run的作用域。第二注意run函數的最後一句,它返回了run2這個函數,所以 f = run(),就是把run2函數賦值給f,執行f就是執行run2函數,但是可以訪問run函數的作用域。

閉包Closure

上個例子裡面出現了一個函數返回了嵌套在它裡面的函數的用法,我們再看一個重複的幫助記憶:

In : def maker(n):
...: def action(m):
...: return m * n
...: return action
...:
In : f = maker(3)
In : f(2)
Out: 6
In : g = maker(10)
In : g(2)
Out: 20

這個maker函數有一個特點,它返回的是內嵌函數action的一個引用,它記憶了嵌套作用域裡面形參n,這個n不是action的本地變量。在調用中,賦給f的函數記憶了n的實參3,賦給g的函數記憶了n的實參10,f和n這2個函數有自己的狀態

action函數就是閉包。我引用流暢的Python裡面對閉包的定義:

閉包指延伸了作用域的函數,其中包含函數定義體中引用,但是不在定義體中定義的非全局變量... 它能訪問定義體之外定義的非全局變量。


另外一個知識點,maker函數叫作工廠函數,就像一個生產函數的工廠,如上例,f和g都是它生產的函數。

nonlocal

上面我們說到了,如果不用global關鍵字,函數內對全局變量的定義不會影響函數外。包含在嵌套作用域裡面:

In : def run():
....: g = 2
....: def run2():
....: g = 4
....: print('inner ---> ', g)
....: run2()
....: print('outer --->', g)
....:
In : run()
inner ---> 4
outer ---> 2

可以看到run2裡面g為4,在run裡面g為2. 怎麼修改嵌套作用域的變量呢,Python3新增了一個關鍵字nonlocal:

In : def run():
....: g = 2
....: def run2():
....: nonlocal g
....: g = 4
....: print('inner ---> ', g)
....: run2()
....: print('outer --->', g)
....:
In : run()
inner ---> 4
outer ---> 4

這樣在run2中就可以對父級作用域裡面的變量的g做修改了。

到這裡,我再重複的進一步總結,賦值的變量名如果不使用global和nonlocal關鍵字聲明為全局變量或者非本地變量,均為本地變量。

匿名函數

有些時候,不需要顯式地定義函數,直接傳入匿名函數更方便。可以用關鍵字lambda創建一個匿名函數,也就是沒有名稱的函數。匿名函數的格式是這樣的:

lambda 參數: 表達式


關鍵字lambda說明它是一個匿名函數,冒號前面的變量是該匿名函數的參數,冒號後面是函數的返回值,注意這裡不需使用return關鍵字。

使用匿名函數不用寫def語句 不用費力的去想名字,很適用於創建一些臨時性的,小巧的函數。

我們舉個例子,現在有一個列表,全部元素都是數字,想把每個元素都乘以2最後成為一個新的列表。最傳統的函數寫法:

In : def double(n):
...: return n * 2
...:

In : double(10)
Out: 20

如果是匿名函數就寫成下面這樣:

In : f = lambda n: n * 2
In : f(10)
Out: 20

這種對一個序列每個元素做一些處理生成新序列的需求很常見的,那麼可不可以寫的更簡潔和優美呢。答案顯然是有的,用匿名函數之前,我們先學習幾個常見的高階函數。

高階函數英文叫Higher-order function,是指把函數作為參數傳入的函數,順便插一句函數式編程就是指這種高度抽象的編程範式。本節先聊5個最常用的高階的函數,分別是map/filter/sum/zip/reduce。

map

In : l1 = [1, 3, 4]
In : l2 = []
In : for i in l1:
....: l2.append(double(i))
....:
In : l2
Out: [2, 6, 8]

map 接收一個函數和一個可迭代的對象,並通過把函數依次作用在序列的每個元素上,得到一個新的可迭代的對象並返回。比如上例可以這麼寫

In : rs = map(double, l1)
In : rs
Out:
In : list(rs)
Out: [2, 6, 8]

用map就是一句話搞定,它返回的是一個迭代器,如果想看到全部內容可以轉換成列表。map接收的可迭代的對象不僅是列表,元組,字典等都可以:

In : list(map(double, {'a':1, 'b': 2}))
Out: ['aa', 'bb']


filter

filter函數接收一個函數和一個可迭代的對象,這個函數的作用是對每個元素進行判斷,返回True或False,返回False會被自動過濾掉,返回由符合條件元素組成的新可迭代的對象。

In : def is_odd(x):
....: return x % 2 == 1
....:
In : rs = filter(is_odd, l1)
In : rs
Out:
In : list(rs)
Out: [1, 3]

由於Python布爾值的設置,如果像過濾那些布爾值為False的對象,可以用下面的方法:

In : list(filter(None, [1, '', {}, (), False, None, set()]))
Out: [1]


reduce

reduce函數接收一個函數和一個可迭代的對象,但行為和 map()不同,reduce()傳入的函數必須接收兩個參數,reduce對可迭代的對象的每個元素反覆調用函數,並返回最終結果值。求合可以這些寫:

In : def add(a, b):
....: return a + b
....:
In : from functools import reduce
In : reduce(add, [1, 2, 3])
Out: 6

reduce在Python2可以直接使用,但是在Python3中被放進了functools模塊,需要先導入這個模塊, 模塊裡面就包含了很多高階函數。這個求合的例子裡面reduce,先把1和2當做add的參數,執行返回3,再把3當做a, 列表最後一個元素3當做參數b, 再調用add函數,最後返回總結果6

reduce還可以接受三個參數,作為計算的初始值10。

In : reduce(add, [1, 2, 3], 10) 

Out: 16


匿名函數續

回到匿名函數問題上,現在我們可以用map了,但是要創建函數double

In : def double(n):
....: return n * 2
....:
In : map(double, l1)

用匿名函數會更直觀:

In : list(map(lambda x: x * 2, l1))
Out: [2, 6, 8]

匿名函數試用場景很多,比如之前介紹列表的sort方法時候沒有說它接受參數key來決定排序方案. 我們看個複雜一點的列表

In : l = [[2, 4], [1, 1], [9, 3]]
In : sorted(l)
Out: [[1, 1], [2, 4], [9, 3]]

這個列表每個元素都是一個列表,sorted函數默認就按元素內容從左到右對比,由於這三個元素的第一項的值各不相同,會按照每項第一個元素從小到大這種升序排了。

如果希望安裝元素的第二項的大小來拍呢?用匿名函數就很方便了

In : sorted(l, key=lambda x:x[1])
Out: [[1, 1], [9, 3], [2, 4]]

key描述的就是用來做排序的每項元素的那個部分。匿名函數的x,就是指代每項元素,x[1] 就是元素的第二項的意思。匿名函數的表達式部分非常靈活,還可以使用對象屬性,調用方法甚至混用:

In : l3 = ['/boot/grub', '/usr/local', '/home/dongwm']
In : sorted(l3, key=lambda x: x.rsplit('/')[2])
Out: ['/home/dongwm', '/boot/grub', '/usr/local']


zip

zip() 函數用於將可迭代的對象作為參數,將對象中對應的元素打包成一個個元組,然後返回由這些元組組成的列表。

如果各個迭代器的元素個數不一致,則返回列表長度與最短的對象相同,多出來的部分元素被忽略掉。

利用*號操作符,可以將元組解壓為列表。

In : list(zip(*zip(a, b)))
Out: [(1, 2, 3), (4, 5, 6)]


第二次用zip可理解為解壓,返回二維矩陣式

sum

sum是求合函數。

In : sum([1, 2, 3])
Out: 6
In : sum([1, 2, 3], 10)
Out: 16

他接收第二個參數,可以傳入一個初始值,默認是0,求合的結果基於這個初始值。另外sum一個有意思的用法是可以把嵌套類型的元素扁平化,就是從嵌套結構中剝離出來

In : sum([[1, 2], [3, 4]], [])
Out: [1, 2, 3, 4]


開發陷阱

可變默認參數

In : def append_to(element, to=[]):
....: to.append(element)
....: return to
....:
In : my_list = append_to(12)
In : my_list
Out: [12]
In : my_other_list = append_to(42)

In : my_other_list
Out: [12, 42]

可以看到,影響了第2次執行的結果。當默認參數值是可變對象的時候,那麼每次使用該默認參數的時候,其實更改的是同一個變量。為了防止出現這種情況,通常使用一個完全不預期的值,比如None,在邏輯中檢查,如果是這個預期的值就初始化。

In : def append_to(element, to=None):
....: if to is None:
....: to = []
....: to.append(element)
....: return to
....:

閉包變量綁定

In : def create_multipliers():
....: return [lambda x : i * x for i in range(5)]
....:
In : for multiplier in create_multipliers():
....: print(multiplier(2))
....:
8
8
8
8
8

但是本來我們希望的應該是[0, 2, 4, 6, 8]這個列表。這是因為閉包中用到的變量的值,是在內部函數被調用時查詢得到的,也就是延遲綁定,i在range(5)最後一個循環時被設置為了4。

如果希望這個需要正常有2個辦法。第一是用 函數默認值:

In : def create_multipliers():
.....: return [lambda x, i=i : i * x for i in range(5)]
.....:

第二種是用之後要講的偏函數。創建一個新的函數,這個新函數可以固定住函數的參數i,從而讓結果正確:

In : from functools import partial
In : from operator import mul
In : def create_multipliers():
.....: return [partial(mul, i) for i in range(5)]


分享到:


相關文章: