第十一章 函数:返回值与进阶用法
11.1 函数的返回值
11.1.1 向函数外部传递信息:返回值
函数返回值的用法
需求:函数并非总是直接通过print()来显示结果。有时,希望函数在执行完成后,将有用的处理结果,返回给调用者,以便做后续的处理
知识点:
返回值:在函数执行完后,返回给它的调用者一个结果
如果把函数看作是一个黑箱的话,那么输入就是函数的参数,输出就是函数的返回值
通过使用函数的返回值,可以让将程序的大部分繁重工作都移到函数子程序中去完成,从而简化主程序
知识点:
在函数中,可使用return关键字来返回结果
外部调用函数之处,可以使用变量来接收函数的返回
任务一:改进带返回的求和函数
例子:带返回的两个数求和程序
1 | def sum_2_num(num1, num2): |
任务二:返回之后的代码将不被执行
注意:return 表示返回,当执行return 后,它后续代码都会不被执行
例子:
1 | def sum_2_num(num1, num2): |
11.1.2 利用元组函数返回多个值
任务一:函数多个返回值的需求分析
问题:在Python中,一个函数执行后能否返回多个结果
下面将通过一个例子来分析函数多个返回值的实际需求
例子:温度和湿度测量程序。假设要开发一个函数能够同时返回当前的温度和湿度。首先,实现函数的单个返回值(温度)功能
1 | def measure(): |
任务二:使用元组传递函数多个返回值
知识点(回顾): 函数传递多个返回值
在C++中,函数允许最多传回一个返回值,否则会出错
在Python中,通过元组可以实现传递函数的多个返回值功能。对函数的多个返回值使用元组进行打包,然后在函数调用之处进行解包赋值
1 | #观察divmod()函数 |
下面,将利用元组在返回温度的同时,也能够返回湿度信息
1 | def measure(): |
知识点:
如果一个函数返回值是元组,括号可以省略
在 Python 中,可以将一个元组通过赋值语句解包给给多个变量
注意:解包时,变量数量需要与元组元素数量保持一致
1 | # 需要单独的处理温度或者湿度 - 不方便 |
11.1.3 实例:交换两个数字
要求:有两个整数变量 a = 6, b = 100,如果不使用其他变量,如何交换两个变量的值
解法 1 —— 使用其他变量
1 | # 解法 1 - 使用临时变量 |
解法 2 —— 不使用临时变量
1 | # 解法 2 - 不使用临时变量 |
解法 3 —— Python 专有,利用元组
1 | # 通过元组的封包和解包功能 |
11.1.4 实例:人物信息构建
任务一:带返回的姓名拼接函数
需求:接受姓和名,并返回首字母大写的全名
1 | def get_formatted_name(first_name, last_name): |
程序解析:
函数get_formatted_name()接受两个参数first_name名和last_name姓
函数体中,名和姓进行拼接(中间加1空格),并将结果存储在变量full_name中
然后,将full_name字符串调用**.title()**方法转换为首字母大写格式,并将其作为返回值
主程序中,使用变量name接受函数**get_formatted_name()**的返回值,并打印输出结果
任务二:使用默认值实参改进姓名函数
需求:有些人还拥有中间名
因此,需要扩展函数get_formatted_name(),来满足中间名需求
1 | def get_formatted_name(first_name, middle_name, last_name): |
程序解析:
在调用函数时,只需提供名、中间名和姓,**get_formatted_name()**函数就能正确地运行
它根据三个实参,创建一个以空格为分隔符的拼接full_name字符串,并将结果转换为首字母大写格式
需求:
并非所有的人都有中间名。如果调用函数时,只提供了名和姓两个实参,它将不能正确地运行
为让中间名变成可选的,可给实参middle_name指定一个默认值——空字符串,并在用户没有提供中间名时不使用这个实参
函数体中,需要对2个参数(名+姓)和3个参数(名+中间名+姓)进行分别处理,可以借助if语句实现流程控制
注意:带默认值的参数,必须要在非默认值参数之后
1 | def get_formatted_name(first_name, last_name, middle_name=''): |
程序解析:
由于每个人都拥有名和姓,因此在函数定义中首先给出这两个形参。中间名是可选的,因此在函数定义中最后列出该形参,并将其默认值设置为空字符串
在函数体中,通过if判断检查参数middle_name是否为空。然后使用两种full_name的拼接方式,最后再将其修改为首字母大写格式,并返回给外部
位置参数调用函数时,必须要确保每个位置实参能正确地关联到相应的形参。调用函数时,如果只指定名和姓,调用起来将非常简单。如果需要指定中间名,就必须确保中间名传递给middle_name(最后个参数)
任务三:改进函数返回值为字典
知识点:函数可返回任何类型的值,包括列表和字典等较复杂的数据结构
例子:人物构建函数,接受名和姓,返回一个表示人属性的字典:
1 | def build_person(first_name, last_name): |
程序解析:
函数**build_person()**接受名和姓,并将这些值封装到字典中
存储first_name的值时,使用的键为**’first’;而存储last_name的值时,使用的键为‘last’**。最后,返回表示人属性的字典
在主程序中,打印返回值(字典),此时原来的两项文本信息存储在一个字典中:**{‘first’: ‘三’, ‘last’: ‘张’}**
任务四:改进函数接受年龄参数
扩展函数功能,让它能接受更多的参数,如中间名、年龄、职业或你要存储的其他任何信息。例如,下面程序可以储存姓名和年龄:
1 | def build_person(first_name, last_name, age=''): |
程序解析:
在函数定义中,新增一个可选形参age,并将其默认值设置为空字符串
如果函数调用中包含这个形参的值,就将该值将存储到字典中;否则,直接返回无年龄信息的字典
任务五:循环接受外部输入储存用户信息
需求:
使用while哨兵式循环,不断接受用户循环输入,直到输入q为止
调用之前姓名构造函数,将用户输入作为函数的参数,并返回完整的姓名
1 | def get_formatted_name(first_name, last_name): |
在程序中,添加了一条消息来告诉用户如何退出,然后在每次提示用户输入时,都检查他输入的是否是退出值,如果是,就退出循环。现在,这个程序将不断地问候,直到用户输入的姓或名为**’q’**为止
11.2(重要)进阶函数
11.2.1 分析函数参数和返回值的作用
任务一:四种组合形式
知识点:函数根据有没有参数以及有没有返回值,可以相互组合,一共有4 种组合形式。
无参数,无返回值
无参数,有返回值
有参数,无返回值
有参数,有返回值
知识点:
定义函数时,是否接收参数,或者是否返回结果,是根据实际的功能需求来决定的
如果函数内部处理的数据不确定,就可以将外界的数据以参数传递到函数内部。
如果希望一个函数 执行完成后,向外界汇报执行结果,就可以增加函数的返回值
任务二:无参数,无返回值
知识点:
此类函数,不接收参数,也没有返回值
应用场景:只是单纯地做一件事情,例如显示菜单、打印提示语等
在函数内部针对全局变量进行操作,例如:新建名片,将最终结果记录在全局变量中
注意:
如果全局变量的数据类型是一个可变类型,在函数内部可以使用方法修改全局变量的内容,变量的引用不会改变
在函数内部,使用赋值语句才会修改变量的引用
任务三:无参数,有返回值
知识点:
此类函数,不接收参数,但是有返回值
例如,采集数据程序(温度计)。返回结果就是当前的温度,而不需要传递任何的参数
任务四:有参数,无返回值
知识点:
此类函数,接收参数,没有返回值
函数内部的代码保持不变,针对不同的参数处理不同的数据
例如,名片管理系统针对找到的名片做修改、删除操作
任务五:有参数,有返回值
知识点:
此类函数,接收参数,同时有返回值
应用场景:函数内部的代码保持不变,针对不同的参数 处理 不同的数据,并且返回期望的处理结果
例如,名片管理系统使用字典默认值和提示信息提示用户输入内容:如果输入,返回输入内容;如果没有输入,返回字典默认值
11.2.2 函数的嵌套调用
任务一:嵌套的函数流程分析
知识点:所谓的函数嵌套调用就是在一个函数里面又调用了另外的函数
例子:函数的嵌套使用
1 | def test1(): |
程序解析:如果函数 test2 中,调用了另外一个函数 test1
那么执行到调用 test1 函数时,会先把函数 test1 中的任务都执行完
才会回到 test2 中调用函数 test1 的位置,继续执行后续的代码
任务二:百度网站解读函数嵌套调用
例子:通过百度网站列举说明现实应用中的嵌套调用,仅为了直观解释什么是函数的嵌套
实例:打印分隔符
知识点:
程序往往会因需求而不停地调整和更新
通过函数可以实现对功能代码的统一管理
当需求发生变化时,只需对相应函数的内部进行更新,而其他外部代码不变
需求 1:定义一个 print_line 函数能够打印 ***** 组成的一条分隔线
1 | def print_line(char): |
需求 2:定义一个函数能够打印由任意字符组成的分隔线
1 | def print_line(char): |
需求 3:定义一个函数能够打印任意重复次数的分隔线
1 | def print_line(char, times): |
需求 4:定义一个函数能够打印5行的分隔线,分隔线要求符合需求3
注意:在面对实际问题变化时,应该冷静思考,不要轻易修改之前已经完成的,能够正常执行的函数
1 | def print_line(char, times): |
11.2.3 模块中的函数
任务一:认识模块的功能
知识点:
模块是Python程序架构的一个核心概念,将多个拥有功能类似的函数打包封装到模块中。它是比函数更高一级的程序集
模块就好比是拥有某种功能的工具包,如果要想使用工具包中的工具(函数),就需要提前通过import导入该模块。
每一个以扩展名 py 结尾的 Python 源代码文件都是一个模块
模块名就是文件名。在其他程序中,通过import 模块名来导入该模块
实例:体验模块
步骤一:新建 py_seplines.py
步骤二:复制打印5行的分隔线程序,并添加一个额外的字符串变量name
注意:复制代码时小心代码块的缩进,建议先放cell中用小锤子插件格式化后,再复制到**.py**文件中
1 | def print_line(char, times): |
步骤三:在Jupyter中,新建一个cell,并添加如下代码
1 | # 模块名为文件名 |
或者使用as关键字对import模块进行重命名:
1 | import py_seplines as py_sep |
总结:
可以在一个Python 文件中定义变量或者函数
然后在另外一个文件中使用 import 导入该模块
导入之后,就可以使用 模块名.变量 / 模块名.函数 的方式,调用该模块中定义的变量或函数
模块可以让曾编写过的代码集方便的被复用
函数是多行代码块的复用,而模块是多个函数和变量的复用
任务二:模块名也是一个标识符
知识点:
模块名、函数名、变量都属于标识符
标示符的命名可由字母、下划线 和数字组成
不能以数字开头
不能与关键字或保留字重名
注意:如果在给Python文件起名时,以数字开头导入模块会出错
任务三:模块的Pyc文件
知识点:
为了提高模块的运行效率,Python对模块进行预编译
预编译后的模块名文件为**.pyc。放在与.py**模块源代码同级目录的__pycache__文件夹中
这里的c 是 compiled 编译过的程序
知识点:查看**.pyc**文件
浏览与**.py**模块源代码同级目录的__pycache__文件夹
该目录下会有一个 py_seplines.cpython-37.pyc 文件,cpython-37 表示 Python 解释器的版本是3.7
这个 pyc 文件是由Python解释器将模块的源码转换为字节码。Python 通过字节码来优化代码运行效率
知识点:字节码
Python 在解释源程序时是分成两个步骤的:
首先处理源代码,编译生成一个二进制字节码
再对字节码进行处理,才会生成 CPU 能够识别的机器码
有了模块的字节码文件之后,下一次运行程序时,如果在上次保存字节码之后没有修改过源代码,Python将会直接加载 .pyc文件并跳过编译这个步骤。
当 Python 重编译时,它会自动检查源文件和字节码文件的时间戳。如果发现源代码被修改,那么下次程序运行时,字节码将被重新创建
模块是Python程序架构的一个核心概念
11.2.4 函数中的变量引用
任务一:变量与数据
知识点:函数通过引用传递数据
变量和数据都是保存在内存中的
在 Python 中函数的参数传递以及返回值都是靠引用传递的
知识点:变量与数据
变量和数据 是分开存储的
数据保存在内存中的一个位置
变量中保存着数据在内存中的地址
变量中记录数据的地址,就叫做引用
使用 id() 函数可以查看变量中保存数据所在的内存地址
知识点:如果变量已经被定义,当给一个变量赋值的时候,本质上是 修改了数据的引用
变量不再对之前的数据引用
变量改为对新赋值的数据引用
任务二:数据的赋值
在 Python 中,变量类似于 贴在数据上的标签
定义一个整数变量 a,并且赋值为 1。a=1
将变量 a 赋值为 2。a = 2
定义一个整数变量 b,并且将变量 a 的值赋值给 b。b = a
变量 b 是第 2 个贴在数字 2 上的标签
任务三:函数的参数和返回值的传递
知识点:在 Python 中,函数的实参/返回值都是是靠引用来传递来的
函数的参数是通过引用来传递
函数的返回值也是通过引用来传递
1 | def test(num): |
11.2.5 不可变类型与哈希函数 (重要)
任务一:基本概念解析
知识点:不可变类型,内存中的数据不允许被修改
数字类型 int, bool, float, complex, long(2.x)
字符串 str
元组 tuple
知识点:可变类型,内存中的数据可以被修改
列表 list
字典 dict
1 | a = 1 |
任务二:赋值与可变类型修改
知识点:
初学者很容易将赋值误认为是对变量对应内存内容修改
通过赋值修改变量时,原内存内容不变,而内存地址变化。重新把标签贴在另个空间上
可变类型数据,通过方法修改数据时,原内存内容变化,而内存地址不变化
任务三:列表和字典的修改和赋值
知识点:
可变类型的数据修改,是通过方法来实现的
如果给一个可变类型的变量,赋值了一个新的数据,引用会修改
变量不再对之前的数据引用。
变量改为对新赋值的数据引用
1 | demo_list = [1, 2, 3] |
任务四:字典的key是不可变类型
知识点:字典的 key 只能使用不可变类型的数据
允许:不可变类型可以作为key
1 | d = {} |
不允许:可变类型不可以作为key。TypeError:unhashable type错误
1 | d[[1,2,3] = "列表" |
任务五:认识哈希函数
知识点:哈希 (hash)
Python 中内置有一个名字叫做 hash(o) 的函数
参数:接收一个不可变类型的数据作为参数
返回:一个整数
哈希 是一种算法,其作用就是提取数据的特征码(指纹)
相同的内容得到相同的结果
不同的内容得到不同的结果
在Python中,设置字典的键值对时,会首先对key进行 hash 来决定如何在内存中保存字典的数据,以便后续对字典的操作:增、删、改、查
键值对的 key 必须是不可变类型数据
键值对的 value 可以是任意类型的数据
允许:不可变类型可以用hash函数生成它的特征码
1 | h = hash(1) |
1 | # 传入相同的数据,hash值永远一样 |
1 | # 元组是不可变类型 |
不允许:可变类型不可以用hash函数生成它的特征码
1 | # 列表是可变类型 |
1 | # 字典是可变类型 |
11.2.6 递归函数
任务一:递归函数的定义
知识点:
在函数体内部,可以调用其他函数也可以调用自己。将函数调用自身的编程技巧称为递归
递归函数的特点:
函数内部的代码是相同的,只是针对参数不同,处理的结果不同
当参数满足递归终止条件时,函数不再执行。这个非常重要,通常被称为递归的出口,否则会出现死循环
例子:递归函数实现倒计时程序
1 | def sum_numbers(num): |
实例:递归实现计算数字累加
需求分析:
定义一个函数 sum_numbers
能够接收一个 num 的整数参数
计算 1 + 2 + … num的结果
1 | # 定义一个函数 sum_numbers |
提示:
递归是一个编程技巧,初次接触递归会感觉有些吃力
建议先想好递归通项和递归出口条件,然后再编写递归函数
在处理不确定的循环条件时,格外的有用,例如:遍历整个文件目录的结构
递归可以等价看作是while循环+函数的流程结构
11.2.7 匿名函数lambda
11.2.8 eval()函数
任务一:eval()函数基本用法
知识点:eval()函数十分强大,它将字符串当成有效的表达式来求值 并返回计算结果
1 | # 基本的数学计算 |
任务二:使用eval()函数实现计算器功能
需求分析:
提示用户输入一个 加减乘除混合运算
返回计算结果
1 | input_str = input("请输入一个算术题:") |
任务三: eval()函数的危险性
危险:
在开发时千万不要使用**eval()**直接转换 input 的结果
用户就可以控制计算机做任何事情
主要的原因是因为用户可以通过eval()函数间接的执行各种终端指令
eval()函数执行终端指令来对计算机进行各种操作
1 | __import__('os').system('ls') |
等价代码
1 | import os |
执行成功,返回 0
执行失败,返回错误信息
11.3 局部变量和全局变量
11.3.1 局部变量
任务一:局部变量和全局变量的基本概念
知识点:
局部变量是在函数内部定义的变量,只能在函数内部使用
全局变量是在函数外部定义的变量(没有定义在函数内),所有函数内部都可以使用这个变量
区别:
局部变量和全局变量的定义位置不一样
局部变量和全局变量的作用域范围不一样
全局变量一般用于在所有函数中共享全局信息。由于作用域范围太大,开发中慎用全局变量。如果全局变量出现错误,那么程序的错误调试将会变得非常复杂
任务二:认识局部变量
知识点:
局部变量是在函数内部定义的变量,只能在函数内部使用
作用域范围:函数执行结束后,函数内部的局部变量,会被系统回收
1 | def demo1(): |
局部变量的生命周期(重要)
知识点:
所谓生命周期就是变量从被创建到被系统回收的过程。
出生:局部变量在函数执行时才会被创建。定义变量时被创建。
消亡:函数执行结束后局部变量被系统回收。函数return或执行所有代码时,变量消亡。
在生命周期内,局部变量可以用来存储函数内部临时使用到的数据
任务三:不同函数中的局部变量相互不影响
知识点:
不同的函数可以定义相同的名字的局部变量,但是彼此之间不会产生影响。
在函数内部使用,局部变量用于临时保存函数内部需要使用的数据
1 | def demo1(): |
11.3.2 全局变量
任务一:认识全局变量
知识点:
全局变量是在函数外部定义的变量
作用域是整个程序,即所有函数内部都可以使用该变量
1 | # 定义一个全局变量 |
任务二:PyCharm单步调试解析全局变量
知识点:
使用PyCharm调试功能,查看定义在函数和模块中的变量
注意:一个python源文件或主程序可以看作是一个模块
在模块中,可以定义多个函数和变量
在模块中定义的变量,相对于函数来说是全局变量。换言之,模块中的变量级别和函数是相同的
任务三:函数内部不能直接修改 全局变量的引用
知识点:
全局变量:在函数外部定义的变量,与函数同一个级别。因此,所有函数内部都可以使用这个变量
虽然要慎用全局变量,但是它可以在各函数中,共享数据信息。所以,对某些需要共享数据的程序开发,使用全局变量会带来很大的便利,可以减少参数个数
全局变量的只读属性,为了防止修改全局变量而影响其他函数的运行:
在函数中,可以通过全局变量的引用获取对应的数据
但是Python不允许直接修改全局变量的引用 ,即不能通过赋值语句来修改全局变量的值
知识点:在函数中,如果使用赋值语句尝试着来修改全局变量值,会发生什么事情
本质上,Python会定义一个新的和全局变量同名的局部变量
因此,在对该变量赋值,本质上是对同名的局部变量进行修改,并不影响原全局变量的值
1 | num = 10 |
任务四:函数变量查找过程的深度解析
知识点:函数执行时,需要处理变量时 会:
首先查找函数内部是否存在指定名称的局部变量,如果有,直接使用。
如果没有,查找函数外部是否存在指定名称 的全局变量,如果有,直接使用。
如果还没有,程序报错
任务五:global 关键字开启全局变量修改权限
知识点:
在函数中需要修改全局变量,需要使用 global 关键字来进行声明
通过 global 关键字开启全局变量修改权限
如果修改没有 global 前缀的全局变量,函数会新建一个同名的局部变量
1 | # 全局变量 |
任务六:全局变量定义的位置
知识点:
全局变量可以定义在函数定义之后
全局变量必须要在函数调用之前被定义,否则会引发异常
在Python程序运行中,程序遇到函数定义时会被跳过,而真正的函数代码执行会发生在函数调用
因此,为了保障函数能够正确使用全局变量,建议将全局变量定义在函数定义之前
1 | # 注意:在开发时,应该把模块中的所有全局变量 |
常用的代码结构规范如下图。关于shebang或**#!标识,属于Linux的shell**编程,有兴趣可以课外查阅相关资料
任务七:全局变量命名规范
知识点:为了避免局部变量和全局变量出现混淆,在定义全局变量时,变量名前应该增加 g_ 或者 gl_ 的前缀
1 | gl_num = 10 |