对于编程语言而言,函数是不可或缺的一块内容。因为在编写代码的过程中,不可避免会出现很多重复、冗余的代码,从而导致代码变得冗长且不易维护。这时候,函数的诞生,就极好地解决了这个问题。
我们将代码中重复的、功能相对单一的代码抽离出来形成一个个代码块,然后使用函数、嵌套将代码块进行包装。优化了代码的简洁程度,同时保证模块功能的一致性,也增加了可扩展性。将单一功能提取出来形成函数后,也大大加强了我们对于功能测试的便利。
所以函数一般具备了以下特性:
- 代码重用性
- 模块功能一致性
- 可扩展性
- 测试便利性
那么,Python中的函数,是怎么样的呢?
函数的结构
def 函数名(参数):
函数体内的代码块
创建一个函数的时候,我们使用def开始定义一个函数,紧接着函数名,并在后面的括号内写入函数的参数。在参数后的冒号后换行写入该函数体中的代码。
这边就涉及到了如下几个概念:
- 函数名
- 参数
- 作用域
- 返回值
函数名
函数名是用来识别、调用这个函数的。其命名规则和变量的命名规则一致。都需要遵守以下的规则:
- 函数名必须以下划线或字母开头,可以包含任意字母、数字或下划线的组合。不能使用任何标点符号
- 函数名是区分大小写
- 函数名不能是保留字
Tip:保留字是Python底层框架中已经有用到或者声明的特定字符串,已经在Python中具有了特定的功能、语法意义。
Python中的保留字请参考下表:
保留字 | 说明 |
---|---|
and | 用于表达式运算,逻辑与操作 |
as | 用于类型转换 |
assert | 断言,用于判断变量或条件表达式的值是否为真 |
break | 中断循环语句的执行 |
class | 用于定义类 |
continue | 继续执行下一次循环 |
def | 用于定义函数或方法 |
del | 删除变量或者序列的值 |
elif | 条件语句 与if else 结合使用 |
else | 条件语句 条件语句,与if,elif结合使用。也可以用于异常和循环使用 |
exceptexcept | 包括捕获异常后的操作代码,与try,finally结合使用 |
exec | 用于执行python语句 |
for | 循环语句 |
finally | 用于异常语句,出现异常后,始终要执行finally包含的代码块与try,except结合使用 |
from | 用于导入模块,与import结合使用 |
global | 定义全局变量 |
if | 条件语句,与else,elif结合使用 |
import | 用于导入模块,与from 结合使用 |
in | 判断变量是否存在序列中 |
is | 判断变量是否为某个类的实例 |
lambda | 定义匿名函数 |
not | 用于表达式运算,逻辑非操作 |
or | 用于表达式运算,逻辑或操作 |
pass | 空的类,函数,方法的占位符 |
打印语句 | |
raise | 异常抛出操作 |
return | 用于从函数返回计算结果 |
try | 包含可能会出现异常的语句,与except,finally结合使用 |
while | 循环语句 |
with | 简化Python的语句 |
yield | 用于从函数依次返回值 |
参数
函数中的参数,是用来向函数中传递函数内部代码块需要用到的值的媒介。
参数的形式分为了形参和实参。
- 形参是紧跟在函数名后括号内的参数名称,用来提醒用户这边需要传入的值的信息。顾名思义,形参就是形式参数,只是一个用来提醒的代号
- 实参是我们在调用函数的时候函数名后面括号中的参数,也就是我们实际传入的参数
- 形参与实参需要一一对应,否则调用函数的时候会报错
函数中的参数,主要分为以下几种:
- 位置参数(必选参数)
- 关键字参数
- 默认参数
- 可变参数
位置参数(必选参数)
def eat_lunch(person, food):
print(f'{person} eat {food} for lunch.')
eat_lunch('Drink', 'bacon') # Drink eats bacon for lunch.
位置参数必须以对应的关系一个一个传递进入函数,函数调用时传递的实参必须和函数定义时的形参一一对应,不能多也不能少,顺序也得一致。
如上述eat_lunch函数中,我们需要传入两个参数,一个是吃午餐的人,一个是午餐的食物,最终打印出'Drink eats bacon for lunch.'。如果我们把person和food两个参数对调传入,就会变成'bacon eats Drink for lunch.'。变成培根中午吃Drink,很可怕吧~当然在上述函数中我们只是打印,不会出大问题。但是在实际开发中,我们常常传入不同类型的参数,如果把Dictionary传递给List,那么系统将会崩溃。
关键字参数
def eat_lunch(person, food):
print(f'{person} eat {food} for lunch.')
eat_lunch(food = 'bacon', person = 'Drink') # Drink eats bacon for lunch.
关键字参数与位置参数不同,关键字参数的位置可以随意调换,但是书写的时候,必须要在传入实参的之前说明对应的形参。
如上述代码所示,我在给函数传入实参的时候,事先说明了对应的形参,便可以无所谓参数传入的顺序。
默认参数
def eat_lunch(person, food='bacon'):
print(f'{person} eat {food} for lunch.')
eat_lunch('Drink') # Drink eats bacon for lunch.
默认参数,顾名思义就是给函数需要传入的参数设置默认值。当调用该函数的时候,如果没有传入已预设默认值的参数时,调用函数的时候就会自动去读取预设的默认值。
如上述代码所示,当我们创建eat_lunch函数的时候,对food参数设置了默认值为'bacon'。我们调用eat_lunch函数,只传了个'Drink',系统默认我们传的是person参数。后面执行函数的时候,因为实参里按照位置参数的方式找不到food参数,所以直接读取了food参数的默认值。
注意:
- 创建函数的时候,必选参数必须放在默认参数的前面,否则系统会报错
- 一般我们对参数值变化小的参数去设置默认参数
- 设置默认参数的时候,尽量使用不可变数据类型。如果使用可变数据类型(如List),该可变数据类型只会在第一次函数被调用的时候去初始化,后续调用则沿用初始化后的参数
可变参数
我们在开发中,可能存在一种情况,就是我们不太确定传入的参数数量的时候,那么我们就可以选择创建函数的时候,将形参设置成可变参数。可变参数主要分为两种:
*args:单星号参数
单星号参数的实质是传入一个元组。打印输出的时候也是返回一个元组
def print_info(*args):
print('args')
print_info('Honda', 'Toyota', 'Nissan', 'Suzuki') # ('Honda', 'Toyota', 'Nissan', 'Suzuki')
**kwargs:双星号参数
def print_info(**kwargs):
for key, values in kwargs.items():
print(f'{key} = {values}')
print_info('Honda' : '本田', 'Toyota' : '丰田', 'Nissan' : '尼桑', 'Suzuki' : '铃木')
# Honda = 本田
# Toyota = 丰田
# Nissan = 尼桑
# Suzuki = 铃木
双星号参数的实质是传入一个字典,在传值的时候,一定要按照键值对的形式传值,且规则需同字典一致,否则系统会报错。
单星号参数和双星号参数其实也可以混搭使用
def print_info(*args, **kwargs):
pass
实质上是声明了一个元组和一个字典,将不以键值对形式传入的参数塞入元组中,然后将以键值对形式传入的参数塞入字典中。
注意:Python中函数的参数是引用传递(注意不是值传递)。对于不可变类型(String、Tuple、Number),因变量不能修改,所以运算不会影响到变量本身;而对于可变类型(List、Dictionary)来说,函数内对于传入的参数的值进行了修改,函数外的该参数的值也会被修改。
作用域(Scope)和命名空间(NameSpace)
在Python中,变量的访问取决于变量赋值的地方。变量的作用域决定了变量的访问权限。Python中变量的作用域一共分为如下几种:
- Local:局部作用域
- Enclosing:闭包函数外的函数中
- Global:全局作用域
- Built-in:内建作用域
作用域的查找顺序如下所示:Local -> Enclosing -> Global -> Built-in
函数会先去找自身作用域内的局部变量是否含有需要的参数,如果没有的话,就去该查找该函数外层的局部变量(如,闭包),还是找不到的话就去找该文件下的全局变量,实在还是找不到,便去查找项目中的内建变量。
创建函数(def/lambda)的时候会创建新的作用域,每个生成器表达式都有引入新的作用域,意思是每次创建一个新函数、新生成器的时候,系统会相对地形成一个作用域,用于该函数、该生成器内部放置代码块。Class的定义是没有作用域的,只会创建一个命名空间。
命名空间(NameSpace)
命名空间是从名字到对象的一个映射。大部分的命名空间都是以 Python中的字典来实现的。有一些常见的命名空间:如类、如全局变量等。
命名空间都是有创建时间和生存期的。对于Python built-in names组成的命名空间,它在Python解释器启动的时候被创建,在解释器退出的时候才被删除;对于一个Python模块的global namespace,它在这个module被import的时候创建,在解释器退出的时候退出;对于一个函数的local namespace,它在函数每次被调用的时候创建,函数返回的时候被删除。
两个不同的命名空间中的两个名字相同的变量之间是不存在任何联系的,因为他们分属于不同的空间。
命名空间的种类有:
-
built-in
名字集合,包括像abs()
这样的函数,以及内置的异常名字等。通常,使用内置这个词表示这个命名空间-内置命名空间 - 模块全局名字集合,直接定义在模块中的名字,如类,函数,导入的其他模块等。通常,使用全局命名空间表示。
- 函数调用过程中的名字集合,函数中的参数,函数体定义的名字等,在函数调用时被“激活”,构成了一个命名空间。通常,使用局部命名空间表示。
- 一个对象的属性集合,也构成了一个命名空间。但通常使用
objname.attrname
的间接方式访问属性,而不是直接访问,故不将其列入命名空间讨论。 - 类定义的命名空间,通常解释器进入类定义时,即执行到
class ClassName:
语句,会新建一个命名空间。
作用域(Scope)
作用域是Python中的一块文本区域。在该文本区域中,可以对命名空间直接访问,而不是通过属性来访问。直接访问的意思是,我们可以通过一个变量名在所有的命名空间内去查找该变量。属性访问(间接访问)的意思是,我们必须要通过该变量的.语法去访问相应的内容。
那么作用域和命名空间之间的关系是怎么样的呢?
- 作用域是由命名空间按照特定的层级结构组合起来的
- 作用域一定是个命名空间,但命名空间不一定是作用域
返回值
Python中的函数使用return语句返回“返回值”,我们可以将获得“返回值”赋予其他变量用于其他用处。所有函数都返回值,如果没有return语句,会隐式地调用return None作为返回值。
一个函数中可以存在多条return语句,但只有一条可以被执行,如果没有任何一条return语句被执行,同样会隐式调用return None作为返回值。
如果函数执行了return语句,函数会立刻结束调用且返回返回值,其余的语句将不会再被执行了。