(请先看置顶博文)https://blog.csdn.net/GenuineMonster/article/details/104495419
目录
(请先看置顶博文)https://blog.csdn.net/GenuineMonster/article/details/104495419
目录
目录
(请先看置顶博文)https://blog.csdn.net/GenuineMonster/article/details/104495419
一、定义函数
二、传递实参
1、位置实参
2、关键字实参
3、默认值
4、避免实参错误
三、函数的返回值
1、返回简单值
2、返回字典
四、传递列表
1、在函数中对列表进行修改
2、禁止函数修改列表
五、传递任意数量的实参
1、结合位置实参,使用任意数量实参。(位置实参和任意数量实参共用)
2、结合关键之字实参,使用任意数量实参。(关键字实参和任意数量实参共用)
六、函数与模块的关系
1、导入模块中的特定函数
2、使用as为函数、模块重命名
3、使用*号导入模块中的所有函数
4、函数编写指南
函数的特点:
1、函数可将程序分成多个部分,每部分都负责完成一项具体的任务,也可存放到单独的文件中。
2、代码分块化、函数化后,更加易于维护、调用、重用和排除故障。
所以在这一节里我们将学习函数的定义、参数类型、如何传参等内容,上一节课也说了,这部分内容相比之前的难度增大了许多,所以要反复理解、及时复习。
一、定义函数
定义函数的关键字是def,在def后要写这个函数的名字,最好起一个和这个函数实现功能相关的函数名,方便阅读。最后增加圆括号(有的函数需要参数,下面会讲)和冒号,函数声明的第一步就完成了。下面给出一个简单函数的例子,包含对函数代码的备注:
# 定义一个具有问候功能的函数
# 使用关键字def告诉编译器接下来的代码块是一个函数
"""显示简单的问候语"""
def greet_user():print("Hello!") #打印hellogreet_user() # 调用greet_user函数
冒号后面缩进的程序构成了函数体,实现函数的主要功能,此处只有一句打印问候语。调用函数也十分简单,只需要在合适的位置依次指定函数名以及括号括起的必要信息(就是参数)。定义函数语句的上一行有被三个双引号括住的汉字,这个被称为“文档字符串的注释”(在我看来就是段备注),Python编译器会使用三引号括起的文档字符串生成有关程序中函数的文档。上面这个函数是最简单的函数结构,接下来的函数都是以这个结构为原型进行拓展的。
我们现在做一些改动,将这段程序的函数变得复杂一丢丢。在函数定义def greet_user()的括号内增加username(这就是所谓的函数参数),这样我们就可以将代码改为:
# 定义一个具有问候功能的函数
# 使用关键字def告诉编译器接下来的代码块是一个函数
"""显示简单的问候语"""
def greet_user(username):print("Hello! " + username.title() + ". ") #打印hello+用户姓名greet_user('jesse') # 调用greet_user函数
因为定义函数时我们加入了参数,所以我们在调用时也得增加内容,函数将会把我们在调用时增加的内容传递给参数“username”,然后使用新的print()语句打印出对某个人的问候语。说到这,就要说一下参数的类型:形参和实参。形参就是我们在声明函数时在括号内写的的“username”,可以将其理解为形式上的参数;而实参就是我们上一段代码中调用函数时在括号中输入的“jesse”。(形参:函数完成其工作所需的一项信息;实参:实参是调用函数时传递给函数的信息)(还有:这个实参形参只是一个称谓,如果有人混淆了,也不要大惊小怪,我们理解那个意思就好!)
二、传递实参
鉴于函数定义中可能包含多个形参,所以在函数调用时,也必须包含多个实参。(或者说:调用函数时,Python编译器必须将函数调用中的每个实参都关联到函数定义中的一个形参)传递实参的方式很多,可使用位置实参(要求实参的顺序与形参的顺序相同)。也可以使用关键字实参,其中每个实参都由变量名和值组成。还可以用列表和字典。
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('hasmster','harry')
上面这段代码定义的函数包含两个形参,分别是动物类型和宠物名称。那么,我们在调用这个函数时,使用位置实参的方法,按照形参的顺序,依次输入“hasmster”和“harry”,就可以将这两个实参分别与形参一一对应起来,最后成功打印出来“我有什么(动物类型)以及我的(宠物类型)的(宠物名称)是什么”。(特别提示:位置实参的顺序很重要)同样,我们可以更换“hasmster”和“harry”对应的内容,重新调用已定义的函数。由此,定义函数的意义也就出来了:高效,只需要写一次代码,改变实参的内容,就可以在接下来的代码中多次使用。
2、关键字实参
关键字实参是传递给函数的名称—值对。直接在实参中将名称和值关联起来,因此向函数传递实参时不会混淆。关键字实参的优点是:无需考虑函数调用中的实参顺序,并且能清楚地指出函数调用中各个值的用途。那么,接下来的代码,我将会使用关键字实参演示:
我们观察可以看到,describe_pet()函数的定义部分没有变,变得只是调用函数时的传参方式(关键字实参)。一旦使用关键字的实参,那么实参的顺序就无关紧要了,因为等号已经指定了实参与形参的对应关系。所以以下两种调用函数代码的输出结果是一致的。
describe_pet(animal_type = 'hasmster',pet_name = 'harry')
describe_pet(pet_name = 'harry',animal_type = 'hasmster')
3、默认值
我们在定义函数时,可以给每个形参指定默认值。在调用函数中给形参提供实参时,Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的参数。使用默认值的优点是:可简化函数调用,还可清楚的指出函数的典型用法。例如,发现调用函数describe_pet()时,描述的大都是小狗,就可将形参animal_type的默认值设为‘dog’,如此一来,再调用函数描述小狗时,就可以不用提供这种信息了。例如:
def describe_pet(pet_name,animal_type='dog'):"""显示宠物信息"""print("I have a " + animal_type + ". ")print("My " + animal_type + "'s name is " + pet_name.title() + ". ")
print("位置实参:")
describe_pet('harry')
print("关键字实参:")
describe_pet(pet_name = 'harry')
print("默认值是否可修改验证:")
describe_pet(pet_name = 'harry',animal_type = 'cat')
上面这段代码为宠物类型指定了形参的默认值,那么我们只需要对宠物名称进行传参即可。我分别使用了两种传参方式,并且验证了一下“宠物类型虽然指定默认值,但仍旧可以传参的事实”。细心的同学观察到:上述代码在定义函数时,修改了形参的排列顺序(与之前的代码相比较)。假设,我们没有对形参的顺序进行修改,并且使用的是“位置实参”。如果此时调用函数,仅仅指定动物名称,那么就会出现下面的错误:
这是因为宠物类型已经被指定,我们在调用函数时,Python编译器将会把第一个实参与第一个形参关联起来。所以,定义函数和调用函数要一致。另外,在使用默认值时,在形参列表中必须先列出没有默认值的形参,再列出有默认值的形参。如此一来,Python编译器能够正确解读位置实参。那么按照红字原则定义出来的函数将会有多种调用函数的方法,无论哪种方法,只要函数调用能生成期待的输出就行。
4、避免实参错误
开始使用函数之后,我们可以犯的错误又多了很多种。其中一种是实参错误:我们在调用函数时所提供的实参多于或少于函数完成其工作所需的信息时,将出现实参不匹配的错误。我们举个例子,解释一下,并且详细讲解一下traceback的报错信息。
def desribe_pet(animal_type,pet_name):"""显示宠物的信息"""print("\nI have a " + animal_type + ". ")print("My " + animal_type + "'s name is " + pet_name.tiltle() + ". ")
describe_pet()
在1处:traceback指出了问题出现在什么地方;在2处,指出了导致问题的函数调用;在3处,traceback指出该函数调用少了两个参数,并指出了相应形参的名称。如果提供的实参多了,traceback也会出现类似的错误提示。
三、函数的返回值
1、返回简单值
完成上面的函数定义、函数传参之后,我们进行函数的返回。通过上面的例代码,我们发现确实定义了函数、确实调用函数(传递参数)、也确实见到了对应结果的输出,但是怎么没看见返回值?这是因为我们上面的函数例子都是无返回值的函数。函数收到实参后进行的处理工作并非都是直接输出,它可能也要处理一些数据什么的,然后返回一个或一组值,所以函数返回的值就被称为返回值。编程语言中(Python、C、C++)用return语句将值返回到调用函数的代码行。下面通过一个例子进行详细的说明:
def get_formatted_name(first_name,last_name):"""返回整洁的姓名"""full_name = first_name + ' ' + last_namereturn full_name.title()
musician = get_formatted_name('jimi','hendrix')
print(musician)
解释:函数名为get_formatted_name,在定义时有两个形参分别是first_name,last_name。当函数被调用且受到两个实参时,代码将会让“名字”和“姓氏”重新组合为一个完整的姓名,然后将其存储在full_name中,使用return语句返回完整姓名(开头大写)。这个full_name的值会返回到调用函数get_formatted()的那一行代码里,并将返回值赋给musician,最后使用print语句将完整的名字输出。值得注意的是在调用有返回值的函数时,需要为其提供一个变量,存储返回值。可能有同学就会询问了,我们明明能很简单的将这个名字完整的输出,为什么要写一个如此繁琐的程序?别急,往下看(手动滑稽)
2、返回字典
函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。如下例代码所示:
def build_person(first_name,last_name):# 返回一个字典,其中包含有关一个人的信息person = {'first':first_name,'last':last_name}return person
musician = build_person('jimi','hendrix')
print(musician)
函数build_person()接受名和姓,并将这些值封装到字典里。我们还可以扩展这个函数,增加其可接受的值,如下例代码所示:(在函数定义时,我们新增了一个可选形参age,空字符串,将其作为备选项)
def build_person(first_name,last_name,age=''):# 返回一个字典,其中包含有关一个人的信息person = {'first':first_name,'last':last_name}if age: #如果调用函数时由年龄,那么就把年龄存储起来,一起输出;若没有,直接输出person['age'] = agereturn person
musician = build_person('jimi','hendrix',age = 27)
print(musician)
下面是没有年龄时的输出:
在本小节的最后,我们将函数与while循环结合起来,写一个问候用户的程序,从而更正规的问候用户。
def get_formatted_name(first_name,last_name):# 返回整洁的姓名full_name = first_name + ' '+last_namereturn full_name.title()# 接下来是一个无限不循环代码片段
while True:print("\nPlease tell me your name:")f_name = input("First name:")l_name = input("last name:")formatted_name = get_formatted_name(f_name,l_name)print("\nHello, " + formatted_name + "!")
四、传递列表
向函数传递列表很有用,列表包含的可能是名字、数字或更复杂的对象(如字典)。将列表传递给函数后,函数就能直接访问了。下面给出一个例子:将名字列表传递给一个名为greet_users()的函数,这个函数问候列表中的每一个人。
def greet_users(names): # 这里的names是形参"""向列表中的每位用户都发出简单的问候"""for name in names: # 函数处理时按列表处理即可(直接传参数就行)msg = "Hello, " + name.title() + "!"print(msg)usernames = ['hannah','try','margot'] # 名字列表
greet_users(usernames) # 这里的usernames是实参
1、在函数中对列表进行修改
将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久的,如此一来就能够高效的处理大量的数据了。有如下例代码,初级版本是这样的:
# 需要打印的列表
unprinted_designs = ['iphone case','robot pendant','dodecahedron']
# 打印完的列表
completed_models = []# 模拟打印每个设计,直到没有未打印的设计为止
# 打印每个设计后,都将其移到列表compled_models中
while unprinted_designs:current_design = unprinted_designs.pop()# 模拟根据设计制作3D打印模型的过程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)
但是上述代码,在处理大量数据时,效率不怎么高。所以,我们将在接下来对其进行改进,使其效率更高。我们设计两个函数,让其分别进行一项具体的工作。第一个函数负责打印设计的工作,第二个函数将概述打印了哪些设计。
def print_models(unprinted_designs,completed_models):"""模拟打印每个设计,直到没有未打印的设计为止打印每个设计后,都将其移到列表completed_models中"""while unprinted_designs:current_design = unprinted_designs.pop()# 模拟根据设计制作3D打印模型的过程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)
这两段代码的输出结果一样,但是后者组织的更有序,大部分工作的代码转移到了这两个函数中,让主程序更加简约、容易理解。除此以外,这段代码更容易被维护和扩展。通过上述代码,可以学习一种编码原则:每个函数都应负责一项具体的工作。除此之外,一个函数还可以调用另一个函数。
2、禁止函数修改列表
函数能修改列表,这已经是事实了。有些时候,我们得禁止函数修改列表。还是打印的例子,比如我们在打印完成后,依然需要保留原来的打印列表。此时,我们可以这样解决:可向函数传递列表的副本而不是原件,这样函数所做的任何修改都只会影响副本而不影响原件。
调用函数使用副本的代码如下所示:
print_models(unprinted_designs[:],completed_models)
不知道你们还是否知道上述代码中“[ :]”这个代表什么,这个是之前学习的切片,这里表示创建原列表的副本。虽然可以向函数传递列表的副本可保留原始列表的内容,但除非是有充分的理由需要传递副本,否则还是应该将原始列表传递给函数。(因为让函数使用现成的列表可避免花时间和内存创建副本,从而提高代码效率。即时间复杂度和空间复杂度)
五、传递任意数量的实参
有时候在写函数时,不知道将会有多少个实参传递进来,此时也不用去构思。因为Python允许函数从调用语句中收集任意数量的实参。如下例代码:
def make_pizza(*toppings): # 带*号的意思是创建一个名为toppings的空元组,将收到的所有值都封装到这个元组内"""打印顾客点的所有配料"""print(toppings)make_pizza('pepperoni')
make_pizza('mushrooms','green peppers','extra cheese')
在成功尝试传递任意数量的实参之后,接下来可以把函数中的print重新改写,要对配料进行遍历,并一一输出。
def make_pizza(*toppings): # 带*号的意思是创建一个名为toppings的空元组,将收到的所有值都封装到这个元组内"""打印顾客点的所有配料"""print("\nMaking a pizza with the following toppings:")for topping in toppings:print("- " + topping)make_pizza('pepperoni')
make_pizza('mushrooms','green peppers','extra cheese')
由上述实验可以看出,无论函数收到的实参个数有多少个,这种语法都管用。
1、结合位置实参,使用任意数量实参。(位置实参和任意数量实参共用)
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。举例:
def make_pizza(size,*toppings): # 带*号的意思是创建一个名为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')
2、结合关键之字实参,使用任意数量实参。(关键字实参和任意数量实参共用)
这种方法适合于需要接受任意数量的实参、未知类型的实参。此时,可以将函数编写成能够接受任意数量的键—值对(调用语句提供多少就接受多少)
def build_profile(first,last,**user_info): # "**"代表字典,“*”代表列表# 函数build_profile()的定义要求提供姓和名,同时允许用户根据需要提供任意数量的“名称-值”对。# “**user_info”代表创建一个名为user_info的空字典,并将所有收到的“名称—值”对,存入这个字典"""创建一个字典,其中包含我们知道的有关用户的一切"""profile = {}profile['first_name'] = firstprofile['last_name'] = lastfor key, value in user_info.items():profile[key] = valuereturn profile
user_profile = build_profile('albert','einstein',location='princeton',field='physics')print(user_profile)
综上,通过上述实验,我们知道:在编写函数时,可以以各种方式混合使用位置实参、关键字实参和任意数量的实参。
六、函数与模块的关系
大家都知道“调包侠”的梗,其中的包就是我们接下来要说的模块。而函数的优点之一时可以让他们的代码块与主程序分离。因此,我们可以把函数存储在被称为“模块”的独立文件中,再将模块使用“import”导入到主程序中。如此一来,不仅可以隐藏程序代码的细节,而且还可以将重点放在程序的高层逻辑上,最后还可以提高程序的重用性。下面将会给出例代码。
第一个文件是模块文件,名为code,我自己起的。
def make_pizza(size,*toppings):"""概述要制作的披萨"""print("\nMaking a " + str(size) + "-inch pizza with the following toppings:")for topping in toppings:print("- " + topping)
第二个文件是主程序文件,名为making_pizzas,书上起的。
import code
# 要想使用被调入模块的函数,得写明是哪个模块的哪个函数
# 可简单的理解为“模块名+‘.’+函数名”。
code.make_pizza(16,'pepperoni')
code.make_pizza(12,'mushrooms','green peppers','extra cheese')
运行时需注意,要运行第二个文件,不能直接点击“run”,要在第二个文件代码间,点击右键,运行代码。(Pycharm)整个程序运行的机理是:Python编译器读取到这个文件时,代码运行import code,让Python编译器打开文件code.py,并将其中的所有函数都复制到这个程序中。你看不到这个复制过程,因为它在幕后进行。上述过程完成之后,code.py中的所有函数都能在making_pizza.py中使用。(调用模块的程序和未调用模块的原始程序的输出结果是完全相同的)
1、导入模块中的特定函数
如果模块过大,而我们只需要导入其中的部分函数即可:“from 模块名 import 函数名”,如果还以做披萨的代码为例,那么如下:
from code import make_pizzamake_pizza(16,'pepperoni')
make_pizza(12,'mushrooms','green peppers','extra cheese')
若使用这样直接导入模块函数的方法,在使用方法时,就无需指明是哪个模块了,直接用就OK了。
2、使用as为函数、模块重命名
使用模块多了之后,模块名或者函数名容易冲突(最好在命名时要遵守一定的规则,我们在第4部分介绍),有的函数名过长,在使用时不太方便,所以需要为这些“特殊情况”指定新的、简短的、独一无二的名字(nickname)。还以做披萨为例:
from code import make_pizza as mp
# 使用as,将起的nickname加在后面mp(16,'pepperoni')
mp(12,'mushrooms','green peppers','extra cheese')
第一句代码执行之后,make_pizza()重命名为mp(),在以后需要使用make_pizza()函数时,都可简写程mp()。以上是函数的重命名,接下来是模块的重命名,仍然以做披萨为例:
import code as p
# 用as将模块名“code”改为“p”
# 下面调用make_pizza()的函数调用方法对应改变,如下所示!!!p.make_pizza(16,'pepperoni')
p.make_pizza(12,'mushrooms','green peppers','extra cheese')
这样做的好处是:不仅能使代码简化,而且可以让我们更专注于描述函数性的函数名,这些函数名指出了函数的功能。
3、使用*号导入模块中的所有函数
from code import *
# 用as将模块名“code”改为“p”
# 下面调用make_pizza()的函数调用方法对应改变make_pizza(16,'pepperoni')
make_pizza(12,'mushrooms','green peppers','extra cheese')
这个和直接导入整个模块有区别吗?import语句中的*号让模块中的全部函数都复制到了主程序文件中,因此可以直接使用函数名称来调用每个函数,无需再用“.”。但是,在使用并非自己写的大型模块时,最好不要采用这种导入方法:如果模块中有函数的名称与自己的项目中使用的名称相同,可能会导致想不到的结构。最佳的做法是:要么导入你需要使用的函数,要么导入整个模块并使用句点表示法。这样做能让代码更清晰,容易阅读、理解。
4、函数编写指南
A、应该给函数指定描述性名称,且只在其中使用小写字母和下划线。这样看到函数名称就知道这个函数的功能。模块命名时,也是同样的道理。
B、每个函数都应包含简要的阐述其功能的注释,该注释应紧跟在函数定义后面,并采用文档字符串格式。只要知道函数的名称、需要的实参以及返回值类型,就能在自己的程序中使用它。
C、给实参指定默认值时,等号两边不要有空格。同样,对于函数调用中的关键字实参,也是同样的道理。
D、建议代码行的长度不要超过79字符,这样只要编辑其窗口适中,就能看到整行代码。如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左括号后按回车,并在下一行按2次Tab键,从而将形参列表和只缩进一层的函数本体区分开来。
E、如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样将更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。
F、所有的import语句都应该放在文件开头,除了在文件开头有注释。