1. 程式人生 > 實用技巧 >python函式進階

python函式進階

<div id="page_begin_html"><script type="text/x-mathjax-config;executed=true">

MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\(','\)']]}});

<!--done-->

太白金星

</div><!--end: blogTitle 部落格的標題和副標題 -->
<div id="navigator">
	<div class="blogStats">
		<span id="stats_post_count">隨筆 - 6&nbsp; </span>

文章 - 121
評論 - 239

	</div><!--end: blogStats -->
</div><!--end: navigator 部落格導航欄 -->

函式的進階
        </h1>
        <div class="clear"></div>
        <div class="postBody">

1. 今日內容

​ 1.1 函式的引數

​ *的魔性用法

​ 函式形參最終順序

​ 1.2名稱空間

​ 全域性名稱空間,區域性名稱空間,內建名稱空間

​ 取值順序與載入順序

​ 作用域

​ 內建函式:globals() locals()

​ 1.3 高階函式(函式的巢狀)

​ 1.4關鍵字:global nonlocal

2. 內容詳細

2.1 函式的引數

​ 昨天我們從形參角度,講了兩種引數,一個是位置引數,位置引數主要是實參與形參從左至右一一對應,一個是預設值引數,預設值引數,如果實參不傳參,則形參使用預設引數。那麼無論是位置引數,還是預設引數,函式呼叫時傳入多少實參,我必須寫等數量的形參去對應接收, 如果不這樣,那麼就會報錯:

def eat(a,b,c,):
print('我請你吃:',a,b,c) 
eat('蒸羊羔','蒸熊掌','蒸鹿尾兒','燒花鴨') # 報錯

如果我們在傳引數的時候不很清楚有哪些的時候,或者說給一個函式傳了很多實參,我們就要對應寫很多形參,這樣很麻煩,怎麼辦?,我們可以考慮使用動態引數也叫萬能引數

2.11 形參的第三種:動態引數

動態引數分為兩種:動態接受位置引數 *args,動態接收關鍵字引數**kwargs.

動態接收位置引數:*args

​ 我們按照上面的例子繼續寫,如果我請你吃的內容很多,但是我又不想用多個引數接收,那麼我就可以使用動態引數*args

def eat(*args):
print('我請你吃:',args)
eat('蒸羊羔兒','蒸熊掌','蒸鹿尾兒','燒花鴨','燒雛雞','燒子鵝')
# 執行結果:
#我請你吃: ('蒸羊羔兒', '蒸熊掌', '蒸鹿尾兒', '燒花鴨', '燒雛雞', '燒子鵝')

​ 解釋一下上面引數的意義:首先來說args,args就是一個普通的形參,但是如果你在args前面加一個,那麼就擁有了特殊的意義:在python中除了表示乘號,他是有魔法的。+args,這樣設定形參,那麼這個形參會將實參所有的位置引數接收,放置在一個元組中,並將這個元組賦值給args這個形參,這裡起到魔法效果的是 * 而不是args,a也可以達到剛才效果,但是我們PEP8規範中規定就使用args,約定俗成的。
練習:傳入函式中數量不定的int型資料,函式計算所有數的和並返回。

def my_max(*args):
n = 0
for i in args:
n += i
return n

動態接收關鍵字引數: **kwargs

​ 實參角度有位置引數和關鍵字引數兩種,python中既然有*args可以接受所有的位置引數那麼肯定也有一種引數接受所有的關鍵字引數,那麼這個就是kwargs,同理這個是具有魔法用法的,kwargs約定俗成使用作為形參。舉例說明:**kwargs,是接受所有的關鍵字引數然後將其轉換成一個字典賦值給kwargs這個形參。

def func(**kwargs):
print(kwargs) # {'name': '太白金星', 'sex': '男'}
func(name='太白金星',sex='男')

我們看一下動態引數的完成寫法:

def func(*args,**kwargs):
    print(args) # ('蒸羊羔兒', '蒸熊掌', '蒸鹿尾兒')
    print(kwargs) # {'name': '太白金星', 'sex': '男'}
func('蒸羊羔兒', '蒸熊掌', '蒸鹿尾兒',name='太白金星',sex='男')

​ 如果一個引數設定了動態引數,那麼他可以接受所有的位置引數,以及關鍵字引數,這樣就會大大提升函式拓展性,針對於實參引數較多的情況下,解決了一一對應的麻煩。

2.12 * 的魔性用法

​ 剛才我們研究了動態引數,其實有的同學對於魔法用法 * 比較感興趣,那麼那的魔性用法不止這麼一點用法,我們繼續研究:

函式中分為打散和聚合。

函式外可以處理剩餘的元素。

函式的打散和聚合

聚合

​ 剛才我們研究了,在函式定義時,如果我只定義了一個形參稱為args,那麼這一個形參只能接受幾個實參? 是不是隻能當做一個位置引數對待?它只能接受一個引數:

def eat(args):
    print('我請你吃:',args) # 我請你吃: 蒸羊羔兒
eat('蒸羊羔兒')

​ 但是如果我給其前面加一個* 那麼args可以接受多個實參,並且返回一個元組,對吧? (**kwargs也是同理將多個關鍵字引數轉化成一個字典返回)所以在函式的定義時: *起到的是聚合的作用。

打散

​ 此時不著急給大家講這個打散,而是出一個小題:你如何將三個資料(這三個資料都是可迭代物件型別)s1 = 'alex',l1 = [1, 2, 3, 4], tu1 = ('武sir', '太白', '女神',)的每一元素傳給動態引數*args?(就是args最終得到的是 ('a','l','e','x', 1, 2, 3, 4,'武sir', '太白', '女神',)?有人說這還不簡單麼?我直接傳給他們不就行了?

s1 = 'alex'
l1 = [1, 2, 3, 4]
tu1 = ('武sir', '太白', '女神',)
def func(*args):
    print(args) # ('alex', [1, 2, 3, 4], ('武sir', '太白', '女神'))
func(s1,l1,tu1)

這樣肯定是不行,他會將這個三個資料型別當成三個位置引數傳給args,沒有實現我的要求。

好像你除了直接寫,沒有別的什麼辦法,那麼這裡就得用到我們的魔法用法 :*

s1 = 'alex'
l1 = [1, 2, 3, 4]
tu1 = ('武sir', '太白', '女神',)
def func(*args):
    print(args) # ('a', 'l', 'e', 'x', 1, 2, 3, 4, '武sir', '太白', '女神')
func(*s1,*l1,*tu1)

​ 你看此時是函式的執行時,我將你位置引數的實參(可迭代型別)前面加上,相當於將這些實參給拆解成一個一個的組成元素當成位置引數,然後傳給args,這時候這個好像取到的是打散的作用。所以在函式的執行時:,**起到的是打散的作用。

dic1 = {'name': '太白', 'age': 18}
dic2 = {'hobby': '喝茶', 'sex': '男'}
def func(**kwargs):
    print(kwargs) # {'name': '太白', 'age': 18, 'hobby': '喝茶', 'sex': '男'}
func(**dic1,**dic2)

​ *處理剩下的元素

*除了在函式中可以這樣打散,聚合外,函式外還可以靈活的運用:

# 之前講過的分別賦值
a,b = (1,2)
print(a, b) # 1 2
# 其實還可以這麼用:
a,*b = (1, 2, 3, 4,)
print(a, b) # 1 [2, 3, 4]
*rest,a,b = range(5)
print(rest, a, b) # [0, 1, 2] 3 4
print([1, 2, *[3, 4, 5]]) # [1, 2, 3, 4, 5]

2.13 形參的順序

​ 到目前為止,從形參的角度我們講了位置引數,預設值引數,動態引數*args,**kwargs,還差一種引數,需要講完形參順序之後,引出。先不著急,我們先看看已經講的這些形參他的排列順序是如何的呢?

首先,位置引數,與預設引數他兩個的順序我們昨天已經確定了,位置引數必須在前面,即 :位置引數,預設引數。

那麼動態引數*args,**kwargs放在哪裡呢?

動態引數*args,肯定不能放在位置引數前面,這樣我的位置引數的引數就接收不到具體的實參了:

# 這樣位置引數a,b始終接收不到實參了,因為args全部接受完了
def func(*args,a,b,sex='男'):
print(args)
print(a,b)
func(1, 2, 3, 4, 5)

那麼動態引數必須在位置引數後面,他可以在預設引數後面麼?

# 這樣也不行,我的實參的第三個引數始終都會將sex覆蓋掉,這樣失去了預設引數的意義。
def func(a,b,sex='男',*args,):
print(args) # (4, 5)
print(sex) # 3
print(a,b) # 1 2
func(1, 2, 3, 4, 5)

所以*args一定要在位置引數與預設值引數中間:位置引數,*args,預設引數

那麼我的kwargs放在哪裡?kwargs可以放在預設引數前面麼?

# 直接報錯:因為**kwargs是接受所有的關鍵字引數,如果你想改變預設引數sex,你永遠也改變不了,因為
# 它會先被**kwargs接受。
def func(a,b,*args,**kwargs,sex='男',):
print(args) # (4, 5)
print(sex) # 3
print(a,b) # 1 2
print(kwargs)
func(1, 2, 3, 4, 5)

所以截止到此:所有形參的順序為:位置引數,*args,預設引數,**kwargs。

2.14 形參的第四種引數:僅限關鍵字引數

​ 僅限關鍵字引數是python3x更新的新特性,他的位置要放在*args後面,kwargs前面(如果有kwargs),也就是預設引數的位置,它與預設引數的前後順序無所謂,它只接受關鍵字傳的引數:

# 這樣傳參是錯誤的,因為僅限關鍵字引數c只接受關鍵字引數
def func(a,b,*args,c):
print(a,b) # 1 2
print(args) # (4, 5)
# func(1, 2, 3, 4, 5)
# 這樣就正確了:
def func(a,b,*args,c):
print(a,b) # 1 2
print(args) # (3, 4)
print(5)
func(1, 2, 3, 4, c=5)

​ 這個僅限關鍵字引數從名字定義就可以看出他只能通過關鍵字引數傳參,其實可以把它當成不設定預設值的預設引數而且必須要傳引數,不傳就報錯。

所以形參角度的所有形參的最終順序為:位置引數,*args,預設引數,僅限關鍵字引數,**kwargs。

課間考一道題:
def foo(a,b,*args,c,sex=None,**kwargs):
print(a,b)
print(c)
print(sex)
print(args)
print(kwargs)
# foo(1,2,3,4,c=6)
# foo(1,2,sex='男',name='alex',hobby='old_woman')
# foo(1,2,3,4,name='alex',sex='男')
# foo(1,2,c=18)
# foo(2, 3, [1, 2, 3],c=13,hobby='喝茶')
# foo(*[1, 2, 3, 4],**{'name':'太白','c':12,'sex':'女'})

2.2 名稱空間,作用域

2.21 名稱空間:

接下來我們講的內容,理論性的偏多,就是從空間角度,記憶體級別去研究python。首先我們看看什麼是全域性名稱空間:

​ 在python直譯器開始執行之後, 就會在記憶體中開闢一個空間, 每當遇到一個變量的時候, 就把變量名和值之間的關係記錄下來, 但是當遇到函式定義的時候, 直譯器只是把函式名讀入記憶體, 表示這個函式存在了, 至於函式內部的變量和邏輯, 直譯器是不關心的. 也就是說一開始的時候函式只是載入進來, 僅此而已, 只有當函式被呼叫和訪問的時候, 直譯器才會根據函式內部宣告的變量來進行開闢變量的內部空間. 隨著函式執行完畢, 這些函式內部變量佔用的空間也會隨著函式執行完畢而被清空.

​ 我們首先回憶一下Python程式碼執行的時候遇到函式是怎麼做的,從Python直譯器開始執行之後,就在記憶體中開闢裡一個空間,每當遇到一個變數的時候,就把變數名和值之間對應的關係記錄下來,但是當遇到函式定義的時候,直譯器只是象徵性的將函式名讀如記憶體,表示知道這個函式存在了,至於函式內部的變數和邏輯,直譯器根本不關心。

​ 等執行到函式呼叫的時候,Python直譯器會再開闢一塊記憶體來儲存這個函式裡面的內容,這個時候,才關注函式裡面有哪些變數,而函式中的變量回儲存在新開闢出來的記憶體中,函式中的變數只能在函式內部使用,並且會隨著函式執行完畢,這塊記憶體中的所有內容也會被清空。

我們給這個‘存放名字與值的關係’的空間起了一個名字-------名稱空間。

程式碼在執行伊始,建立的儲存“變數名與值的關係”的空間叫做全域性名稱空間;

在函式的執行中開闢的臨時的空間叫做區域性名稱空間也叫做臨時名稱空間。

現在我們知道了,py檔案中,存放變數與值的關係的一個空間叫做全域性名稱空間,而當執行一個函式時,記憶體中會臨時開闢一個空間,臨時存放函式中的變數與值的關係,這個叫做臨時名稱空間,或者區域性名稱空間。

​ 其實python還有一個空間叫做內建名稱空間:內建名稱空間存放的就是一些內建函式等拿來即用的特殊的變數:input,print,list等等,所以,我們通過畫圖捋一下:

那麼這就是python中經常提到的三個空間。

總結:

​ \1. 全域性名稱空間--> 我們直接在py檔案中, 函式外宣告的變量都屬於全域性名稱空間

​ \2. 區域性名稱空間--> 在函式中宣告的變量會放在區域性名稱空間

​ \3. 內建名稱空間--> 存放python直譯器為我們提供的名字, list, tuple, str, int這些都是內建名稱空間

2.22 載入順序:

​ 所謂的載入順序,就是這三個空間載入到記憶體的先後順序,也就是這個三個空間在記憶體中建立的先後順序,你想想他們能是同時建立麼?肯定不是的,那麼誰先誰後呢?我們捋順一下:在啟動python直譯器之後,即使沒有建立任何的變數或者函式,還是會有一些函式直接可以用的比如abs(-1),max(1,3)等等,在啟動Python直譯器的時候,就已經匯入到記憶體當中供我們使用,所以肯定是先載入內建名稱空間,然後就開始從檔案的最上面向下一行一行執行,此時如果遇到了初始化變數,就會建立全域性名稱空間,將這些對應關係存放進去,然後遇到了函式執行時,在記憶體中臨時開闢一個空間,載入函式中的一些變數等等。所以這三個空間的載入順序為:內建名稱空間(程式執行伊始載入)->全域性名稱空間(程式執行中:從上到下載入)->區域性名稱空間(程式執行中:呼叫時才載入。

2.23 取值順序:

​ 取值順序就是引用一個變數,先從哪一個空間開始引用。這個有一個關鍵點:從哪個空間開始引用這個變數。我們分別舉例說明:

# 如果你在全域性名稱空間引用一個變數,先從全域性名稱空間引用,全域性名# 稱空間如果沒有,才會向內建名稱空間引用。
input = 666
print(input) # 666
# 如果你在區域性名稱空間引用一個變數,先從區域性名稱空間引用,
# 區域性名稱空間如果沒有,才會向全域性名稱空間引用,全域性名稱空間在沒有,就會向內建名稱空間引用。
input = 666
print(input) # 666
input = 666
def func():
    input = 111
    print(input) # 111
func()

所以空間的取值順序與載入順序是相反的,取值順序滿足的就近原則,從小範圍到大範圍一層一層的逐步引用。

2.24 作用域

作用域就是作用範圍, 按照生效範圍來看分為全域性作用域和區域性作用域

全域性作用域: 包含內建名稱空間和全域性名稱空間. 在整個檔案的任何位置都可以使用(遵循 從上到下逐⾏執行).

區域性作用域: 在函式內部可以使用.

作⽤域名稱空間:

1. 全域性作用域: 全域性名稱空間 + 內建名稱空間

2. 區域性作⽤域: 區域性名稱空間

2.25 內建函式globals(),locals()

這兩個內建函式放在這裡講是在合適不過的,他們就直接可以反映作用域的內容,有助於我們理解作用域的範圍。

globals(): 以字典的形式返回全域性作用域所有的變數對應關係。

locals(): 以字典的形式返回當前作用域的變數的對應關係。

這裡一個是全域性作用域,一個是當前作用域,一定要分清楚,接下來,我們用程式碼驗證:

# 在全域性作用域下列印,則他們獲取的都是全域性作用域的所有的內容。
a = 2
b = 3
print(globals())
print(locals())
'''
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001806E50C0B8>, 
'__spec__': None, '__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>, 
'__file__': 'D:/lnh.python/py project/teaching_show/day09~day15/function.py',
'__cached__': None, 'a': 2, 'b': 3}
'''

# 在區域性作用域中列印。
a = 2
b = 3
def foo():
c = 3
print(globals()) # 和上面一樣,還是全域性作用域的內容
print(locals()) # {'c': 3}
foo()

​2.3 高階函式(函式的巢狀)

​ 其實我們見到了巢狀這個詞不陌生,之前我們講過列表的巢狀,列表的巢狀就是一個列表中還有列表,可能那個列表中還有列表......那麼顧名思義,函式的巢狀,就是一個函式中,還有函式。

​ 想要玩明白函式的巢狀,關鍵點:只要遇見了函式名+()就是函式的呼叫. 如果沒有就不是函式的呼叫,吃透這一點就算明白了。那麼我們舉例練習:找同學依次說出下面程式碼的執行順序

# 例1:
def func1():
    print('in func1')
    print(3)
def func2():
    print('in func2')
    print(4)
func1()
print(1)
func2()
print(2)

# 例2:
def func1():
print('in func1')
print(3)
def func2():
print('in func2')
func1()
print(4)
print(1)
func2()
print(2)
# 例3:
def fun2():
print(2)
def fun3():
print(6)
print(4)
fun3()
print(8)
print(3)
fun2()
print(5)

2.4 關鍵字:global、nonlocal

global

講這個關鍵字之前,先給大家看一個現象:

a = 1
def func():
    print(a)
func()
a = 1
def func():
    a += 1 # 報錯
func()

​ 區域性作用域對全域性作用域的變數(此變數只能是不可變的資料型別)只能進行引用,而不能進行改變,只要改變就會報錯,但是有些時候,我們程式中會遇到區域性作用域去改變全域性作用域的一些變數的需求,這怎麼做呢?這就得用到關鍵字global:
global第一個功能:在區域性作用域中可以更改全域性作用域的變數。

count = 1
def search():
    global count
    count = 2
search()
print(count)

利用global在區域性作用域也可以宣告一個全域性變數。

def func():
    global a
    a = 3
func()
print(a)

所以global關鍵字有兩個作用:

1,宣告一個全域性變數。

2,在區域性作用域想要對全域性作用域的全域性變數進行修改時,需要用到 global(限於字串,數字)。

nonlocal

nonlocal是python3x新加的功能,與global用法差不多,就是在區域性作用域如果想對父級作用域的變數進行改變時,需要用到nonlocal,當然這個用的不是很多,瞭解即可。

def add_b():
    b = 42
    def do_global():
        b = 10
        print(b)
        def dd_nonlocal():
            nonlocal b
            b = b + 20
            print(b)
        dd_nonlocal()
        print(b)
    do_global()
    print(b)
add_b()

nonlocal關鍵字舉例

nonlocal的總結:

1,不能更改全域性變數。

2,在區域性作用域中,對父級作用域(或者更外層作用域非全域性作用域)的變數進行引用和修改,並且引用的哪層,從那層及以下此變數全部發生改變。