Python學習之路7-函式

VPointer發表於2018-05-29

《Python程式設計:從入門到實踐》筆記。

本章主要介紹Python中函式的操作,包括函式的概念,定義,如何傳參等,最後還有小部分模組的概念。

1. 定義函式

1.1 一般函式

函式是帶名字的程式碼塊,該程式碼塊是完成特定工作的固定程式碼序列。如果程式中多次出現相同或相似的程式碼塊,則應將這段程式碼提取出來,編寫成函式,然後多次呼叫。通過編寫函式可以避免重複工作,使程式的編寫、閱讀、測試和修復更容易。**請使用描述性的函式名來命名函式,以大致表明函式的功能,這樣即使沒有註釋也能容易理解。函式名應儘量只有小寫字母和下劃線。**以下是兩個最基本的函式,有引數與無參函式:

# 定義無參函式
def greet_user1():
    """顯示簡單的問候語"""
    print("Hello!")

# 定義有參函式
def greet_user2(username):
    """顯示簡單的問候語"""
    print("Hello, " + username.title() + "!")

# 呼叫函式
greet_user1()
greet_user2("jesse")

# 結果:
Hello!
Hello, Jesse!
複製程式碼

在呼叫函式前,必須先定義函式!即函式的定義部分必須在呼叫語句之前。 上述程式碼中的三引號字串叫做文件字串,他們既可以被用作程式碼註釋,也可用於自動生成有關程式中函式的文件。

實參和形參 這兩個概念經常被搞混,函式定義中的引數叫做形參,比如上述函式greet_user2(username)中的username就是形參;傳遞給函式的引數叫做實參,比如在呼叫greet_user2("jesse")時的"jesse"就是實參。

1.2 空函式

如果想定義一個什麼都不做的函式,可以使用pass語句

def do_nothing():
    pass
複製程式碼

如果為了讓程式能跑起來,但暫時又不寫這個函式,可以使用pass語句。這裡pass用作佔位符。

2. 傳遞引數

2.1 位置引數(必選引數)

這就是要求實參的順序和形參的順序相同。

# 程式碼:
def describe_pet(animal_type, pet_name):
    """顯示寵物的資訊"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")

describe_pet("hamster", "harry")
describe_pet("dog", "willie")

# 結果:
I have a hamster.
My hamster's name is Harry.

I have a dog.
My dog's name is Willie.
複製程式碼

對於位置引數,應該注意實參的傳遞順序,如果順序不對,結果會出乎意料:有可能報錯,如果不報錯,函式所要表達的意思可能改變。

# 程式碼:
describe_pet("willie", "dog")

# 結果:
I have a willie.           # 尷尬
My willie's name is Dog.
複製程式碼

2.2 關鍵字引數(傳實參時)

如果函式的形參過多,則很難記住每個位置的引數是用來幹什麼的,如果用鍵值對的方式傳遞實參,這個問題就能迎刃而解,這就是關鍵字引數。在傳遞引數時,直接將形參與實參關聯,這樣就不用在意實參的位置,依然以上述程式碼為例,函式定義不變:

# 程式碼:
describe_pet(animal_type="hamster", pet_name="harry")
describe_pet(pet_name="harry", animal_type="hamster")

# 結果:
I have a hamster.
My hamster's name is Harry.

I have a hamster.
My hamster's name is Harry.
複製程式碼

請注意,這是一種傳遞引數的方法!在呼叫函式時使用!

2.3 預設引數(定義函式時,形參)

編寫函式時可以為每個形參指定預設值,給形參指定了預設值之後,在呼叫函式時可以省略相應的實參。使用預設值可以簡化函式呼叫,也可清楚地指出函式的典型用法。比如上述describe_pet()函式如果給形參animal_type指定預設值“dog”,則可以看出這個函式主要是用來描述狗這種寵物的。

# 程式碼:
def describe_pet(pet_name, animal_type="dog"):
    """顯示寵物的資訊"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")


describe_pet(pet_name="willie")
describe_pet("happy")
describe_pet("lili", "cat")

# 結果:
I have a dog.
My dog's name is Willie.

I have a dog.
My dog's name is Happy.

I have a cat.
My cat's name is Lili.
複製程式碼

在函式呼叫時,如果給形參提供了實參,Python將使用指定的實參;否則將使用形參的預設值。 注意:預設引數是在函式定義時使用!在定義函式時帶有預設值的形參必須在沒有預設值的形參後面!

還有一點值得注意:**預設引數必須指向不變物件!**請看以下程式碼:

# 程式碼:
def add_end(temp=[]):
	"""在傳入的列表最後新增“end”"""
    temp.append("end")
    return temp


print(add_end([1, 2, 3]))
print(add_end(["a", "b", "c"]))
print(add_end())
print(add_end())
print(add_end())

# 結果:
[1, 2, 3, 'end']
['a', 'b', 'c', 'end']
['end']
['end', 'end']
['end', 'end', 'end']
複製程式碼

當給這個函式傳遞了引數時,結果是正確的,而且,在沒有傳遞引數且第一次呼叫時,返回結果也是正確的,然而,沒有傳遞引數且第二次、第三次呼叫時,結果則成了問題。這是因為,Python在函式定義的時候,預設引數的值就被計算了出來,形參只要不指向新的值,它就會一直指向這個預設值,但如果這個預設值是個可變物件,就會出現上述情況。 要修正上述例子,可以使用Nonestr之類的不變物件。如下:

def add_end(temp=None):
    """在傳入的列表最後新增“end”"""
    if temp is None:
        temp = []
    temp.append("end")
    return temp

print(add_end())
print(add_end())

# 結果:
['end']
['end']
複製程式碼

補充--設計不變物件的原因: ①物件一旦建立則不可修改,可以減少因修改資料而產生的錯誤; ②由於物件不可修改,在多工環境下不需要加鎖,同時讀不會出錯。所以,我們在設計一個物件時,能設計成不變物件則設計成不變物件。

3. 返回值

3.1 返回簡單值

函式並非總是直接顯示輸出,它可以處理一些資料並返回一個或一組值。在Python的函式中,使用return語句來返回值。以下是一個引數可選的帶有返回值的函式例子:

# 程式碼:
def get_formatted_name(first_name, last_name, middel_name=""):
    """返回標準格式的姓名"""
    if middel_name:
        full_name = first_name + " " + middel_name + " " + last_name
    else:
        full_name = first_name + " " + last_name

    return full_name.title()


musician = get_formatted_name("jimi", "hendrix")
print(musician)

musician = get_formatted_name("john", "hooker", "lee")
print(musician)

# 結果:
Jimi Hendrix
John Lee Hooker
複製程式碼

3.2 返回字典

Python函式可以返回任何型別的值,包括列表和字典等複雜的資料結構。

# 程式碼:
def build_person(first_name, las_name, age=""):
    """返回一個字典,其中包含一個人的資訊"""
    person = {"first": first_name, "last": las_name}
    if age:
        person["age"] = age
    return person


musician = build_person("jimi", "hendrix", age=27)
print(musician)

# 結果:
{'first': 'jimi', 'last': 'hendrix', 'age': 27}
複製程式碼

3.3 返回多個值

return語句後面用逗號分隔多個值,則可返回多個值:

# 程式碼:
def return_mult():
    return 1, 2

a, b = return_mult()
print("a = " + str(a) + "\nb = " + str(b))

# 結果:
a = 1
b = 2
複製程式碼

但其實這是個假象,其實函式返回的是一個元組(Tuple),只是最後對元組進行了解包,然後對ab進行了平行賦值。

# 程式碼:
print(return_mult())

# 結果:
(1, 2)
複製程式碼

如果函式返回多個值,但有些值並不想要,則這些位置的值可以用下劃線_進行接收:

def return_mult():
    return 1, 2, 3
    
a, _, _ = return_mult()
複製程式碼

4. 傳遞列表

將列表傳遞給函式,函式可以直接訪問其內容或對其進行修改。用函式處理列表可以提高效率。 以下程式碼是一個列印程式,將未列印的設計在列印後轉移到另一個列表中,此程式碼中未使用函式:

# 程式碼:
# 未列印列表
unprinted_designs = ["iphone case", "robot pendant", "dodecahedron"]
completed_models = []

# 模擬列印過程,知道沒有未列印的設計為止,並將已列印的設計移動到“完成列表”
while unprinted_designs:
    current_design = unprinted_designs.pop()

    # 模擬列印過程
    print("Printing model: " + current_design)
    completed_models.append(current_design)

print("\nThe following models have been printed:")
for completed_model in completed_models:
    print(completed_model)

# 結果:
Printing model: dodecahedron
Printing model: robot pendant
Printing model: iphone case

The following models have been printed:
dodecahedron
robot pendant
iphone case
複製程式碼

現在用兩個函式來重組這些程式碼:

# 兩個函式:
def print_models(unprinted_designs, completed_models):
    """模擬列印過程,知道沒有未列印的設計為止,並將已列印的設計移動到“完成列表”"""
    while unprinted_designs:
        current_design = unprinted_designs.pop()

        # 模擬列印過程
        print("Printing model: " + current_design)
        completed_models.append(current_design)


def show_completed_models(completed_models):
    print("\nThe following models have been printed:")
    for completed_model in completed_models:
        print(completed_model)

# 主程式程式碼:
unprinted_designs = ["iphone case", "robot pendant", "dodecahedron"]
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
複製程式碼

從以上程式碼可以看出,使用了函式後,主程式變為了短短四行。 相比於沒有使用函式的程式碼,使用了函式後程式碼更易讀也更容易維護。 在編寫函式時,儘量每個函式只負責一項功能,如果一個函式負責的功能太多,應將其分成多個函式。同時,函式裡面還能呼叫另一個函式;函式裡也能再定義函式!

禁止函式修改列表: 有時候需要禁止函式修改列表,以上述程式碼為例,print_models()函式在執行完成後清空了未列印列表unprinted_design,但有時我們並不希望這個列表被清空,而是留作備案。為解決此問題,可以向函式傳遞副本而不是原件,如下:

# 不用改變函式定義,在函式呼叫時使用切片操作:
print_models(unprinted_designs[:], completed_models)
複製程式碼

如果從C/C++的角度來看(沒有研究過Python底層程式碼,這裡僅是猜測),實參unprinted_designs是一個指標,當他傳遞給函式時,形參得到了這個變數的一個拷貝,形參也指向了記憶體中的那片區域,所以能直接修改。而當使用切片傳遞拷貝時,Python先在記憶體中複製一遍實參unprinted_designs指向的資料,並給這片資料的地址賦給一個臨時的變數,然後再將這個臨時變數傳遞給形參。

5. 傳遞任意數量的引數

5.1 結合使用位置引數(必選引數)和任意數量引數(*args)

有時候你並不知道要向函式傳遞多少個引數,比如製作披薩,你不知道顧客要多少種配料,此時使用帶一個星號*的形參,來定義函式:

# 程式碼:
def make_pizza(*toppings):
    """列印顧客點的所有配料"""
    print(toppings)
    
make_pizza()    # 不傳引數
make_pizza("pepperoni")
make_pizza("mushrooms", "green peppers", "extra cheese")

# 結果:
()
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
複製程式碼

從結果可以看出,以可變引數的方式傳入值時,Python將值封裝成了一個元組,即使是隻傳入了一個值。

補充:多個引數都在一個列表裡面,如果一個元素一個元素的傳遞,則程式碼會很難看,可以使用如下方式傳遞引數,任以上述make_pizza()函式為例:

toppings = ["mushrooms", "green peppers", "extra cheese"]
make_pizza(*toppings)    # 這裡是在執行函式,而不是在定義函式!
複製程式碼

在後面的“任意關鍵字引數”小節中,也可用這種方式傳值,只不過得用雙星號**

注意:如果要讓函式接收不同型別的引數,必須將可變引數放在最後,因為Python先匹配位置引數和關鍵字引數,再將剩餘的引數封裝到最後一個可變引數中。

# 程式碼:
def make_pizza(size, *toppings):
    """概述要製作的披薩"""
    print("\nMaking a " + str(size) + "-inch pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping)

make_pizza(16, "pepperoni")
make_pizza(12, "mushrooms", "green peppers", "extra cheese")

# 結果:
Making a 16-inch pizza with the following toppings:
- pepperoni

Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
複製程式碼

5.2 使用任意數量的關鍵字引數(**kw)

有時候需要傳入任意數量的引數,並且還要知道這些引數是用來幹什麼的,此時函式需要能夠接受任意數量的關鍵字引數,這裡使用雙星號**來實現:

# 程式碼:
def build_profile(first, last, **user_info):
    print(user_info)

    """建立一個字典,其中包含我們知道的有關使用者的一切"""
    profile = {}
    profile["first_name"] = first
    profile["last_name"] = last
    for key, value in user_info.items():
        profile[key] = value
    return profile

user_profile = build_profile("albert", "einstein", location="princeton", field="physics")
print(user_profile)

# 結果:
{'location': 'princeton', 'field': 'physics'}
{'first_name': 'albert', 'last_name': 'einstein', 'location': 'princeton', 'field': physics'}
複製程式碼

從上述結果可以看出,Python將任意關鍵字引數封裝成一個字典。這裡也要注意,指示任意關鍵字引數的形參必須放到最後!

區分---命名關鍵字引數(也叫命名引數): 上述程式碼可以傳遞任意數量的關鍵字引數,但有時需要限制傳入的關鍵字引數,比如上述build_profile()函式除了傳入firstlast這兩個必選引數之外,還必須且只能傳入agecountry這兩個引數(一個不多,一個不少)時,則需要用到命名關鍵字引數,它使用一個星號分隔必選引數和命名關鍵字引數,如下:

# 程式碼:
# 星號後面的為命名關鍵字引數
def build_profile(first, last, *, age, country="China"):
    """建立一個字典,其中包含我們知道的有關使用者的一切"""
    profile = {}
    profile["first_name"] = first
    profile["last_name"] = last
    profile["age"] = age
    profile["country"] = country
    return profile


print(build_profile("albert", "einstein", country="USA", age=20))
print(build_profile("albert", "einstein", age=20))
print(build_profile(age=20, country="USA", first="albert", last="einstein"))
# print(build_profile("albert", "einstein"))

# 結果:
# 如果不看最後一個print(),則程式碼的執行結果如下:
{'first_name': 'albert', 'last_name': 'einstein', 'age': 20, 'country': 'USA'}
{'first_name': 'albert', 'last_name': 'einstein', 'age': 20, 'country': 'China'}
{'first_name': 'albert', 'last_name': 'einstein', 'age': 20, 'country': 'USA'}

# 如果將最後一個print()的註釋去掉,則程式碼會報錯
複製程式碼

從以上結果可以看出命名關鍵字引數必須每個都賦值,可以有預設值,有預設值的可以不用再賦值;命名關鍵字之間可以交換順序,如果要和前面的必選引數也交換順序,則必須使用關鍵字引數的方式傳遞實參。

為什麼有命名關鍵字引數: (網上搜的答案,個人暫時認為這種引數可以被位置引數給替換掉)命名引數配合預設引數使用可以簡化程式碼,比如在寫類的建構函式時,有10個引數,8個有合理的預設值,那麼可以將這8個定義為命名關鍵字引數,前兩個就是必須賦值的位置引數。這樣,在後面生成物件時,如果要替換預設值: ①要麼按順序給後面8個引數替換預設值(C++做法); ②要麼用關鍵字引數的傳值方式給這8個關鍵字不一定按順序來賦值(Python做法); ③要麼混合①②的做法,不過容易混淆。(也就是全用必選引數,前面一部分按順序賦值,後面一部分用關鍵字引數賦值)

一點感想:但如果是筆者自己寫程式碼,暫時更偏向於全用必選引數,帶預設值,即如下定義形式:

def func(a, b, c="test1", d="test2", e="test3", f="test4", g="test5"):
    pass
複製程式碼

而不是如下形式:

def func(a, b, *, c="test1", d="test2", e="test3", f="test4", g="test5"):
    pass
複製程式碼

可能筆者才疏學淺,暫時還沒領會到這種方式的精髓之處。 不過上述是沒有可變引數的情況,如果是以如下形式定義函式:

def func(a, b="test", c="test2", *args):
    pass
複製程式碼

在以如下形式呼叫時則會報錯:

func("test1", c="test3", b="test2", "test4")

# 結果:
SyntaxError: positional argument follows keyword argument
複製程式碼

可以看出,Python在這裡將test4解釋為了位置引數,但筆者是想將其作為可變引數。所以筆者推測,在以下情況時,使用命名關鍵字引數比較好: 必選引數數量不少(其中有些引數的預設值不常變動),後面又跟有可變引數,由於必選引數很多,不容易記住位置,如果不用命名引數,按照上述關鍵字方式呼叫函式則會出錯,所以此時將這些有合理預設值的必選引數變為命名關鍵字引數,則可以使用關鍵字引數不按順序傳值。但如果沒有可變引數時,筆者還是傾向於使用帶預設值的必選引數。

還有一點值得注意:命名關鍵字引數可以和可變引數(*args)混用,此時語法如下:

def func(a, b, *agrs, c, d):
    pass
複製程式碼

這裡cd為命名關鍵字引數,並且前面也不用加單個星號進行區分了,但是,如果和可變數量關鍵字引數(**kw)進行混用,命名關鍵字不能在可變數量關鍵字引數之前,即不存在如下函式定義形式:

def func(a, b, **kw, c, d):
    pass
複製程式碼

如果這樣定義,Pycharm會標紅(其他IDE沒用過,不知道提不提示)。

綜上所述:Python中一共有五中引數型別,即必選引數(位置引數),預設引數(帶預設值的引數),可變引數(*args),命名關鍵字引數和關鍵字引數(數量可變,**kw),這五種可以同時混用,但是必須遵照如下順序: (從左到右)必選引數、預設引數、可變引數、命名關鍵字引數和關鍵字引數。以下是這兩個引數混用的幾個例子:

def func1(a, b, *, c, d, **kw):
    """
    a, b 為必選引數
    c, d 為命名關鍵字引數
    kw 為關鍵字引數,可包含多對
    """
    pass


def func2(a, b="test", *args, c, d="test2", **kw):
    """
    :param a: 必選引數
    :param b: 帶預設值的必選引數
    :param args: 可變引數
    :param c: 命名關鍵字引數
    :param d: 帶預設值的命名關鍵字引數
    :param kw: 關鍵字引數,可包含多對 
    """
    pass
複製程式碼

常用的包含任意數量關鍵字,且不區分引數型別的函式定義方式如下

def func(*args, **kw):
    pass

def func(*args, **kwargs):
    pass
複製程式碼

6. 將函式儲存在模組(Module)中

在python中,一個.py檔案就是一個模組。使用模組的最大好處就是提高了程式碼的可維護性。其次,程式碼不用從零開始編寫,一個模組編寫完成後,可以在其他地方被呼叫。再次,可以避免函式名和變數名衝突,不同模組可以有相同的函式名和變數名。

6.1 匯入整個模組

要讓函式是可以匯入的,得先建立模組。以上述make_pizza()函式為例,將其餘程式碼刪掉,只保留這一個函式,然後再在當前目錄中建立一個making_pizzas.py的檔案,執行如下程式碼以匯入整個模組:

# making_pizzas.py檔案:
import pizza

pizza.make_pizza(12, "mushromms", "green peppers", "extra cheese")
pizza.make_pizza(16, "pepperoni")

# 結果:
Making a 12-inch pizza with the following toppings:
- mushromms
- green peppers
- extra cheese

Making a 16-inch pizza with the following toppings:
- pepperoni
複製程式碼

以這種方式匯入模組時,按如下方式呼叫函式:

module_name.function_name()
複製程式碼

6.2 匯入某模組中特定的函式

語法結構為:

# 匯入一個函式
from module_name import function_name

# 匯入多個函式,逗號分隔
from module_name import func1, func2, func3

# 以此方式匯入模組式,直接以函式名呼叫函式,前面不用加模組名
複製程式碼

仍以上述pizza.py為例:

from pizza import make_pizza

make_pizza(12, "mushromms", "green peppers", "extra cheese")
make_pizza(16, "pepperoni")
複製程式碼

6.3 模組補充

別名: 當函式名發生衝突,或者函式名、模組名太長時,可以取一個簡短的名稱,類似“外號”,以上述程式碼為例:

# 函式取別名
from pizza import make_pizza as mp

mp(12, "mushromms", "green peppers", "extra cheese")
mp(16, "pepperoni")

# -------------------- 另一個檔案 ------------------------------
# 模組取別名
import pizza as p
p.make_pizza(12, "mushromms", "green peppers", "extra cheese")
p.make_pizza(16, "pepperoni")
複製程式碼

匯入模組中的所有函式: 例如匯入pizza模組中的所有函式:

from pizza import *

make_pizza(12, "mushromms", "green peppers", "extra cheese")
make_pizza(16, "pepperoni")
複製程式碼

然而,使用並非自己編寫的大型模組時,最好不要採用這種匯入方法,因為如果模組中有函式或變數和你自己寫的函式、變數同名,結果將有問題。所以,一般的做法是要麼只匯入你需要的函式,要麼匯入整個模組並用句點表示法。

: Python中的包就是一個資料夾,但這個資料夾下面必須包含名為__init__.py的檔案(前後都是雙下劃線),包中可以放多個模組,組織結構與Java包類似。


迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~

Python學習之路7-函式

相關文章