使用者:Xyy23330121/Python/函數

來自維基學院


定義函數[編輯 | 編輯原始碼]

在 Python 中,使用 def 關鍵詞來定義函數。以下是一種簡單的定義函數和調用的方式:

def func():  #定义函数。函数名为"func"。
    print("运行了函数:func()")  #调用函数时,执行的语句组

func()       #调用函数,此处输出:运行了函数:func()

可以看出,對於需要多次執行的操作,使用函數可以縮減代碼。如果要對這種操作進行更改,直接對函數進行更改也更容易。函數是增加代碼可維護性、使代碼簡潔易讀的手段。

帶參數的函數[編輯 | 編輯原始碼]

def func1(arg):  #定义函数,带有参数 arg 。
    print("运行了函数:func1(),参数为", arg)
def func2(arg1, arg2):  #定义函数,带有参数 arg1, arg2 。
    print("运行了函数:func2(),参数为", arg1, arg2)

在函數名後面的括號內,可以添加任意個參數。這相當於在調用函數時,用傳入的參數執行了賦值語句。如果調用上例中的 func1 函數:

func1("传入的参数字符串")

其運行結果,等同於:

arg = "传入的参数字符串"
print("运行了函数:func1(),参数为", arg)

注意,此時定義的參數是必須傳入的,如果不傳入,則會報錯。我們看以下示例:

func1()   #报错:TypeError: func1() missing 1 required positional argument

參數與作用域[編輯 | 編輯原始碼]

作用域規則[編輯 | 編輯原始碼]

任何定義在函數中的參數,或在函數中被賦值的變量,都無法用在函數外。這些變量被稱作「局部變量」。在函數中,對變量賦值時,會自動設置變量為「局部變量」,對函數外的變量無影響。我們看以下幾個示例:

外部變量可以應用於內部,內部變量無法應用於外部[編輯 | 編輯原始碼]

def argFunc(arg):
    var = 0   #在函数内,给变量 var 赋值
    print(a)  #使用了函数外的变量 a 
    print(arg)

a = 1
argFunc(123)   #输出:1。
print(var)     #报错:NameError: name 'var' is not defined.
print(arg)     #报错:NameError: name 'arg' is not defined.

可以看出,函數內賦值的變量,或函數的參數不可以用於函數外。而在函數調用前賦值的函數外變量可以使用於函數內。

在內部對變量賦值,對外部變量無影響[編輯 | 編輯原始碼]

def argFunc2():
    a = 0
    print(a)

a = 1
argFunc2()  #输出:0
print(a)    #输出:1

特別情況[編輯 | 編輯原始碼]

只要在函數內,有給變量 a 賦值的語句。變量 a 就會被視為局部變量,而非外部變量。

def argFunc3():
    print(a)
    a = 0

a = 1
argFunc3()    #报错:UnboundLocalError: cannot access local variable 'a' where it is not associated with a value

在執行 argFunc3 的時候,print(a) 查詢了局部變量 a 的值。但此時局部變量 a 尚未賦值,從而導致了報錯。

作用域查詢變量的規則[編輯 | 編輯原始碼]

上面已經說明了幾種作用域的簡單情況,這裡討論更複雜的情況。

在查詢變量名時,會先查詢同層作用域是否有同名變量,隨後查詢上一層作用域。如果上一層作用域沒有,會再查詢更上一層。我們看以下示例:

a = b = c = "Global"

def first():
    b = c = "First"
    def second():
        c = "Second"
        print(a,b,c)
    second()

first()    #输出:Global First Second

在輸出時,c 優先輸出定義在 second 中的版本;而 b 由於在 second 中沒有,所以往上一層,輸出定義在 first 中的版本;而 a 由於在 first 中也沒有,從而輸出了全局版本。

作用域和 def 子句的位置相關,而不與運行函數的位置相關。我們把上面示例改為:

a = b = c = "Global"

def first():
    b = c = "First"
    second()
    
def second():
    c = "Second"
    print(a,b,c)

first()    #输出:Global Global Second

可以發現輸出改變了。這是因為 second 的上一層直接就是全局作用域。因此,在運行 second 時,根本就沒查詢 first 的作用域。

global 語句[編輯 | 編輯原始碼]

global 語句用於說明變量為全局變量。

a = b = c = d = "Global"

def first():
    b = c = d = "First"
    def second():
        global c,d
        c = d = "Second"
    second()
first()
print(a,b,c,d)  #输出:Global Global Second Second

通過最後的輸出,我們看出函數直接對全局變量進行了更改。

對於 global 語句聲明的變量,不能在 global 語句之前調用該變量。以下的示例會報錯:

def causeError():
    a = 1
    Global a

causeError()

nonlocal 語句[編輯 | 編輯原始碼]

nonlocal 語句用於說明變量不是局部變量。此時,Python 會按照查詢規則,將變量設置為查詢到的變量——到查詢到全局作用域之前為止。我們看以下示例:

a = b = "Global"
def first():
    a = b = "First"
    def second():
        b = "Second"
        def third():
            nonlocal a,b
            a = b = "Third"
        third()
        print(a,b)
    second()
    print(a,b)

first()
print(a,b)

輸出為:

Third Third
Third First
Global Global

third 中,遇到的各變量與作用域如下:

全局 first 的作用域 second 的作用域
a "Global" "First"
b "Global" "First" "Second"

nonlocal 語句查詢到 first 中的 asecond 中的 b——從而之後更改的也是這兩個。於是,各變量及作用域變為如下的情況:

全局 first 作用域 second 作用域
a "Global" "Third"
b "Global" "First" "Third"

所以輸出會變成上面那樣。

注意,nonlocal 語句不會查詢全局作用域。如果一個變量只有全局作用域才有,此時使用 nonlocal 會報錯。

global 語句相同,nonlocal 語句聲明的變量,不能在 nonlocal 語句之前調用該變量。

return 語句[編輯 | 編輯原始碼]

return 語句可以返回一個值,從而向函數外傳遞信息。

def argFunc4():
    return 0

a = argFunc5()
print(a)    #输出:0

由於返回的可以是元組、列表、字典等任何數據結構。所以想返回多少個信息就能返回多少個信息。

特殊:可變序列[編輯 | 編輯原始碼]

函數在對外部的可變序列變量進行更改(而非直接賦值)時,在函數內的更改會表現在函數外。比如:

def add1(s):
    s.append(1)

iter = [0, 0]
add1(iter)
print(iter)    #输出:[0, 0, 1]

def badAdd1(s):
    s = s + [1]

badAdd1(iter)
print(iter)    #输出:[0, 0, 1]

由於函數 badAdd1 裡面是一個賦值語句,所以對外部的序列變量沒有影響。而 Add1 裡面是一個更新列表的語句(而非直接賦值),所以它可以影響到外部的序列變量。

傳入參數的方法[編輯 | 編輯原始碼]

我們考慮以下函數:

def func3(arg1, arg2, arg3):
    print(arg1, arg2, arg3)

傳入參數的簡單方法[編輯 | 編輯原始碼]

該函數有幾種傳入參數的方法。

按順序傳入[編輯 | 編輯原始碼]

我們可以不管變量名,直接按順序傳入參數。Python 會按順序給參數賦值。

func3(1,2,3)  #输出:1 2 3

按關鍵字傳入[編輯 | 編輯原始碼]

可以按參數名傳入參數。此時,Python 會根據參數名給參數賦值。

func3(arg1 = 1, arg2 = 2, arg3 = 3)  #输出:1 2 3
func3(arg2 = 2, arg3 = 3, arg1 = 1)  #输出:1 2 3

混合按序、按關鍵字傳入[編輯 | 編輯原始碼]

func3(1, arg3 = 3, arg2 = 2)  #输出:1 2 3
func3(1, 2, arg3 = 3)         #输出:1 2 3

可以混合上面的方法來傳入參數。此時,按序傳入的參數必須在按關鍵字傳入的參數前面。

參數解包[編輯 | 編輯原始碼]

除上述方法外,以下還有兩種方法,可以分別變換為按順序傳入與按關鍵字傳入。

解包參數序列[編輯 | 編輯原始碼]

有時,我們希望把一個序列中的所有元素分別作為參數按序傳入到函數中。此時,我們可以使用:

a = [1,2]
func3(*a,3)  #输出:1 2 3

在調用函數時,*a,3等同於1,2,3

解包參數字典[編輯 | 編輯原始碼]

類似,如果需要把一個字典中的所有鍵-值對分別作為參數傳入到函數中。我們可以使用:

s = {arg2: 2, arg3: 3}
func3(arg1 = 1, **s}  #输出:1 2 3

處理參數的方法[編輯 | 編輯原始碼]

規定傳入方式[編輯 | 編輯原始碼]

規定按順序傳入[編輯 | 編輯原始碼]

若在參數列表中加入 / ,則位於 / 之前的參數只能按序傳入。比如,以下的代碼會報錯:

def func4(arg1, arg2, /, arg3):
    print(arg1, arg2, arg3)

func4(arg1 = 1, arg2 = 2, arg3 = 3)  #报错:TypeError: func4() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

規定按關鍵字傳入[編輯 | 編輯原始碼]

若在參數列表中加入 * ,則位於 * 之後的參數只能按關鍵字傳入。比如,以下的代碼會報錯:

def func4(arg1, arg2, *, arg3):
    print(arg1, arg2, arg3)

func4(1, 2, 3)  #报错:TypeError: func4() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

同時使用[編輯 | 編輯原始碼]

帶默認值的參數(可選參數)[編輯 | 編輯原始碼]

def func5(arg1, arg2=2, arg3):
    print(arg1, arg2, arg3)

func5(1, arg3 = 3)  #输出:1 2 3

帶默認值的參數可以不傳入。如果不傳入,則會直接使用默認值。這裡 arg2 帶有默認值 2,從而不用傳入它。

在,本例中,如果想只傳入兩個參數,只用按順序傳入會導致 arg3 不被傳入任何值,從而導致報錯。因此,如果只傳入兩個參數,此處的 arg3 必須是用關鍵字傳入的。

一般情況下,帶默認值的參數會放在必須傳入的參數後。這是為了在不傳入可選參數的情況下,必須的參數也可以選擇傳入方式。

參數打包[編輯 | 編輯原始碼]

元組[編輯 | 編輯原始碼]

我們看以下示例:

def func6(arg, *args):
    print(args)

func6(1, 2, 3)  #输出:(2, 3)

可以看出,在按序傳入參數1之後,對於多出來的、按序傳入的參數,Python 把它們弄成一個元組,傳入了*後面跟着的參數名稱args裡面。

字典[編輯 | 編輯原始碼]

我們看以下示例:

def func7(arg, **kwargs):
    print(kwargs)

func7(0, one = 1, two = 2)  #输出:{'one': 1, 'two': 2}

可以看出,在按序傳入參數1之後,對於多出來的、按關鍵字傳入的參數,Python 把它們弄成一個字典,傳入了**後面跟着的參數名稱args裡面。

參數打包的提示[編輯 | 編輯原始碼]

由於元組和字典打包的是不同傳入方式的參數,一個函數在處理時可以同時打包元組和字典。同時,按關鍵字傳入的參數可以打亂順序。因此,以下的方式也是可行的:

def func8(arg0, *args, arg1, **kwargs):
    print(arg0, args, arg1, kwargs)

func8(0, 1, 2, one = 1, arg1 = True, two = 2)
#输出:0 (1, 2) True {'one': 1, 'two': 2}

函數對象[編輯 | 編輯原始碼]

函數對象不過是支持在名稱後添加 () 並輸入參數進行操作的對象:函數也是對象。對於一般對象適用的操作,對函數對象也適用。比如:

函數可以作為參數傳入另一個函數:

def do(func):
    func(1)

def todo(arg):
    print(list(range(arg,10)))

do(todo)  #输出:[1, 2, 3, 4, 5, 6, 7, 8, 9]

以上方式向函數 do 中傳入函數 todo 作為參數。並執行了作為參數的函數。

函數可以打印,或作為變量進行賦值:

def MyFunc(): return "调用了函数"

a = MyFunc
print(a)      #输出:<function MyFunc at *>
print(MyFunc) #输出:<function MyFunc at *>
print(a())    #输出:调用了函数

MyFunc = "更改"
print(a)      #输出:<function MyFunc at *>
print(MyFunc) #输出:更改

賦值後,a 也支持 MyFunc 的一切操作。而 MyFunc 作為變量也可以用賦值語句修改。

函數可以作為返回值:

def getFunc():
	def f(): return "返回的函数"
	return f

a = getFunc()
print(a()) #输出:返回的函数

用於 list.sort()[編輯 | 編輯原始碼]

我們在學習列表的排序時,提到過 list.sort() 方法。注意到該方法在 Python 文檔中寫為: sort(*, key=None, reverse=False)。這代表該函數的所有參數必須是按參數名傳入。

我們將在此處講解該方法中,參數 key 的作用。

參數 key 需要傳入的是一個函數,它以列表中的元素為參數,並返回一個可比較的值。sort 方法將用輸出的、可比較的值來進行排序。比如:

def key0(elem):
    return elem[0]
def key1(elem):
    return elem[1]

s = [(2,2), (0,1), (1,0)]
s.sort(key=key0)  #用 key0 输出的值进行排序
print(s)          #输出:[(0, 1), (1, 0), (2, 2)]
s.sort(key=key1)  #用 key1 输出的值进行排序
print(s)          #输出:[(1, 0), (0, 1), (2, 2)]

裝飾器[編輯 | 編輯原始碼]

注意到有些函數可以取函數作為參數,裝飾器就是這樣的一類函數。以下是一個簡單的裝飾器。

def SimpleDecorator(func):
    return "String"

裝飾器的使用方法如下:

def MyFunc(): pass

MyFunc = SimpleDecorator(MyFunc)

對於需要對一些函數作統一的處理時,可以使用裝飾器。

為了簡便起見,Python 也支持用這種方法:

@SimpleDecorator
def MyFunc(): pass

這種方法和上面的方法沒什麼區別。

lambda 表達式[編輯 | 編輯原始碼]

lambda 表達式可以用於創建簡單函數。我們看以下示例:

square_sum = lambda x,y=0: x**2+y**2

print(square_sum(3,4))  #输出:25

其中,冒號左邊的內容為參數列表,參數列表可以設置默認值。而冒號右邊的內容為函數的返回值。

這對於某些只使用一次的函數很有用。比如上面的 list.sort() 的示例,我們可以改為:

s = [(2,2), (0,1), (1,0)]
s.sort(key=lambda elem: elem[0])  #用 key0 输出的值进行排序
print(s)          #输出:[(0, 1), (1, 0), (2, 2)]
s.sort(key=lambda elem: elem[1])  #用 key1 输出的值进行排序
print(s)          #输出:[(1, 0), (0, 1), (2, 2)]

這種方法除了看起來更簡潔美觀之外,其作用和上面的「定義函數」一模一樣。