Python函式的進階

veelion發表於2019-01-06

上一節我們學習了函式的定義和呼叫,理解了基本的函式知識。本節進一步學習函式相關的更多內容,深入瞭解函式,包括:預設引數、關鍵字引數、位置引數、變數的作用域等等。

Python函式進階

形參和實參的不同

首先,我們先搞清兩個概念:

形參(parameters),是定義函式時宣告的引數名稱,它定義了函式可以接受的引數型別;
實參(arguments),是呼叫函式時傳給函式的實際值。
比如下面的函式定義:

def func(foo, bar=True, **kwargs):
    pass

foo, bar, kwargs 是函式的形參(parameters)。

當我們呼叫func時,比如:

func(20, bar=False, abc='xyz')

傳給函式的值20False'xyz'就是實參。

預設引數值

預設引數就是在函式定義時,給引數一個預設值。呼叫該函式時可以不給有預設值的引數傳值,這樣呼叫時的引數可以減少。

看下面的例子:

def say_hi(name, greeting='Hi', more=''):
    print(greeting, name, more)

這個函式有三個引數,其中greetingmore有預設值,呼叫該函式時,有以下幾種形式:

  • 只傳一個值給name: say_hi(‘Tom’)
  • 給可選引數greeting傳值: say_hi(‘Tom’, ‘Hello’)
  • 給所有引數傳值: say_hi(‘Tom’, ‘Hello’, ‘how are you’)

一個函式可以有任意多個預設引數,也可以全部引數都有預設值。但是,沒有預設值的引數不能在有預設值的引數的後面,否則會報錯。也就是說,引數的先後順序必須是無預設值引數在前,有預設值引數在後面。

In [175]: def foo(a=1, b): 
     ...:     return a + b
  File "<ipython-input-175-f8e0e97b520a>", line 1
    def foo(a=1, b):
           ^
SyntaxError: non-default argument follows default argument

預設引數值只生成一次,如果預設值是可變物件比如list、dict、set等就會出現詭異的結果,使用時要非常留心。下面的例子,預設值為一個空list,函式呼叫時它把第一個引數放到list裡面。

def func(a, L=[]):
    L.append(a)
    return L

print(func(1))
print(func(2))
print(func(3))

程式的輸出是:

[1]
[1, 2]
[1, 2, 3]

這是因為預設值L在函式定義時生成,後面的呼叫(使用預設值,不給L傳值)就會不斷給定義時生成的預設list新增元素。

如果你希望使用這個詭異的特性也沒問題,但要清楚它是什麼。通常我們不建議預設值為可變物件,而是不可變的整數、浮點數、字串等等。

關鍵字引數

呼叫函式時,我們可以使用“關鍵字引數”,它的形式是:kwarg=value。比如前面的say_hi函式的呼叫:

In [180]: say_hi(name='Tom')
Hi Tom 

In [181]: say_hi(name='Tom', greeting='Hello')
Hello Tom 

In [182]: say_hi(name='Tom', more='how are you')
Hi Tom how are you

In [183]: say_hi(more='good day', name='Tom', greeting='Hi')
Hi Tom good day

上面最後一個呼叫告訴我們,關鍵字引數是透過關鍵字來確認引數的,所以可以不用按照函式定義時的順序傳遞引數。

關鍵字引數跟預設引數類似有引數位置的限制,關鍵字引數後面必須都是關鍵字引數。
下面的呼叫都是無效的:

say_hi()  # 缺少必須的引數name
say_hi(name='Tom', 'Hello')  # 關鍵字引數後面出現了非關鍵字引數
say_hi('Tom', name='Tim')  # 同樣的引數傳了兩個值
say_hi(age=10)  # 函式定義中不存在的關鍵字引數

如果函式定義的最後一個引數是兩個星號加名稱:**name,那麼它接受一個字典包含所有關鍵字引數,這個字典不包括name前面宣告的普通引數:

In [190]: def foo(a, **kwargs): 
     ...:     print(a) 
     ...:     for k, v in kwargs.items(): 
     ...:         print('%s : %s' % (k, v)) 
     ...:

In [191]: foo(1)
1

In [192]: foo(a=1)
1

In [193]: foo(a=1, b=2, c=3, d=4, e='a')
1
b : 2
c : 3
d : 4
e : a

可見,**kwargs把a後面的所有關鍵字引數都接收了。這對我們以後寫引數非常多的函式時很有幫助。

可變引數列表

可變引數列表類似關鍵字引數**kwargs,因為它沒有key只有value,所以它是一個序列(確切的說是一個tuple),它的形式是*args,名稱前面有一個星號*,用以接收不確定數量的引數。我們常用的內建函式print就是一個可變引數函式。
下面我自己定義一個可變引數函式:

In [197]: def foo(*args): 
     ...:     print(type(args)) 
     ...:     print('|'.join(args)) 
     ...:

In [198]: foo('a', 'b', 'c', 'd')
<class 'tuple'>
a|b|c|d

同樣的,可變引數後面必須跟關鍵字引數:

In [204]: def foo(*args, joiner='|'): 
     ...:     print(type(args)) 
     ...:     print(joiner.join(args)) 
     ...:

In [205]: foo('a', 'b', 'c', 'd')
<class 'tuple'>
a|b|c|d

In [206]: foo('a', 'b', 'c', 'd', joiner='/')
<class 'tuple'>
a/b/c/d

解包引數列表

這個正好跟可變引數列表相反,如果要呼叫的函式的引數值已經在list或tuple裡面了,我們可以透過解包list或tuple來給函式傳值。比如內建的range()函式可以輸入兩個引數:start和stop,如果它們在一個llist或tuple裡面,可以透過*運算子解包來傳值:

In [207]: list(range(1, 8))
Out[207]: [1, 2, 3, 4, 5, 6, 7]

In [208]: args = [1, 8]

In [209]: list(range(*args))
Out[209]: [1, 2, 3, 4, 5, 6, 7]

同樣的,dict可以透過**運算子傳遞關鍵字引數:

In [212]: d = {'name':'Tom', 'greeting':'Hello', 'more':'good day'}

In [213]: say_hi(**d)
Hello Tom good day

lambda函式

透過關鍵字lambda來實現小的匿名函式。匿名函式返回一個函式物件,在可以作為函式引數傳遞給函式。比如內建函式sorted中的key引數就接受一個函式物件。

In [215]: func = lambda a, b: a * b

In [216]: func(1,2)
Out[216]: 2

In [217]: func(3,5)
Out[217]: 15

再看看sorted使用lambda函式的情況,先對學生按姓名排序,再按年齡排序:

In [218]: students = [ 
     ...: {'name': 'Tom', 'age': 12}, 
     ...: {'name': 'Jack', 'age': 13},  
     ...: {'name': 'Aby', 'age': 10},]

In [219]: sorted(students, key=lambda s: s['name'])
Out[219]: 
[{'name': 'Aby', 'age': 10},
 {'name': 'Jack', 'age': 13},
 {'name': 'Tom', 'age': 12}]

In [220]: sorted(students, key=lambda s: s['age'])
Out[220]: 
[{'name': 'Aby', 'age': 10},
 {'name': 'Tom', 'age': 12},
 {'name': 'Jack', 'age': 13}]

變數的作用域和生命週期

程式中的變數不是在任何地方都可見的,它有自己的作用域。

區域性變數

定義在函式內部的變數只在函式內部可見,也就是說,它是函式的區域性變數。

In [1]: def func(): 
   ...:     x = 'hello' 
   ...:     print(x) 
   ...:

In [2]: func()
hello

In [3]: x
------------------------
NameError        Traceback (most recent call last)
<ipython-input-3-6fcf9dfbd479> in <module>
----> 1 x

NameError: name 'x' is not defined

x是func內部的一個變數,對該函式內部可見,所以print(x)語句能列印x的值。但是在函式外部訪問x時就會報錯:x是未定義的。

全域性變數

相對於區域性變數,全域性變數是定義在函式外部的,它具有全域性作用域。

In [4]: x = 'hello'

In [5]: def func2(): 
   ...:     print(x) 
   ...:

In [6]: func2()
hello

In [7]: x
Out[7]: 'hello'

如果要在函式內部修改全域性變數,就需要用關鍵字global來宣告全域性變數:

In [8]: def func3(): 
   ...:     global x 
   ...:     x += 'world' 
   ...:     print(x) 
   ...:

In [9]: func3()
helloworld

In [10]: x
Out[10]: 'helloworld'

區域性變數變數的生命週期從函式呼叫開始,到函式執行結束為止;全域性變數的生命週期直到整個程式結束為止。

刪除函式

前面的章節中,我們使用關鍵字del來刪除列表或其中的元素,它同樣可以用來刪除函式:

In [11]: def func4(): 
    ...:     print('func4') 
    ...:

In [12]: func4()
func4

In [13]: del func4

In [14]: func4()
-------------------
NameError          Traceback (most recent call last)
<ipython-input-14-0e6ad11a93c1> in <module>
----> 1 func4()

NameError: name 'func4' is not defined

在Python中,函式也是物件,所以用del刪除函式就跟刪除其它物件一樣。

文件字串(docstring)

作為類、函式或模組之內的第一個表示式出現的字串字面值。它在程式碼執行時會被忽略,但會被直譯器識別並放入所在類、函式或模組的 doc 屬性中。由於它可用於程式碼內省,因此是物件存放文件的規範位置。

In [15]: def my_func(): 
    ...:     '''first line is summary of this function 
    ...:      
    ...:     the more lines are details of this function 
    ...:     ''' 
    ...:     pass 
    ...:

In [16]: print(my_func.__doc__)
first line is summary of this function

    the more lines are details of this function

寫docstring的規則一般是這樣的:
(1)第一行簡短概述該函式或類的功能
(2)第二行空白
(3)後面幾行詳細描述函式的引數、返回值等等

總結

定義函式時,引數稱為“形參”,表述引數的型別;呼叫函式時,引數為“實參”,是傳給函式的具體值。

定義函式時,可以為引數指定預設值;呼叫函式時,可以透過關鍵字引數呼叫。

定義函式是,可以指定引數為可變引數列表 *args**kwargs;呼叫函式時,可以透過解包list,tuple和dict來傳入引數。

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章