函数

罗大富 BigRich大约 15 分钟Python

在编写程序的过程中,可以将完成重复工作的语句提取出来,将其编写为函数。这样,在程序中也可以方便地调用函数来完成这些重复的工作,而不必重复地粘贴复制代码。此外,函数也可以使得程序结构更加清晰,更容易维护。

1. 函数基础

1.1. 函数的声明与调用

在 Python 中,函数必须先声明,然后才能调用它,使用函数时,只要按照函数定义的形式,向函数传递必须的参数,就可以调用函数完成相应的功能或者获得函数返回的处理结果。

在 Python 中,使用 def 可以声明一个函数,完整的函数是有函数名参数以及函数体(语句)组成。与之前的基本语句一样,在函数生命中,也要使用缩进以表示语句属于函数体。

如果函数有返回值,那么需要在函数中使用 return 语句返回计算结果,声明函数的一般形式如下:

def '函数名'([参数列表]):
	'函数语句'
	return '返回值'

其中参数列表和返回值不是必须的,return 后也可以不跟返回值,甚至连 return 也没有。对于 return 后没有返回值的和没有 return 语句的函数都会返回 None 值。

有些函数可能既不需要传递参数,也没有返回值。即使没有参数,包裹参数的小括号也不许协商,括号后面也必须有冒号:

首先我们定义一个最简单的函数,示例:

# 定义一个函数,打印'Hello, Python'
def hello():	# 函数名为 hello, 无参数
    print('Hello, Python')	# 缩进的语句表示函数内的语句

在第三章中,我们学习过 input(),print()函数,第四章中,我们也学习了len(), range() 函数,这些都是 Python 的内建函数。

而调用自己定义的函数与调用内建函数及标准库中的函数方法都是相同的,要调用指定的函数就在语句中使用函数名,并且在函数名之后用小括号将调用参数括起来(即使没有参数,也必须写圆括号),而多个阐述之间则用逗号隔开。

调用自定义函数与内建函数不同点在于自定义函数调用前,必须声明函数。

因此,我们可以这样调用上面的函数:

hello()

再举一个函数参数与返回值的简单例子:

# 对列表内的所有元素求和

def list_sum(list_num):
    result = 0
    for i in list_num:
        result += i
    return result

# 声明一个列表
list_a = [1, 4, 5, 23, 5]

# a. 
print(list_sum(list_num=list_a))
# b. 
print(list_sum(list_a))

1.2. 函数值传递与引用传递

通常情况下,定义函数时都会选择有参数的函数形式,函数参数的作用是传递数据给函数,令其对接收的数据做具体的操作处理。

在使用函数时,经常会用到形式参数(简称“形参”)和实际参数(简称“实参”),二者都叫参数,之间的区别是:

  • 形式参数:在定义函数时,函数名后面括号中的参数就是形式参数,例如:

    #定义函数时,这里的函数参数 obj 就是形式参数
    def demo(obj):    
        print(obj)
    
  • 实际参数:在调用函数时,函数名后面括号中的参数称为实际参数,也就是函数的调用者给函数的参数。例如:

    a = "C语言中文网"
    #调用已经定义好的 demo 函数,此时传入的函数参数 a 就是实际参数
    demo(a)
    

实参和形参的区别,就如同剧本选主角,剧本中的角色相当于形参,而演角色的演员就相当于实参。

明白了什么是形参和实参后,再来想一个问题,那就是实参是如何传递给形参的呢? Python 值传递引用(地址)传递

  1. 值传递:适用于实参类型为不可变类型(字符串、数字、元组);
  2. 引用(地址)传递:适用于实参类型为可变类型(列表,字典);

值传递和引用传递的区别是,函数参数进行值传递后,若形参的值发生改变,不会影响实参的值;而函数参数继续引用传递后,改变形参的值,实参的值也会一同改变。

例如:

# 定义一个名为 demo 的函数,分别为传入一个字符串类型的变量(代表值传递)和列表类型的变量(代表引用传递)

def demo(obj) :
    obj += obj
    print("形参值为:",obj)

print("-------值传递-----")
a = "C语言中文网"
print("a的值为:",a)
demo(a)
print("实参值为:",a)
print("-----引用传递-----")
a = [1,2,3]
print("a的值为:",a)
demo(a)
print("实参值为:",a)

分析运行结果不难看出,在执行值传递时,改变形式参数的值,实际参数并不会发生改变;而在进行引用传递时,改变形式参数的值,实际参数也会发生同样的改变。

2. 参数类型

函数的参数除了上节介绍的一种方式之外,还可以有多种形式。例如,在调用某些函数,既可以向其传递参数,也可以不传递参数,函数依然可以正确调用;还有一些情况,比如函数中的参数数量不确定,可能是 1 个,也可能是多个。对于这种情况,Python 可以定义不同的参数类型。

2.1. 位置参数

位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。

在调用函数,指定的实际参数的数量,必须和形式参数的数量一致(传多传少都不行),否则 Python 解释器会抛出 TypeError 异常,并提示缺少必要的位置参数。

# 计算矩形的面积
def area(width, length):
    return width * length

# 调用函数时,必须传递两个参数,否则报错
print(area(2, 4, 5))

运行结果为:

TypeError: area() takes 2 positional arguments but 3 were given

可以看到,抛出的异常类型为 TypeError,具体是指 area() 函数需要 2 个位置参数,但是获取了 3 个。

同样,少传参也会抛出异常:

print(area(2))

运行结果为:

TypeError: area() missing 1 required positional argument: 'height'

抛出的异常类型为 TypeError,具体是指 area () 函数缺少一个必要的 height 参数。

如果我们传入的参数顺序不对应,也会有问题,示例:

# 计算半径为2,高度为4圆柱的体积
def volume(height, radius):
    return 3.14 * radius ** 2 * height


print(f'正确结果:{volume(4, 2)}')
print(f'错误结果:{volume(2, 4)}')

2.2. 关键字参数

目前为止,我们使用函数时所用的参数都是位置参数,即传入函数的实际参数必须与形式参数的数量和位置对应。而本节将介绍的关键字参数,则可以避免牢记参数位置的麻烦,令函数的调用和参数传递更加灵活方便。

关键字参数是指使用形式参数的名字来确定输入的参数值。通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。

因此,Python 函数的参数名应该具有更好的语义,这样程序可以立刻明确传入函数的每个参数的含义。

示例如下:

# 创建一个函数,用来打印两个字符串

def print_two_string(str_1, str_2):
    print(f'第一个参数是: {str_1}, 第二个参数是: {str_2}')


# 位置传参
print_two_string('a', 'b')
print_two_string('b', 'a')

# 关键字传参
print_two_string(str_1='a', str_2='b')
print_two_string(str_2='a', str_1='b')

# 混合传参
print_two_string('a', str_2='b')

可以看到,在调用有参函数时,既可以根据位置参数来调用,也可以使用关键字参数来调用。在使用关键字参数调用时,可以任意调换参数传参的位置。

当然,还可以使用位置参数和关键字参数混合传参的方式。但需要注意,混合传参时关键字参数必须位于所有的位置参数之后。也就是说,如下代码是错误的:

print_two_string(str_1='a', 'b')

解释器会抛出异常:

SyntaxError: positional argument follows keyword argument

2.3. 默认值参数

在 Python 中,可以在声明函数的时候,预先为参数设置一个默认值,当调用函数,如果某个参数具有默认值,则可以不向函数传递该参数,这时,函数将使用声明函数时为该参数设置的默认值来运行。

声明一个参数具有默认值的参数形式如下:

def '函数名'('参数'='默认值'):
	'语句'

示例:

# 声明一个带默认值参数的函数

def hello(name='Python'):  # 设置参数 name 的默认值为 'Python'
    print(f'Hello, {name}')


# 不传参调用该函数
hello()

# 传参调用
hello('Pycharm')

代码中声明的默认值参数 name 的函数 hello(), 若调用时不加参数,则 name 值被自动赋予 'Python', 如果添加了参数,则 name 值会被赋予所给参数值。

如果声明函数时,其参数列表中既包含无默认值参数,又包含默认值参数,那么声明函数的参数时,必须先声明无默认值参数,后声明默认值参数。

示例如下:

# 计算 a 与 b 的和,a 默认值为 10

def sum(a=10, b):
    return a + b

我们执行上述代码时,会得到错误提示 SyntaxError: non-default argument follows default argument,意思是语法错误,非默认值参数写在了默认值参数的后面。

正确写法应该是:

# 计算 a 与 b 的和,a 默认值为 10
def sum(b, a=10):
    return a + b

2.4. 可变数量参数传递

在设计函数的时候,有时候我们能够确认参数的个数,比如一个用来计算矩形面积的函数,它所需要的参数就是长乘以宽,这个函数的参数是确定的。但是,很多时候参数的个数是不确定的,这时就需要用到可变数量参数。

在自定义函数时,如果参数名前加上一个星号 '*', 则表示该参数就是一个可变长参数。

在调用该函数时,如果依次序将所有的其他变量都赋予值之后,剩下的参数将会收集在一个元组中,元组的名称就是前面带星号的参数名。

示例,定义一个只带有一个型号的参数的函数:

def function(*args):
    print(type(args))
    print(args)

function(1, 2, 4)

执行以上代码,我们得到输出类型为元组(tuple),并且把所有参数都收集到一个元组中。

当自定义函数的参数中,含有前面所介绍的几种类型的参数时,一般来说,带星号的参数应该放到最后。当带星号的参数放在最前面时,仍然可以正常工作,但调用时后面的参数必须以关键字参数方式提供,否则因其后的位置参数无法获取值而发生错误。

示例如下:

def function_2(*args, param_1, param_2='默认参数'):
    print(f'args的值为:{args}')
    print(f'param_1的值为:{param_1}')
    print(f'param_2的值为:{param_2}')

# 正确示范
function_2(1, 2, 3, param_1=4)
# 错误使用
function_2(1, 2, 3)

function_2() 函数中有三种类型的参数, 并且带星号的参数放在最前面。第一次调用时给了 3 个位置参数和 1 个关键字参数,因此,args 变量收集了 1、2、3 作为一个元组,而变量 param_1 则取值为4, 变量 param_2 则使用了默认值;第二次调用,没有提供关键字参数,无默认值的参数 param_1 没有或得到值,因此调用失败。

使用元组来收集参数时,调用时提供的参数不能为关键字参数,如果要收集不定数量的关键字参数可以在自定义函数时的参数前加两颗星即 **kwargs, 这样一来,多余的关键字参数就可以以字典的方式被收集到变量 kwargs 中。

示例:

def function_3(**kwargs):
    print(type(kwargs))
    print(kwargs)

function_3(a=1, b=2)

运行结果:

<class 'dict'>
{'a': 1, 'b': 2}

注意,收集关键字参数时,要放在函数声明的参数列表中的所有参数之后。

示例,声明一个带有大量关键字参数的函数并调用:

def cube(name, **features):
    print(f'======== {name} 方块的特征:=============')
    print(f'体积是{features["length"] * features["width"] * features["height"]}')
    print(f'质量为{features["mass"]}')
    print(f'颜色为{features["color"]}')


cube('a', length=1, width=2, height=3, mass=4, color='green')
cube('b', length=3, width=34, height=12, mass=65, color='red')

运行结果为:

======== a 方块的特征:=============
体积是6
质量为4
颜色为green
======== b 方块的特征:=============
体积是1224
质量为65
颜色为red

代码中定义了一个 cube() 函数,其参数为两个,第一个是普通参数 name, 第二个是可变长关键字参数。

3. 函数的返回值

所谓返回值,就是程序中函数执行完成后,返回给调用者的值。

例如,定义一个两数相加的函数

def sum(a, b):
	result = a + b
	return result

r = sum(1, 3)
print(r)

上述代码中,定义了两数相加的函数。功能完成后,会将相加的结果返回给函数调用者,所以,变量 r 接受了函数的执行结果。

注意:函数体在遇到 return 之后就结束了,因此,return 之后的代码不会执行

如果函数没有使用 return 语句返回数据,那么函数有返回值吗?

答案是有的。Python 中有一个特殊的常量:None.无返回值的函数,实际上就是返回了 None 这个常量。

示例:

def sum(a, b):
	result = a + b

r = sum(1, 3)
print(r, type(r))

4. 变量的作用域

在 Python 中,作用域可以分为:

  • 内置作用域:Python 预先定义的;
  • 全局作用域:所编写的整个程序;
  • 局部作用域:某个函数内部范围。

每次执行函数,都会常见一个新的命名空间,这个新的命名空间就是局部作用域,同一函数不同时间运行,其作用域都是独立的,不同的函数也可以使用相同的参数名,其作用域也是独立的。在函数内已经声明的变量名,在函数以外依然可以使用。并且在程序运行的过程中,其值并不互相影响。

示例:

def demo():
    a = 3  # 在函数内声明变量 a 并赋值为 3
    a += 2
    print('函数内 a:', a)


a = '全局作用域'  # 在全局作用域内 声明变量 a 并赋值为'全局作用域'
print('全局作用域 a:', a)
demo()
print('全局作用域 a:', a)

运行结果:

全局作用域 a: 全局作用域
函数内 a: 5
全局作用域 a: 全局作用域

代码中在函数中声明了变量 a = 3,在函数外也声明了同名变量 a = '全局作用域'。在调用函数前后,函数外声明的变量 a 的值不变。在函数内可以对 a 的值进行任意操作,他们互不影响。

两个变量 a 出于不同的作用域中,所以互不影响。但是,有一种方法可以使函数中引用全局变量并进行操作,即在变量名前加 global 关键字。

示例:

def demo():
    global a
    a = 3  # 在函数内声明变量 a 并赋值为 3
    a += 2
    print('函数内 a:', a)


a = '全局作用域'  # 在全局作用域内 声明变量 a 并赋值为'全局作用域'
print('全局作用域 a:', a)
demo()
print('全局作用域 a:', a)

运行结果:

全局作用域 a: 全局作用域
函数内 a: 5
全局作用域 a: 5

在局部作用域内可以引用全局作用域内的变量,但不可以修改它。

示例:

# 声明全局变量
a = 3


# 声明函数用来打印全局变量 a
def print_a():
    print(a)


print_a()

运行结果:

3

运行该函数时,会输出全局变量 a 的值 3,此时,如果将其改为 5 则会引发错误:

a = 3


# 声明函数用来打印全局变量 a
def print_a():
    print(a)
    a = 5


print_a()

运行会抛出异常:

UnboundLocalError: local variable 'a' referenced before assignment

5. 匿名函数 lambda

lambda 可以用来创建匿名函数,也可以将匿名函数赋给一个变量调用,它是 Python 中一类比较特殊的声明函数的方式,lambda来源于 LISP 语言,其语法形式如下:

lambda arguments : expression

其中 arguments 相当于声明函数时的参数(多个参数用逗号分隔),expression 是函数要返回值的表达式,而表达式中不能包含其他语句,也可以返回元组(要用括号),还允许在表达式中调用其它函数。

示例:

# 计算两个数的平方和
square_sum = lambda x, y: x ** 2 + y ** 2

print(square_sum(1, 2))

lambda适合以下类型的函数:

  • 为了代码清晰,有些地方使用它,代码更清晰易懂
  • 复用性低,在有些时候需要一个抽象简单的功能,又不想单独定义一个函数;
  • 写起来快速简单,省代码。

6. 函数的注释

之前我们一直在讲,写代码一定要记得写注释,尤其是对函数而言,因为函数是一个代码块,如果我们选择,逐行解释代码的作用,效率比较低。

因此,我们可以给函数添加说明文档,帮助理解函数的作用。

语法如下:

def sum(x, y):
    """
	函数说明
    :param x: 形参 x 的说明
    :param y: 形参 y 的说明
    :return: 返回值的说明
    """
    return x + y
上次编辑于:
贡献者: Luo