函数

0 阅读22分钟

在前面几个章节中我们经常使用到print(),那么它是什么呢?
print() 是一个函数,可以向控制台打印输出内容。

一、函数的概念

函数是带名字的代码块,用于完成具体的任务,可重复使用。当需要在程序中多次执行同一项任务时,无须反复编写完成该任务的代码,只需要调用执行该任务的函数,让Python运行其中的代码即可。

通过使用函数,程序编写、阅读、测试和修复起来都更加容易。Python中的函数必须先定义后使用,Python提供了许多内建函数,比如print()。也可以自己创建函数,这被叫做用户自定义函数。

案例:在控制台打印输出一个2x3的*,那么可以编写如下代码

# 该案例演示了向控制台打印2*3的 "*"
row = 2
while row > 0 :
    print("*" * 3)
    row -= 1

如果:我现在想要再次输出这样的图形,那么以我们现在的知识,我们的代码就会很冗余。那么这个时候就可以通过定义函数来解决我们的问题。

row = 2
while row > 0 :
    print("*" * 3)
    row -= 1
print("-" * 50)

# 如果需要再次输出
row = 2
while row > 0 :
    print("*" * 3)
    row -= 1
二、函数的定义
1、语法

Python 定义函数使用 def 关键字,一般格式如下:

def 函数名 (参数列表) :
        函数体
        [return]
2、定义一个函数的规则
  • 函数代码块以def关键词开头,后接函数标识符名称和圆括号 ()。
  • 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。用三个引号括起来,单引号和双引号都可以。
  • 函数参数后面以冒号结束。
  • 函数体开始缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回None。
3、函数名

函数名是程序员给这个函数起的名称,需要遵循标识符的命名规则。
函数名一般是一个动词,第一个单词小写,其他每个单词的首字母大写。

4、参数

函数在完成某个功能时,可能需要一些数据,在定义函数时指定函数参数来接收这些数据。如在屏幕上打印信息,需要把要打印的信息传递给print()函数。如果有多个参数,参数之间使用逗号分隔。函数也可以没有参数,但是小括弧不能省略。

5、函数体

调用函数时执行的代码,可包含函数说明文档与返回值。

6、返回值

有些函数在执行的时候,需要有返回值给调用者,通过return关键字进行返回,返回到函数调用的位置。

三、函数的抽取以及调用

在上面打印两次2x3的的案例中,我们的代码出现了冗余。我们分析,可以将打印2x3的这个功能封装为一个单独的函数。
函数定义好之后,通过函数名()对函数进行调用。

(1)案例:打印两次2x3的*。

'''
    该案例演示了函数的抽取以及调用
    打印如下图形
        ***
        ***
        -------
        ***
        ***
'''
# 定义一个函数,该函数完成打印输出2*3 "*"的功能
def printStar() :
    '''
        这是对函数功能的说明
    '''
    row = 2
    while row > 0 :
        print("*" * 3)
        row -= 1
 
# 调用函数
printStar()
print("-" * 20)
printStar()

(2)注意:

  • 函数必须先定义再调用
  • 函数在定义的时候只是告诉解释器我定义了一个这样的函数,可以完成某些功能,但是这个时候函数还没有执行,需要调用函数后,才会执行。
四、使用函数的好处
  • 使程序变得更简短而清晰
  • 可以提高程序开发的效率
  • 提高了代码的重用性
  • 便于程序分工协作开发
  • 便于代码的集中管理
  • 有利于程序维护
五、函数的参数
1.参数的抽取

在上面的案例中,虽然我们抽取了函数去完成打印2x3的*功能。如果现在希望打印出如下图形,该如何实现?
image.png

1)第一种方式:不封装函数

'''
    该案例演示了不封装函数
    向命令窗口打印输出
    ***
    ***
    --------
    ****
'''

row = 2
while row > 0 :
    print("*" * 3)
    row -= 1
print("-" * 20)
row = 1

while row > 0 :
    print("*" * 4)
    row -= 1

2)第二种方式:封装函数

'''
    该案例演示了封装函数
    向命令窗口打印输出
    ***
    ***
    --------
    ****
'''

def printStar1() :
    '''
        该函数可以打印2行3列的*
    '''

    row = 2
    while row > 0 :
        print("*" * 3)
        row -= 1

def printStar2() :
    '''
        该函数可以打印1行4列
    '''

    row = 1
    while row > 0 :
        print("*" * 4)
        row -= 1

# 调用函数
printStar1()
print("-" * 20)
printStar2()

注意:这里我们虽然封装了函数,但是printStar1和printStar2这两个函数中的大部分代码还是一样的,存在冗余。

我们从上图可以看出来,printStar1和printStar2中不一样的地方就是在函数体中row变量值和打印的个数不一样。这两个值分别可以理解代表打印行数以及打印的列数,既然我们现在的需求,打印的行和列是不固定的,所以我们只提供打印的函数,具体要打印几行几列的,让函数的调用者自己决定。这就要求我们在提供函数的时候,需要通过函数的参数接收行和列数据(函数的形式参数-形参);在调用函数的时候需要把行和列数据作为参数传递过来(函数的实际参数-实参)。

3)第三种方式:封装带参数的函数

'''
    该案例演示了封装带参数的函数
    向命令窗口打印输出
    ***
    ***
    --------
    ****
'''

def printStar(row,col) :
    while row > 0 :
        print("*" * col)
        row -= 1

printStar(2,3)
print("-" * 20)
printStar(1,4)
2.形参和实参
  • 定义函数时,指定的参数称为形式参数,简称为形参(函数的提供者)
  • 调用函数时,给函数传递的参数称为实际参数,简称为实参(函数的调用者)
  • 在定义函数时,形参没有分配存储空间,也没有值,相当于一个占位符;
  • 在调用函数时, 会在栈区中给函数分配存储空间, 然后给形参/局部变量分配存储空间,传递的是实际的数据
  • 当函数执行结束,函数所占的栈空间会被释放,函数的形参/局部变量也会被释放。
3.函数的参数传递

1)在 python 中,类型属于对象,变量是没有类型的:

a=10
a="helloworld"

以上代码中,10是数字类型," helloworld " 是 String 类型,而变量 a 是没有类型,她仅仅是一个对象的引用(一个指针),可以是指向数字类型对象,也可以是指向 String 类型对象。

2)引用的概念
在 Python 中,变量和数据是分开存储的,数据保存在内存中的一个位置,变量中保存着数据在内存中的地址,变量中记录数据的地址,就叫做引用。

使用id()函数可以查看变量中保存数据所在的内存地址

注意:如果变量已经被定义,当给一个变量赋值的时候,本质上是修改了数据的引用,变量不再对之前的数据引用,变量改为对新赋值的数据引用,变量的名字类似于便签纸贴在数据上。

3)可变(mutable)与不可变(immutable)类型对象
在Python常见的类型中,数字类型、string、tuple和number是不可更改的对象,而list、set、dict等则是可以修改的对象。

  • 不可变类型
    变量赋值 a=500 后再赋值 a=1000,这里实际是新生成一个 int 值对象 1000,再让 a 指向它,而500被丢弃,不是改变a的值,相当于新生成了a。
  • 可变类型
    变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。

4)Python函数的参数传递

  • 不可变类型
    类似c++的值传递,如整数、字符串、元组。如fun(a),传递的只是a的值,没有影响a对象本身。比如在fun(a)内部修改a的值,只是修改另一个复制的对象,不会影响 a 本身。
  • 可变类型
    类似c++的引用传递,如列表,字典。如 fun(la),则是将 la 真正的传过去,修改后fun外部的la也会受影响

Python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。****

案例: Python函数传不可变对象实例

'''
    该案例演示了Python函数传递不可变对象
'''

def changeInt(a) :
    print("函数体中未改变前a的内存地址",id(a))
    a = 10
    print("函数体中改变后a的内存地址",id(a))

b = 2
changeInt(b)
print(b)
print("函数外b的内存地址",id(b))

输出结果:
函数体中未改变前a的内存地址 140711474555352
函数体中改变后a的内存地址 140711474555608
2
函数外b的内存地址 140711474555352

id()查看对象的内存地址

说明:实例中有 int 对象 2,指向它的变量是b,在传递给 changeInt 函数时,按传值的方式复制了变量 b,a 和 b 都指向了同一个 int 对象,函数外b的内存地址和未改变前a的地址是相同的。在 a=10 时,则新生成一个 int 值对象 10,并让 a 指向它。这个时候内存地址也发生了改变。

案例:Python函数传可变对象实例

'''
    该案例演示了Python函数传递可变对象
'''

def changeList(myList) :
    myList[1] = 50
    print("函数内的值",myList)
    print("函数内列表的内存",id(myList))

mlist = [1,2,3]
changeList(mlist)
print("函数外的值",mlist)
print("函数外列表的内存",id(mlist))

输出结果:
函数内的值 [1, 50, 3]
函数内列表的内存 1546427570560
函数外的值 [1, 50, 3]
函数外列表的内存 1546427570560

说明:
可变对象在函数里修改了参数,那么在函数外面,这个原始的参数也被改变了。
通过内存地址的输出,我们可以看出来,是在原有的列表对象上进行的修改。

5)var1 *= 2 与 var1 = var1 * 2 的区别:

  • var1 *= 2使用原地址。
  • var1 = var1 * 2开辟了新的空间。
  • 同样的对于类似,var1 +=2 和 var1 = var1 + 2也是同理
def multiply2(var1):
    print("函数内var1 id:", id(var1))
    var1 *= 2
    print("var1 *= 2后,函数内var1 id:", id(var1))
    var1 = var1 * 2
    print("var1 = var1 * 2后,函数内var1 id:", id(var1))
 
list1 = [1, 2, 3]
print("list1 id:", id(list1))
multiply2(list1)

# 输出结果:
list1 id: 2302584035712
函数内var1 id: 2302584035712
var1 *= 2后,函数内var1 id: 2302584035712
var1 = var1 * 2后,函数内var1 id: 2302584033664
4.函数可使用的参数形式

1)必须参数
调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。为此,最简单的关联方式是基于位置把每个相应位置的实参和形参相关联,调用时的数量必须和声明时的一样。

'''
    该案例演示了函数位置实参
'''

def func(a, b, c):
    print(a, b, c)

func(1, 2, 3)  # 1 2 3

可以看到,1传给了a,2传给了b,3传给了c。

2)关键字参数
函数调用使用关键字参数来确定每个变量传入的参数值,使用关键字参数允许函数调用时参数的顺序与声明时不一致。

'''
    该案例演示了函数调用时的关键字参数
'''

def printInfo(name,age) :
    print("姓名:",name)
    print("年龄:",age)
 
# Python解释器可以通过age和name这样的关键字去和形参进行匹配
printInfo(name = "zhangsan",age = 18)
printInfo(age = 18,name = "zhangsan")

3)默认值参数
定义函数时,可给每个形参指定默认值。在调用函数时,给形参提供了实参则使用指定的实参值,否则使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。

'''
    该案例演示了函数调用时的默认参数
'''

def printInfo(name,age = 20) :
    print("姓名:",name)
    print("年龄:",age)

printInfo("zhangsan")

printInfo("lisi",30)

printInfo(age = 40,name = "wangwu")

4)不定长参数
参数的个数是不确定的。
(1)语法:

def 函数名([普通参数,] *var_args_tuple ):
   函数体

(2)案例:

'''
    该案例演示了函数调用时的不定长参数
'''

def printInfo(num,*vartuple):
    print(num)
    print(vartuple)

printInfo(70,60,50)

print("-" * 20)

# 如果不定长的参数后面还有参数,必须通过关键字参数传参
def printInfo1(num1,*vartuple,num) :
    print(num)
    print(num1)
    print(vartuple)
 
printInfo1(10,20,num = 40)

print("-" * 20)

# 如果没有给不定长的参数传参,那么得到的是空元组
printInfo1(70,num = 60)

(3)注意:

  • 加了星号 * 的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。
  • 如果形参中出现了不定长参数,那么在调用函数的时候,先通过位置进行必须参数的匹配,然后不定长参数后面的参数必须通过关键字参数匹配
  • 如果不定长的参数后面还有参数,必须通过关键字参数传参
  • 还有一种就是参数带两个星号 **的可变长参数,基本语法如下:
def 函数名([普通参数,] **var_args_dict ):
   函数体

加了两个星号 ** 的参数会以字典的形式导入,后面就不能再有其他参数了

'''
    该案例演示了函数调用时的不定长参数
'''

def printInfo(num,**vardict):
    print(num)
    print(vardict)
    # return

printInfo(10,key1 = 20,key2 = 30)

printInfo(10,a = 20,b = 30)
5.解包传参

若函数的形参是定长参数,可以通过 * 和 ** 对列表、元组、字典等解包传参。

def func(a, b, c):
    return a + b + c

tuple11 = (1, 2, 3)
print(func(*tuple11))
# 字典中key的名称和参数名必须一致
dict1 = {"a": 1, "b": 2, "c": 3}
print(func(**dict1))
6.强制使用位置参数或关键字参数

/ 前的参数必须使用位置传参,* 后的参数必须用关键字传参。

def f(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)

f(1, 2, 3, d=4, e=5, f=6)
7.防止函数修改列表

有时要函数对列表进行处理,又不希望函数修改原列表,可以使用 copy.deepcopy()

import copy

def multiply2(var1):
    var1[3].append(400)
    print("函数内处理后:", var1)

list1 = [1, 2, 3, [100, 200, 300]]
print("函数外处理前:", list1)
multiply2(copy.deepcopy(list1))
print("函数外处理后:", list1)

image.png

六、函数说明文档

编写了函数说明文档后,可以通过 help(函数名)  获取函数说明文档。

def adult(age=18):
    """根据年龄判断是否成年"""
    result = "未成年"[age >= 18 :]
    return result

help(adult)

PyCharm中将鼠标悬停在函数名上方也可以看到函数说明文档。

七、返回值

在程序开发中,有时候希望一个函数执行结束后,告诉调用者一个结果,以便调用者针对具体的结果做后续的处理。返回值就是函数完成工作后,给调用者的一个结果

  • 在函数中使用 return 关键字可以返回结果 ,并结束正在执行的函数
  • 如果return后面跟[表达式],在结束函数的同时向调用方返回一个表达式。
  • 如果仅仅是return关键字,后面没有加内容,函数执行返回调用方None。
  • 调用函数一方,可以使用变量来接收函数的返回结果

(1)不带表达式的 return 语句,返回 None

def f(a, b, c):
    pass
    return

print(f(1, 2, 3))  # None

(2)函数中如果没有 return 语句,在函数运行结束后也会返回 None

def f(a, b, c):
    pass

print(f(1, 2, 3))  # None

(3)用变量接收返回结果

def add(num1,num2) :
    '''求两个数的和'''
    sum1 = num1 + num2
    return sum1

res = add(10,20)
print("两个数的和为:" ,res)

(4)return 语句可以返回多个值,多个值会放在一个元组中。

def f(a, b, c):
    return a, b, c, [a, b, c]
print(f(1, 2, 3))  # (1, 2, 3, [1, 2, 3])
八、函数嵌套调用

在一个函数中调用另一个函数,当内层函数执行完之后才会继续执行外层函数。

def function_A():
    print("\t函数 A 开始执行")
    print("\t函数 A 执行中...")
    print("\t函数 A 结束执行")

def function_B():
    print("函数 B 开始执行")
    print("函数 B 执行中...")
    function_A()
    print("函数 B 执行中...")
    print("函数 B 结束执行")

function_B()

输出结果:
函数 B 开始执行
函数 B 执行中...
        函数 A 开始执行
        函数 A 执行中...
        函数 A 结束执行
函数 B 执行中...
函数 B 结束执行
九、变量的作用域

Python中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。变量的作用域决定了哪一部分程序可以访问哪个变量,Python的作用域一共有4种,分别是:

  • L (Local) 局部作用域
  • E (Enclosing)嵌套作用域 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域

以 L –> E –> G –>B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。以下案例演示各种作用域类型。

'''
    该案例演示了变量的作用域
'''
a = int(2.9)  # 内建作用域 (Python本身提供的,在所有位置都可以访问)
b = 0  # 全局作用域
def outer():
    c = 1  # 嵌套作用域
    def inner():
        d = 2  #局部作用域
        print(d,c,b,a)
    return inner

in_func=outer()
in_func()

Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下代码:

# 分支,循环不会引入新的作用域
num = 2
if num > 1:
    msg = "helloWorld"
print(msg)

def test():
    msg_test = "welcome"
print(msg_test)

实例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。

如果将 msg 定义在函数中,则它就是局部变量,外部不能访问:

从报错的信息上看,说明了 msg_inner 未定义,无法使用,因为它是局部变量,只有在函数内可以使用。

1.全局变量和局部变量

定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。

'''
    该案例演示了全局变量和局部变量
'''

sum = 0  # 这是一个全局变量

def add(num1,num2) :
    sum = num1 + num2  # 这是一个局部变量
    print("函数内局部变量的值:",sum,id(sum))
    return sum

add(10,20)
print(num1) # num1访问不到
print("函数外全局变量:",sum,id(sum))

image.png

2.global关键字

1)使用global修改全局变量

定义了一个全局变量,如何在函数内对其进行修改?
(1)直接在函数内修改
(2) 通过var1 += 200修改。会报错

var1 = 100

def function_a():
    var1 += 200 # 将var1当做局部变量处理,+=得先定义变量
function_a()  # 报错

(3)通过var1 = 200修改。全局变量var1的值并没被修改,仍是100。我们只是在function_a函数中新定义了一个局部变量var1并将其赋值为200。

var1 = 100

def function_a():
    var1 = 200
    print("var1:", var1)

print(var1)  # 100
function_a()  # var1: 200
print(var1)  # 100

(4)在函数内使用 global 声明全局变量
函数内使用 global 声明全局变量后,可以修改全局变量。

def function_a():
    global var1
    var1 = 200
    print("var1:", var1)

var1 = 100
print(var1)  # 100
function_a()  # var1: 200
print(var1)  # 200

2)修改可变类型的全局变量

当全局变量为可变类型时,函数内不使用 global 声明,也可以对其进行修改。

def function_a():
    list1[0] = -1000
    print("list1:", list1)

list1 = [1, 2, 3]
print(list1)  # [1, 2, 3]
function_a()  # list1: [-1000, 2, 3]
print(list1)  # [-1000, 2, 3]

在函数中不使用 global 声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。

不可变类型的全局变量其指向的数据不能修改,所以不使用 global 无法修改全局变量。

可变类型的全局变量其指向的数据可以修改,所以不使用 global 也可修改全局变量。

3.nonlocal关键字

nonlocal 也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域而是嵌套作用域。

def function_outer():
    var1 = 1
    print(var1)
    def function_inner():
        nonlocal var1
        var1 = 200
    function_inner()
    print(var1)

function_outer()  # var1: 1 -> 200
十、递归
1.概念

递归一种是逻辑思想,将一个大工作分为逐渐减小的小工作,比如说一个和尚要搬50块石头,他想,只要先搬走49块,那剩下的一块就能搬完了,然后考虑那49块,只要先搬走48块,那剩下的一块就能搬完了……,递归是一种思想,只不过在程序中,就是依靠函数嵌套这个特性来实现了

2.本质

递归调用就是在函数体中又调用了函数本身

3.在定义递归函数的时候,主要确定两点
  • 确定它们之间的规律
  • 确定递归结束的条件
4.递归案例 求一个整数n的阶乘!
'''
    该案例演示了求整数的阶乘
    5! = 5 * 4 * 3 * 2 *1
'''
# 不使用递归的方式
def get_factorial(num):
    res = 1 #  用于存放积
    for n in range(1,num+1):
        res *= n    
    return res
    
print(get_factorial(5))
print("-"*20)

def get_factorial2(n):
    return n * get_factorial2(n - 1) if n > 1 else 1

print(get_factorial2(5))  # 120
5.递归执行流程分析

image.png

十一、匿名函数
1.语法

Python使用 lambda 来定义匿名函数,所谓匿名,指其不用 def 的标准形式定义函数。

lambda 参数列表: 表达式
  • lambda 只是一个表达式,函数体比def简单很多。
  • lambda的主体是一个表达式,而不是一个代码块,所以仅仅能在lambda表达式中封装有限的逻辑进去。
  • lambda函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
2.使用普通函数传参
def operator(a, b):
    return a + b

def function(a, b, operator):
    return operator(a, b)

print(function(1, 2, operator))
3.使用匿名函数传参
def function(a, b, operator):
    return operator(a, b)

print(function(1, 2, lambda x, y: x + y))
4.匿名函数作为内置函数的参数

可以将匿名函数与常用的内置参数搭配使用。

1)sorted()
有三名学生的姓名和年龄,按年龄排序。

student_list = [{"name": "zhang3", "age": 36}, {"name": "li4", "age": 14}, {"name": "wang5", "age": 27}]

print(sorted(student_list, key=lambda x: x["age"]))

2)map()
map()  函数对序列中元素逐一处理。

map_result = map(lambda x: x * x, [0, 1, 3, 7, 9])
print(list(map_result))  # [0, 1, 9, 49, 81]

3)filter()
filter()  函数对序列中元素过滤。

filter_result = filter(lambda x: x >= 0, [-0, -1, -3, 7, 9])
print(list(filter_result))  # [0, 7, 9]

4)reduce()
reduce()  函数对序列中元素进行累积。

from functools import reduce
reduce_result = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])
print(reduce_result)  # 120
十二、函数的注释

Python 3.x 引入了函数注释,以增强函数的注释功能

# 普通的自定义函数:
def dog(name, age, species):
   return (name, age, species)

# 添加了注释的自定义函数:
def dog(name:str, age:(1, 99), species:'狗狗的品种') -> tuple:
    return (name, age, species)

print(dog.__annotations__)

如上,可以使用:对参数逐个进行注释,注释内容可以是任何形式,比如参数的类型、作用、取值范围等等,返回值使用->标注,所有的注释都会保存至函数的属性。

查看这些注释可以通过自定义函数的特殊属性__annotations__获取,结果会以字典的形式返回,另外,使用函数注释并不影响默认参数的使用

{'name': <class 'str'>, 'age': (1, 99), 'species': '狗狗的品种', 'return': <class 'tuple'>}