系列文章目录
第一章 起步 第二章 变量和简单数据类型 第三章 列表简介 第四章 操作列表 第五章 if 语句 第六章 字典 第七章 用户输入和 while 语句 第八章 函数 第九章 类 第十章 文章和异常 第十一章 测试代码
前言
在本章中,你将学习编写函数。函数是带名字的代码块,用于完成具体的工作。要执行函数定义的特定任务,可调用该函数而无需反复编写完成该任务的代码。你将发现,通过使用函数,程序编写、阅读、测试和修复起来都更加容易。
在本章学习中,你将学习向函数传递信息的方式;学习如何编写主要任务是显示信息的函数,以及旨在处理数据并返回一个或一组值的函数;最后,学习如何将函数存储在称为模块的独立文件中,让主程序文件的组织更为有序。
8.1 定义函数
下面是一个打印问候语的简单函数,名为 greet_user():
def greet_user():
"""显示简单的问候语。"""
print("Hello!")
greet_user()
Hello!
本例演示了最简单的函数结构。首先用关键字 def 来告诉 Python ,你要定义一个函数。这里就是函数定义,向 Python 指出了函数名,而圆括号内则是负责指出函数为完成工作需要说明信息,因为这里只是打印一个 “Hello!” ,所以不需要任何信息,括号里是空的(即便如此,括号也是必不可少的)。最后,定义以冒号结尾。
紧跟在 def greet_user(): 后面的所有缩进行构成了函数体。其中用三个引号括起来的文本称为**文档字符串(docstring)**的注释,描述了函数是做什么用的,Python 使用他们来生成有关程序中函数的文档。
代码行 print(“Hello!”) 是函数体内的唯一一行代码,因此 greet_user() 只做一项工作:打印 Hello!。
要使用这个函数,那么就可直接调用它。函数调用让 Python 执行函数的代码。要调用函数,可依次指定函数名以及用圆括号括起来的必要信息,由于这个函数不需要任何信息,调用它只需输入 greet_user() 即可。
8.1.1 向函数传递信息
只需稍作修改,就可以让函数 greet_user() 不仅向用户显示 Hello!,还将用户的名字作为抬头。而这个修改就职在函数定义的括号内添加一个参数 username ,这样就可以让函数接受你给 username 指定的任意值:
def greet_user(username):
"""显示简单的问候语。"""
print(f"Hello, {username.title()}!")
greet_user('jesse')
哝,这样就行啦:
Hello, Jesse!
8.1.2 实参和形参
其实,在前面函数 greet_user() 的定义中,变量 username 是一个形参(parameter),即函数完成工作所需要的信息。在代码 greet_user(‘jesse’) 中,值 ‘jesse’ 是一个实参(argument),即调用函数时传递给函数的信息。
实际上,在调用函数时,将要让函数使用的信息放在圆括号内。在 greet_user(‘jesse’) 中,将实参 ‘jesse’ 传递给了函数 greet_user(),这个值被赋给了形参 username。简单来说,函数定义时使用的是形参,函数调用时使用的是实参。
8.2 传递实参
函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。向函数传递实参的方式很多:可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由变量名和值组成;还可以使用列表和字典。下面依次介绍这些方式,相信你只看示例也就明白的差不多了吧。
8.2.1 位置实参
调用函数时,Python 必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于实参的顺序,这种关联方式就是位置实参。
为明白其中的工作原理,来看一个显示宠物信息的函数,这个函数指出一个宠物属于哪种动物以及它叫什么名字,如下所示:
def describe_pet(animal_type, pet_name):
"""显示宠物的信息。"""
print(f"\nI habve a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry')
输出描述了一只名为 Harry 的仓鼠:
I habve a hamster.
My hamster's name is Harry.
总结:在函数中,可根据需要使用任意数量的位置实参,Python 将按顺序将函数调用的实参关联到函数定义中相应的形参。 最后,还有两点需要强调:
- 多次调用函数——可以根据需要调用函数人一次。要再描述一个宠物,只需再次调用 describe_pet() 即可,所以说,多次调用函数是一种效率极高的工作方式。
- 位置实参的顺序很重要——使用位置实参来调用函数时,如果实参的顺序不正确,结果可能出乎意料,例如在上述程序中得到一个名为 Hamster 的 harry !?😂😂😂
8.2.2 关键字实参
关键字实参是传递给函数的名称值对。因为直接在实参中将名称和值关联起来,所以向函数传递实参是不会混淆,这让你无需考虑函数调用中的实参顺序,还清楚地指出函数调用中各个值的用途。
例如上面介绍宠物的程序,如果要使用关键字参数来调用 describe_pet() ,则只需修改调用函数的部分: describe_pet(animal_type = 'hamster', pet_name='harry')
关键字实参的顺序无关紧要,因为 Python 知道各个值该赋给哪个值赋给哪个形参,但务必准确指定函数定义中的形参名。
8.2.3 默认值
编写函数时,可给每个形参指定默认值,在调用函数中给形参提供实参时,Python 将使用指定的实参值;否则将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参,这使得使用默认值可简化函数调用并清除地指出函数的典型用法。
例如,如果发现调用 describe_pet() 时,描述的大多是小狗,就可将形参 animal_type 的默认值设置为 ‘dog’ 。这样,调用 describe_pet() 来描述小狗时,就可不提供这种信息:
def describe_pet(pet_name, animal_type = 'dog'):
"""显示宠物的信息。"""
print(f"\nI habve a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie')
I habve a dog.
My dog's name is Willie.
请注意,在这个函数的定义中,修改了形参的排列顺序。因为给 animal_type 指定了默认值,无需通过实参来指定动物类型,所以在函数调用中只包含一个实参——宠物的名字。然而,Python 依然将这个实参视为位置实参,因此如果函数调用中只包含宠物的名字,这个实参将关联到函数中定义的第一个形参。这就是需要将 pet_name 放在形参列表开头的原因。
现在,使用这个函数最简单的方式就是在函数调用中只提供小狗的名字:describe_pet('willie') 这个函数调用的输出与前一个示例相同。只提供了一个实参 ‘willie’ ,这个实参将关联到函数定义中的第一个形参 pet_name 。由于没有给 animal_type 提供实参,所以Python 将使用默认值 ‘dog’ 。
如何要描述的动物不是小狗,可使用类似下面的函数调用:describe_pet(pet_name='harry',animal_type = 'hamster' ) 由于显式地给 animal_type 提供了实参,Python 将忽略这个形参的默认值。
注意:使用默认值时,必须先在形参列表中列出没有默认值的形参,再列出由默认值的实参。这让 Python 依然能够正确地解读位置实参。
8.2.4 等效的函数调用
鉴于可混合使用位置实参、关键字实参和默认值,通常有多种等效的函数调用方式。请看下面对函数 describe_pet() 的定义,其中给一个形参提供了默认值:def describe_pet(pet_name, animal_type = 'dog'):
下面对这个函数的调用都可行:
describe_pet('willie')
describe_pet(pet_name='willie')
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type = 'hamster')
describe_pet(animal_type = 'hamster', pet_name='harry')
8.2.5 避免实参错误
其实出现实参不匹配的错误也不必大惊小怪,traceback(Python 错误信息的报告) 会帮你指出来的。
8.3 返回值
函数并非总是直接显示输出,他还可以处理一些数据,并返回一个或一组值。函数返回的值称为返回值。在函数中,可使用 return 语句将值返回到调用函数的代码行。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。
8.3.1 返回简单值
下面来看一个函数,它接受名和姓并返回整洁的姓名:
def get_formatted_name(first_name, last_name):
"""返回整洁的姓名"""
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)
在这里,我们将返回值赋给了变量 musician 。输出为整洁的姓名:
Jimi Hendrix
原本只需编写print("Jimi Hendrix") 这样一个代码就可以输出整洁的姓名,这样一比较,前面做的工作是不是太多了?其实在需要分别存储大量名和姓的大型程序中,向 get_formatted_name() 这样的函数就非常有用。可以分别存储名和姓,每当需要显示姓名时都调用这个函数。
8.3.2 让实参变成可选的
有时候,让实参变成可选的,这样使用函数的人就能只在必要时提供额外的信息,而方法也很简单,就是使用默认值。 例如,假设要扩展函数 get_formatted_name() ,使其同时处理中间名。为此,我们可以将其修改成类似下面这样:
def get_formatted_name(first_name, middle_name, last_name):
"""返回整洁的姓名"""
full_name = f"{first_name} {middle_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'lee', 'hendrix')
print(musician)
但又有问题来了,并非所有人都有中间名,所以一旦用户只提供名和姓两个实参,这个程序将不能正确运行。为了让中间名变成可选的,可给形参 middle_name 指定一个空的默认值,并在用户没有提供中间名时不是用这个形参(将其默认值设为空字符串,并移至末尾):
def get_formatted_name(first_name, last_name, middle_name = ''):
"""返回整洁的姓名"""
if middle_name:
full_name = f"{first_name} {middle_name} {last_name}"
else:
full_name = f"{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
8.3.3 返回字典
函数可以返回任何类型的值,包括列表和字典等较复杂的数据结果。例如,下面的函数接受姓名的组成部分,并返回一个表示人的字典:
def build_person(first_name, last_name):
"""返回一个字典,其中包含有关一个人的信息"""
person = {'first': first_name, 'last': last_name}
return person
musician = build_person('jimi', 'hendrix')
print(musician)
{'first': 'jimi', 'last': 'hendrix'}
这个函数接受简单的文本信息,并将其放在一个更合适的数据结构中,让你不仅能打印这些信息,还能以其他方式处理它们。例如,你可以轻松地扩展这个函数,使其可以接受可选值,如中间名、年龄、职业或其他任何要存储的信息。例如,下面的修改能让你存储年龄:
def build_person(first_name, last_name, age = None):
"""返回一个字典,其中包含有关一个人的信息"""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person
musician = build_person('jimi', 'hendrix', age = 19)
print(musician)
{'first': 'jimi', 'last': 'hendrix', 'age': 19}
在这里的函数定义中,新增了一个可选形参 age ,并将其默认值设置为特殊值 None(表示变量没有值),这里我们可将 None 视为占位值,在条件测试中,None 相当于 False 。
8.3.4 结合使用函数和 while 循环
这一小节我们来看一个示例即可—— 结合使用函数 get_formatted_name() 和 while 循环,以更正式的方式问候用户并使用名和姓跟用户打招呼:
def get_formatted_name(first_name, last_name):
"""返回整洁的姓名"""
full_name = f"{first_name} {last_name}"
return full_name.title()
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")
f_name = input("First name: ")
if f_name == 'q':
break
l_name = input("Last name: ")
if l_name == 'q':
break
formatted_name = get_formatted_name(f_name, l_name)
print(f"\nHello, {formatted_name}!")
Please tell me your name:
(enter 'q' at any time to quit)
First name: eric
Last name: mattes
Hello, Eric Mattes!
Please tell me your name:
(enter 'q' at any time to quit)
First name: q
8.4 传递列表
向函数传递列表是一种很有用的操作,其中包含的可能是名字、数或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问其内容。下面使用函数来提高处理列表的效率。
下面一个示例用函数来问候列表中的每个人:
def greet_users(names):
"""向列表中的每位用户发出简单的问候"""
for name in names:
msg = f"Hello, {name.title()}!"
print(msg)
usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)
Hello, Hannah!
Hello, Ty!
Hello, Margot!
8.4.1 在函数中修改列表
将列表传递给函数后,函数就可以对其进行修改,而在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量数据。
来看一家为用户提交的设计制作 3D 打印模型的公司。需要打印的设计存储在一个列表中,打印后将移到另一个列表中。下面是分别是不适用 / 使用函数的情况下来模拟这个过程的代码:
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"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)
def print_models(unprinted_designs, completed_models):
"""
模拟打印每个设计,直到没有未打印的设计为止
打印每个设计后,都将其移动到列表 completed_models 中
"""
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"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 = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
输出一样:
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case
The following models have been printed:
dodecahedron
robot pendant
phone case
8.4.2 禁止函数修改列表
有时候,需要禁止函数修改列表,例如前面的示例需要保存原来未打印的设计列表以供备案,那该怎么办呢(上述程序完成后 unprinted_designs 列表就变成空列表了)?为解决这个问题,可向函数传递列表的副本而非原件——用切片表示法 [:] 创建列 表的副本:function_name(list_name[:])
在上一个小节中,如何不想清空未打印的设计列表,可像这样调用 print_models() :print_models(unprinted_designs[:], completed_models
虽然向函数传递列表的副本可保留原始列表的内容,但除非有充分的理由,否则还是应该将原始列表传递给函数。这是因为让函数使用现成的列表可避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。
8.5 传递任意数量的实参
有时候,预先不知道函数需要接受多少个实参,好在 Python 允许函数从调用语句中收集任意数量的实参。
例如以下示例就是一个制作一个比萨的函数,但前提是无法预料顾客选用多少配料:
def make_pizza(*toppings):
"""打印顾客点的所有配料"""
print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
这里的形参名 *toppings 中的星号让 Python 创建一个名为 toppings 的元组,并将收到的所有值都封装到这个元组中。也就是说,Python 将实参封装到一个元组中,即便函数只收到一个值:
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
现在,可以将函数调用 print() 替换为一个循环,遍历配料列表并对顾客点的比萨进行描述:
def make_pizza(*toppings):
"""打印顾客点的所有配料"""
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
Making a pizza with the following toppings:
- pepperoni
Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
综上,要想传递任意数量的实参,只需在函数定义形参时在前面加上一个星号(*)就可以啦。
8.5.1 结合使用位置实参和任意数量实参
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python 先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。
例如,如果前面的函数还需要一个表示比萨尺寸的形参,必须将其放在形参 *toppings 的前面:
def make_pizza(size, *toppings):
"""打印顾客点的所有配料"""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {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
注意:你经常会看到通用实参名 *args ,它也收集任意数量的位置实参
8.5.2 使用任意数量的关键字实参
有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。在这种情况下,可将函数编写成能够接受任意数量的键值对——调用语句提供了多少就接受多少。一个这样的示例是创建用户简介:你知道将收到有关用户的信息,但不确定会是什么样的信息。
例如在下面的示例中,函数 build_profile() 不仅接受名和姓,还接受任意数量的关键字实参:
def build_profile(first, last, **user_info):
"""创建一个字典,其中包含我们知道的有关用户的一切"""
user_info['first_name'] = first
user_info['last_name'] = last
return user_info
user_profile = build_profile('albert', 'einstein',
location = 'princeton',
field = 'physics')
print(user_profile)
函数 bulid_profile() 的定义要求提供名和姓,同时允许根据需要提供任意数量的名称值对。形参 **user_info 中的两个星号让 Python 创建一个名为 user_info 的空字典,并将收到的所有名称值对都放到这个字典中。在这个函数中,可以像访问其他字典那样访问 user_info 中的名称值对。
在这里,返回的字典包含用户的名和姓,,还有求学的地方和所学专业:
{'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}
注意:你经常会看到形参名 **kwargs ,它用于收集任意数量的关键字实参
8.6 将函数存储在模块中
使用函数的有优点之一是可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在称为模块的独立文件中,再将模块导入到主程序中。import 语句允许再当前运行程序文件中使用模块中的代码。
通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重点放在程序的高级逻辑上。这还能让你在众多不同的程序中重用函数。将函数存储在独立文件中后,可与其他程序员共享这些文件而不是整个程序。知道如何导入函数还能让你使用其他程序员编写的函数库。
导入模块的方法有多种,下面对每种进行简要的介绍。
8.6.1 导入整个模块
要让函数是可导入的,得先创建模块。模块是扩展名为 .py 的文件,包含要导入到程序中的代码。
下面来创建一个包含函数 make_pizza() 的模块。为此,将文件 pizza.py 中除了函数 make_pizza() 之外的其他代码删除:
def make_pizza(size, *toppings):
"""打印顾客点的所有配料"""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
接下来,在 pizza.py 所在的目录创建一个名为 making_pizzas.py 的文件。这个文件导入刚创建的模块,再调用 make_pizza() 两次:
import pizza
pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Python 读取这个文件时,代码行 import pizza 让 Python 打开 pizza.py,并将其中的所有函数都复制到这个程序中(当然,Python 只是在幕后复制了这些代码,所以在显示器上是看不出来的)。你只需知道,在 making_pizzas.py 中,可使用 pizza.py 中定义的所有函数。
当然,这些代码的输出与没有导入模块的原始程序相同:
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
这就是一种导入方法:只需编写一条 import 语句并在其中指定模块名,就可以在程序中使用模块中的所有函数:module_name.function_name()
8.6.2 导入特定的函数
导入模块中特定函数的语法为:from module_name import function_name()
且通过用逗号分隔函数名来根据需要从模块中导入任意数量的函数:from module_name import function_0, function_1, function_2
对于前面的 making_pizzas.py 所示,如果只想导入要使用的函数,代码将类似于下面这样:
form pizza import make_pizza
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
使用这种语法时,调用函数无需使用句点。这是由于在 import 语句中显式地导入了函数 make_pizza(),这样调用时只需指定其名称即可。
8.6.3 使用 as 给函数指定别名
如果要导入函数的名称可能与程序中现有的名称冲突,或者函数的名字太长,可指定简短而独一无二的别名:函数的另一个名称,类似于外号。要给函数取这种特殊外号,需要在导入它时指定。
下面给函数 make_pizza() 指定了别名 mp()。这是在 import 语句中使用 make_pizza as mp 实现的,关键字 as 将函数命名为指定的别名:
form pizza import make_pizza as mp
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
最后给出指定别名的通俗语法:from module_name import function_name() as fn
8.6.4 使用 as 给模块指定别名
我们还可以给模块指定别名。通过给函数指定简短的别名(如给模块 pizza 指定别名 p ),让你能够更轻松地调用模块中的函数:
import pizza as p
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
最后给出指定别名的通用语法:import module_name as mn
8.6.5 导入模块中的所有函数
使用星号(*)运算符可让 Python 导入模块中的所有函数:
import pizza *
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
import 语句中的星号让 Python 将模块 pizza 中的每个函数都复制到这个程序文件中。由于导入了每个函数,就可直接通过名称来调用每个函数,而无需使用句点表示法。
然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法。这是因为如果模块中有函数名与当前项目中使用的名称一样,则可能导致意想不到的结果:Python 可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是跟别导入所有的函数。所以说,最佳的做法是要么只导入需要使用的函数,要么导入整个模块并使用句点表示法。这样一来,代码会更清晰,更容易阅读和理解。
最后给出其通用语法:from module_name import *
8.7 函数编写指南
编写函数时,需要牢记几个细节。应给函数指定描述性名称,且只在其中使用小写字母和下划线。描述性名称可帮助你和别人明白代码想要做什么。给模块命名时也应遵循上述约定。
另外,关于函数中有些等号两边是最好不要空格的,在本篇文章中出于以前习惯都给打上了空格,这是不对的做法,但又懒得去改了,所以在这里说明一下:
- 给形参指定默认值时,等号两边不要有空格 :
def function_name(parameter_0, parameter_1='default value') - 对于函数调用中的关键字实参,也应遵循这种约定:
function_name(value_0, parameter_1='value'
另外,如果形参很多,可在函数定义中输入左括号后按回车键,并在下一行按两次 Tab 键,从而将形参列和只缩进一层的函数体区分开来。
如果程序或模块包含很多哥函数,可使用两个空行将相邻的函数分开,这样更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。
最后,所有 import 语句都应放在文件开头。唯一例外的情形是,在文件开头使用了注释来描述整个程序。
总结
在本章中,你学习了:
- 如何编写函数,以及如何传递实参,让函数能够访问完成其工作所需信息;
- 如何使用位置实参和关键字实参,以及如何接受任意数量的实参;
- 显示输出的函数和返回值的函数;
- 如何将函数同列表、字典、if 语句和 while 循环结合起来使用;
- 如何将函数存储在称为模块的独立文件中,让程序始终结构良好,并对你和其他人来说易于阅读。
在第 9 章,你将学习编写类。类将函数和数据整洁地封装起来,让你能够灵活而高效的使用它们。
|