《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在函式定義的時候,預設引數的值就被計算了出來,形參只要不指向新的值,它就會一直指向這個預設值,但如果這個預設值是個可變物件,就會出現上述情況。
要修正上述例子,可以使用None
,str
之類的不變物件。如下:
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),只是最後對元組進行了解包,然後對a
,b
進行了平行賦值。
# 程式碼:
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()
函式除了傳入first
和last
這兩個必選引數之外,還必須且只能傳入age
和country
這兩個引數(一個不多,一個不少)時,則需要用到命名關鍵字引數,它使用一個星號分隔必選引數和命名關鍵字引數,如下:
# 程式碼:
# 星號後面的為命名關鍵字引數
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
複製程式碼
這裡c
,d
為命名關鍵字引數,並且前面也不用加單個星號進行區分了,但是,如果和可變數量關鍵字引數(**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 ~