Python函数和函数式编程(两万字长文警告!一文彻底搞定函数,建议收藏!)

Python函数和函数式编程

函数是可重用的程序代码段,在Python中有常用的内置函数,例如len()、sum()等。 在Pyhon模块和程序中也可以自定义函数。使用函数可以提高编程效率。

1、函数概述

1.1函数的基本概念

函数用于在程序中分离不同的任务。在程序设计过程中,如果可以分离任务,则建议使用函数分别实现分离后的子任务。

函数为代码复用提供了一个通用的机制,定义和使用函数是Python程序设计的重要组成部分。

函数允许程序的控制在调用代码和丽数代码之间切换,也可以把控制转换到自身的函数,即函数自己调用本身,此过程称为递归(recursion)调用。

1.2函数的功能

函数是模块化程序设计的基本构成单位,使用函数具有如下优点。
(1)实现结构化程序设计:通过把程序分割为不同的功能模块可以实现自顶向下的结构化设计。
(2)减少程序的复杂度:简化程序的结构,提高程序的可阅读性。
(3)实现代码的复用:一次定义多次调用,实现代码的可重用性。
(4)提高代码的质量:实现分割后子任务的代码相对简单,易于开发、调试、修改和维护。
(5)协作开发:在将大型项目分割成不同的子任务后,团队多人可以分工合作,同时进行协作开发。
(6)实现特殊功能:递归函数可以实现许多复杂的算法。

1.3Python中函数的分类

在Python语言中函数可以分为以下4类。
(1)内置函数: Python语言内置了若干常用的函数,例如abs()、Jen()等,在程序中可以直接使用。
(2)标准库函数:Python语言安装程序同时会安装若干标准库,例如math.random等,

通过import语句可以导人标准库,然后使用其中定义的函数。

(3)第三方库函数: Python社区提供了许多其他高质量的库,例如Python图像库等。在下载、安装这些库后,通过import语句可以导人库,然后使用其中定义的函数。

(4)用户自定义丽数:本章将详细讨论的数的定义和调用方法。

2、函数的声明和调用

2.1函 数对象的创建

在Python语言中函数也是对象,使用def语句创建,其语法格式如下。

def函数名([形参列表]) :
函数体

说明:
(1)函数使用关键字def声明,函数名为有效的标识符(命名规则为全小写字母,可以使用下画线增加可阅读性,例如my func),形参列表(用圆括号括起来,并用逗号隔开,可能为空)为函数的参数。函数定义的第一行称为函数签名(signature),函数签名指定函数名称以及函数的每个形式参数变量名称。

(2)在声明函数时可以声明函数的参数,即形式参数,简称形参;形参在函数定义的圆括号对内指定,用逗号分隔。在调用函数时需要提供函数所需参数的值,即实际参数,简称实参。

(3) def是复合语句,故函数体需采用缩进书写规则。

(4)函数可以使用return返回值。如果函数体中包含return语句,则返回值;否则不返回,即返回值为空(None)。无返回值的函数相当于其他编程语言中的过程。

(5) def是执行语句,Python解释执行def语句时会创建一个函数对象,并绑定到函数名变量。

[例1 定义返回两个数的平均值的函数。

def my average(a, b):
    return(a+ b)/2

[例2] 定义打印n个星号的无返回值的函数。

def print star(n):
    print(("."* n). center(50))     #打印n个星号,两边填充空格总宽度为50

[例3] 定义计算并返回第n阶调和数(1+ 1/2+1/3+…+ 1/n)的函数。

def harmonic(n):     #计算n阶调和数(1 + 1/2 + 1/3+.+ 1/n)
    total = 0.00
    for i in range(1, n+ 1):
        total += 1.00/ i
        return total

2.2函数的调用

在进行雨数调用时,根据需要可以指定实际传人的参数值。函数调用的语法格式如下,

函数名([实参列表]);

说明:
(1)函数名是当前作用域中可用的函数对象,即调用函教之前程序必须先执行def语句,创建函数对象(内置函数对象会自动创建import导入模块时会执行模块中的def语句,模块中定义的函数)。函数的定义位置必须位于调用该函数的全局代码之前,故典型的Python程序结构顺序通常为①import语句>②函数定义>③全局代码。

(2)实参列表必须与函数定义的形参列表一一对应
(3)函数调用是表达式。如果函数有返回值,可以在表达式中直接使用;如果函数没有返回值,则可以单独作为表达式语句使用。

[例4]先定义一个打印n个星号的无返回值的函数print_star(n),然后从命令行第一个参数中获取所需打印的三角形的行数lines,并循环调用print_star() 函数输出由星号构成的等腰三角形,每行打印1,3,5,.,2* lines-1个星号。

import sys
def print_star(n): 
    print(("*"*n).center(50)) #打印n个星号,两边填充空格,总宽度50
lines = int(sys.argv[1])      #三角形行数
for i in range(1, 2*lines,2):   #每行打印1、3、5、...个星号
    print_star(i)

在这里插入图片描述
[例5]函数的调用示例 2Charmonic py):定义计算并返回第n阶调和数(1 + 1/2+1/3十…十1/n)的函数,输出前n个调和数。

import sys
def harmonic(n):    #计算n阶调和数(1 + 1/2 + 1/3 + … + 1/n)
    total = 0.0
    for i in range(1, n+1):
        total += 1.0 / i
    return total
n = int(sys.argv[1])     #从命令行第一个参数中获取调和数阶数
for i in range(1, n+1):   #输出前n个调和数的值
    print(harmonic(i))

在这里插入图片描述

2.3函数的副作用

大多数函数授收 一个或多个参数,通过计算返回一个值,这种类型的两牧称为纯函数(pure function),即给定同样的实际参数,其返回值唯一, 且不会产生其他的可观察到的副作用,例如读取键盘输人、产生输出、改变系统的状态等。

相对于纯函数,产生副作用的函数也有一定的应用。在一般情况 下,产生副作用的函数相当于其他程序设计语言中的过程。在这些函数中可以省略return语句:当Python执行完函数的最后一条语句后,将控制权返回给调用者。

例如,函数print star(n)的副作用是向标准输出写人若干星号。编写同时产生副作用和返回值的函数通常被认为是不良编程风格,但有一个例外,即读取函数。例如.input()函数既返回一个值,同时又产生副作用(从标准输人中读取并消耗一个字符串)。

3、参数的传递

3.1形式参数和实际参数

函数的声明可以包含一个[形参列表],而函数调用则通过传递[实参列表],以允许函数体中的代码引用这些参数变量。
声明函数时所声明的参数即为形式参数,简称形参;调用函数时提供函数所需要的参数的值即为实际参数,简称实参。
实际参数值默认按位置顺序依次传递给形式参数。如果参数个数不对,将会产生错误。
[例6]形式参 数和实际参数示例

def my_max1(a, b):
    if a > b: print(a, '>', b)
    elif a == b: print(a, '=', b)
    else: print(a, '<', b)
my_max1(1, 2)
x = 11; y = 8
my_max1(x, y)
my_max1(1)

在这里插入图片描述

3.2形式参数变量和对象引用传递

声明函数时声明的形式参数等同于函数体中的局部变量在函数体中的任何位置都可以使用。

局部变量和形式参数变量的区别在于局部变量在函数体中绑定到某个对象,而形式参数变量则绑定到函数调用代码传递的对应实际参数对象。

Python参数传递方法是传递对象引用,而不是传递对象的值。

3.3传递不可变对象的引用

在调用函数时,若传递的是不可变对象(例如int lont.sr 和bol对象)的引用,则如果函数体中修改对象的值,其结果实际上是创建了一个新的对象。
[例7]错误的递增函数。

i=100
def inc(j,n):
    j += n
inc(i,10)
print(i)

在本例中,i的初值为100,当调用函数inc(i,10)后,在函数体内执行了“1+=10”语句,函数体内的i变成了110但是,当函数调用完毕返回主程序时i的值仍然为100,因为整数i是不可变对象,而在Python语言中一个函数不能改变 一个不可变对象(例如整数、浮点数、布尔值或字符串)的值(即函数无法产生副作用)。

[例8]正确的递增函数。

i=100
def inc(j,n):
    j += n
    return j
i =inc(i,10)
print(i)

在本例中,i的初值为100,当使用表达式“i=inc(i,10)”调用函数ine(i,10)后,在函数体内执行了“i+= 10”语句,函数体内的i变成了110,并 且函数返回了110。当函数调用完毕返回主程序时i被赋值为110.

3.4传递可变对象的引用

在调用函数时,如果传递的是可变对象(例如list对象)的引用,则在函数体中可以直接修改对象的值。

[例9] 定义一个可以交换给定列表中两个指定下标的元素值的函数。

def exchange(a,i,j):
    temp=a[i]
    a[i]=a[j]
    a[j]=temp

[例10] 随机混排给定列表的元素值。

def shuffle(a):
    n=len(a)                                     #获取列表a的长度n
    for i in range(n) :                          #从0到n-1进行循环迭代
        r = random.randrange(i, n)               #取[i,n)的随机整数
        exchange(a, i, r)                        # 交换列表a中下标分别为i和r的元素的值

3.5可选参数

在声明函数时,如果希望函数的一些参数是可选的,可以在声明函数时为这些参数指定默认值。在调用该函数时,如果没有传人对应的实参值,则的数使用声明时指定的默认参数值。
注意:
必须先声明没有默认值的形参,然后再声明有默认值的形参。这是因为在函数调用时默认是按位置传递实际参数值的。

[例11]基于期中成绩和期末成绩,按照指定的权重计算总评成绩。

def my_sum1(mid_score, end_score, mid_rate = 0.4):  #期中成绩、期末成绩、期中成绩权重
    #基于期中成绩、期末成绩和权重计算总评成绩
    score = mid_score * mid_rate + end_score * (1 - mid_rate) 
    print(format(score, '.2f'))  #输出总评成绩,保留2位小数
my_sum1(88, 79)        #期中成绩权重为默认的40%
my_sum1(88, 79, 0.5)     #期中成绩权重设置为50%

在这里插入图片描述

3.6位置参数和命名参数

在函数调用时,实参默认按位置顺序传递形参。按位置传递的参数称为位置参数。

在函数调用时,也可以通过名称(关键字)指定传人的参数,例如my_ max1(a=1, b=2)或者my_ max1(b=2, a=1)。

按名称指定传人的参数称为命名参数,也称为关键字参数。使用关键字参数具有3个优点参数按名称意义明确;传递的参数与顺序无关:如果有多个可选参数,则可以选择指定某个参数值。

在带星号的参数后面声明的参数强制为命名参数,如果这些参数没有默认值,且调用时必须使用命名参数赋值,则会引发错误。

如果不需要带星号的参数,只需要强制命名参数,则可以简单地使用一个星号,例加

def total(initial=5, *,vegetables)

[例12]基于期中成绩和期末成绩,按照指定的权重计算总评成绩。本例中所使用的3种调用方式等价。

def my_sum2(mid_score, end_score, mid_rate = 0.4): #期中成绩、期末成绩、期中成绩权重
    #基于期中成绩、期末成绩和权重计算总评成绩
    score = mid_score * mid_rate + end_score * (1 - mid_rate)
    print(format(score, '.2f'))  #输出总评成绩,保留2位小数
#期中88,期末79,并且期中成绩权重为默认的40%。三种调用方式等价
my_sum2(88, 79)     
my_sum2(mid_score = 88, end_score = 79)
my_sum2(end_score = 79, mid_score = 88)

在这里插入图片描述

3.7可变参数

在声明函数时,可以通过带星的参数(例如* paraml)向函数传递可变数量的实参。在调用函数时,从那一点后所有的参数被收集为一个元组。
在声明函数时,也可以通过带双星的参数(例如**param2)向函数传递可变数量的实参。
在调用函数时,从那一点后所有的参数被收集为一个字典。
带星或带双星的参数必须位于形参列表的最后位置。

[例13] 利用带星的参数计算各数字的累加和。

def my_sum3(a, b, *c):          #各数字累加和
    total = a + b
    for n in c:
       total = total + n
    return total
print(my_sum3(1, 2))            #计算1+2
print(my_sum3(1, 2, 3, 4, 5))      #计算1+2+3+4+5
print(my_sum3(1, 2, 3, 4, 5, 6, 7))  #计算1+2+3+4+5+6+7

在这里插入图片描述
[例14] 利用带星和带双星的参数计算各数字的累加和。

def my_sum4(a, b, *c, **d):  #各数字累加和
    total = a + b
    for n in c:             #元组中各元素累加和
       total = total + n
    for key in d:           #字典中各元素累加和
        total = total + d[key]
    return total
print(my_sum4(1, 2))                        #计算1+2
print(my_sum4(1, 2, 3, 4, 5))                  #计算1+2+3+4+5
print(my_sum4(1, 2, 3, 4, 5, male = 6, female = 7)) #计算1+2+3+4+5+6+7

在这里插入图片描述

3.8 强制命名参数

在带星号的参数后面声明参数会导致强制命名参数(Keyword- only).在调用时必须显式使用命名参数传递值,因为按位置传递的参数默认收集为一个元组,传递给前面带星号的可变参数。

如果不需要带星号的可变参数,只想使用强制命名参数,可以简单地使用一个星号, 例如

def my_ func(* ,a,b, c)

[例15] 基于期中成绩和期末成绩,按照指定的权重计算总评成绩。

def my_sum(*, mid_score, end_score, mid_rate = 0.4): #期中成绩、期末成绩、期中成绩权重
    #基于期中成绩、期末成绩和权重计算总评成绩
    score = mid_score * mid_rate + end_score * (1 - mid_rate)
    print(format(score, '.2f'))  #输出总评成绩,保留2位小数
my_sum(mid_score = 88, end_score = 79) #期中88,期末79,期中权重为默认的40%
my_sum(end_score = 79, mid_score = 88) #期末79,期中88,期中权重为默认的40%
my_sum(88, 79)                     #报错,必须显式使用命名参数传递值

在这里插入图片描述

3.9参数类型检查

通常,函数在定义时既要指定定义域也要指定值城,即指定形式参数和返回值的类型。

基于Python语言的设计理念,在定义函数时不用限定其参数和返回值的类型。这种灵活性可以实现多态性,即允许函数适用于不同类型的对象,例如my _average(a,b)函数,既可以返回两个int对象的平均值,也可以返回两个float 对象的平均值。

当使用不 支持的类型参数调用函数时会产生错误。例如,my_avreg(a,b) 函数传递的多数为str对象,Python在运行时将抛出错误TypeError.
原则上可以增加代码检测这种类型错误,但Python程序设计遵循一种惯例即用户调用所数时必须理解并保证传人正确类型的参数值。

4、函数的返回值

4.1return语句和函数返回值

在函数体中使用return语句可以实现从函数中返回一个值并跳出函数的功能.

[例16] 编写函数,利用return语句返回函数值
求若干数中的最大值。
求若干数中最大值的方法一般如下。
(1)将最大值的初值设为一个比较小的数,或者取第-一个数为最大值的初值。
(2)利用循环将每个数与最大值比较,若此数大于最大值,则将此数设置为最大值。

def my_max(a, b, *c): #求若干数中的最大值
    max_value = a  #假设第一个数为最大值
    if max_value < b: #如果最大值小于b,则b为最大值
        max_value = b
    for n in c:      #循环迭代c中每个元素n,如果最大值小于n,则n为最大值
        if max_value < n:
            max_value = n
    return max_value  #利用return语句返回最大值
#测试代码
print(my_max(1, 2))         #求(1, 2)中的最大值
print(my_max(1, 7, 11, 2, 5))  #求(1, 7, 11, 2, 5)中的最大值

程序运行结果如下。

在这里插入图片描述

4.2多条return语句

reurn语向可以放置在函数中的任何位置,当执行到第一个retum语句时程序返回到调用程序。

[例17]判断素数示例(prime py):先编写判断一个数是否为素数的函数,然后编写测试代码,判断并输出1~99中的素数。
所谓素数(或称质数),是指除了1和该数本身,不能被任何整数整除的正整数。判断一个正整数,是否为素数,只要判断n可否被2~n中的任何个整数整除, 如果。不能被此范围中的任何一个整数整除,n即为素数,否则n为合数。

def is_prime(n):
    if n < 2: return False  #如果n小于2,返回False
    i = 2
    while i*i <= n:
        #一旦n能够被2~i 中的任意整数整除,n就不是素数,返回False
        if n % i == 0: return False 
        i += 1
    return True
#测试代码
for i in range(100):  #判断并输出1~99中的素数,以空格分隔
    if is_prime(i):print(i, end=' ')

在这里插入图片描述

4.3 返回多个值

在函数体中使用return语句可实现从函数返回一个值并跳出函数。如果需要返回多个值,则可以返回一个元组。

[例18]编写函数 ,返回一个随机列表。先编写一个函数,生成由n个随机整数构成的列表,然后编写测试代码,生成并输出由5个随机整数构成的列表的各元素值。

import random
def randomarray(n): #生成由n个随机数构成的列表
    a = []
    for i in range(n):
        a.append(random.random())
    return a
#测试代码
b=randomarray(5)     #生成由5个随机数构成的列表
for i in b: print(i) #输出列表中每个元素

在这里插入图片描述

5、变量的作用域

变量声明的位置不同,其可以被访问的范围也不同。变量的可被访问范围称为变量的作用域。变量按其作用域大致可以分为全局变量、局部变量和类成员变量。

5.1全局变量

在一个源代码文件中,在函数和类定义之外声明的变量称为全局变量。全局变量的作用域为其定义的模块,从定义的位置起,直到文件结束位置。

通过import语句导人模块,也可以通过全限定名称‘模块名.变量名”访问;或者通过from-import语句导人模块中的变量并访问。

不同的模块都可以访问全局变量,这会导致全局变量的不可预知性。如果多个语句同时修改一个全局变量,则可能导致程序产生错误,且很难发现和更正。

全局变量降低了丽数或模块之间的通用性,也降低了代码的可读性。在一般情况下 ,应该尽量避免使用全局变量。全局变量-般作为常量使用。

[例19]全局变量定义示例。

TAX1 = 0.17   #税率常量17%
TAX2 = 0.2    #税率常量20%
TAX3 = 0.05   #税率常量5%
PI = 3.14      #圆周率3.14

[例20]全局变量定义示例。

TAX1 = 0.17   #税率常量17%
TAX2 = 0.2    #税率常量20%
TAX3 = 0.05   #税率常量5%
PI = 3.14      #圆周率3.14
def tax(x):            #根据税率常量20%计算纳税值
    return x * TAX2
#测试代码
a = [1000, 1200, 1500, 2000]
for i in a:           #计算并打印4笔数据的纳税值
    print(i, tax(i))

在这里插入图片描述

5.2 局部变量

在函数体中声明的变量(包括的函数参数)称为局部变量,其有效范围(作用域)为函数体。

全局代码不能引用一个函数的局部变量或形式参数变量;一个函数也不能引用在另一个函数中定义的局部变量或形式参数变量。

如果在一个函数中定义的局部变量(或形式参数变量)与全局变量重名,则局部变量(或形式参数变量)优先,即函数中定义的变量是指局部变量(或形式参数变量),而不是全局变量。

[例21]局部变量定义示例。

num = 100      #全局变量
def f():
  num = 105    #局部变量
  print(num)    #输出局部变量的值
#测试代码
f();print(num)

在这里插入图片描述

5.3全 局语句global

在函数体中可以引用全局变量,但如果函数内部的变量名是第一次出现且在赋值语句之前(变量赋值),则解释为定义局部变量。

[例22]函数体错误引用全局变量的示例。

m = 100
n = 200
def f():
    print(m+5)   
    n += 10  
f()

如果要为定义在函数外的全局变量赋值,可以使用global语句,表明变量是在外面定义的全局变量。global 语句可以指定多个全局变量,例如“global x, y, z”。一 般应该尽量避免这样使用全局变量,全局变量会导致程序的可读性差。

[例23]全局语句global

pi = 3.141592653589793 #全局变量
e = 2.718281828459045 #全局变量
def my_func():
    global pi #全局变量,与前面的全局变量pi指向相同的对象
    pi = 3.14 #改变了全局变量的值
    print('global pi =', pi) #输出全局变量的值
    e = 2.718 #局部变量,与前面的全局变量e指向不同的对象
    print('local e =', e)  #输出局部变量的值
#测试代码
print('module pi =', pi)   #输出全局变量的值
print('module e =', e)   #输出全局变量的值
my_func()           #调用函数
print('module pi =', pi)  #输出全局变量的值,该值在函数中已被更改
print('module e =', e)   #输出全局变量的值

在这里插入图片描述

5.4非局部语句nonlocal

在函数体中可以定义嵌套函数,在嵌套函数中如果要为定义在上级函数体的局部变量赋值,可以使用nonlocal语句,表明变量不是所在块的局部变量而是在上级函数体中定义的局部变量,nonlocal语句可以指定多个非局部变量。例如“nonlocalx,y,z".

[例24]非局部语句

def outer_func():
    tax_rate = 0.17  #上级函数体中的局部变量
    print('outer func tax rate =', tax_rate) #输出上级函数体中局部变量的值
    def innner_func():
        nonlocal tax_rate #不是所在块的局部变量,而是在上级函数体中定义的局部变量
        tax_rate = 0.05  #上级函数体中的局部变量重新赋值
        print('inner func tax rate =', tax_rate) #输出上级函数体中局部变量的值
    innner_func()                       #调用函数
    print('outer func tax rate =', tax_rate)   #输出上级函数体中局部变量的值(已更改)
#测试代码
outer_func()

在这里插入图片描述

5.5 类成员变量

类成员变量是在类中声明的变量,包括静态变量和实例变量,其有效范围(作用域)为类定义体内。
在外部,通过创建类的对象实例,然后通过“对象.实例变量”访问类的实例变量,或者通过“类.静态变量”访问类的静态变量。

5.6 输出局部变量和全局变量

在程序运行过程中,在上下文中会生成各种局部变量和全局变量,使用内置函数global和locals()可以查看并输出局部变量和全局变量列表。

[例25]局部变量和全局变量列表示例(locals_ globals. py)。

a=1
b=2
def f(a, b):  
    x = 'abc'
    y = 'xyz'  
    for i in range(2):  #i=0~1
        j = i  
        k = i**2
        print(locals())
f(1,2)
print(globals())

在这里插入图片描述

6、递归函数

6.1递归函 数的定义

递归函数即自调用函数,在函数体内部直接或间接地自己调用自己,即函数的嵌套调用是函数本身。递归函数常用来实现数值计算的方法。

[例26]使 用递归函数实现阶乘

def factorial(n):
    if n == 1: return 1
    return n * factorial(n - 1)
#测试代码
for i in range(1,10): #输出1~9的阶乘
    print(i,'! =', factorial(i))

在这里插入图片描述

6.2递归函数的原理

递归提供了建立数学模型的一种直接方法,与数学上的数学归纳法相对应。
每个递归函数必须包括以下两个主要部分。
(1)终止条件:表示递归的结束条件,用于返回函数值,不再递归调用。例如,factorial()函数的结束条件为“n等于1”。
(2)递归步骤:递归步骤把第n步的参数值的函数与第n-1步的参数值的函数关联。
例如,对于factorial(),其递归步骤为“n * factorial(n- 1)”。
另外,一序列的参数值必须逐渐收敛到结束条件。例如,对于factorial(),每次递归调用参数值n均递减1,所以一序列参数值逐渐收敛到结束条件(n=1)。
例如,调和数的计算公式如下。
H。=1十1/2+…+1/n
故可以使用递归函数实现。
(1)终止条件: H。= 1
当n==l时
(2)递归步骤: H。= H。-1+ 1/n
当n>1时
每次递归,n严格递减,故逐渐收敛于1。
[例27]使 用递归函数实现调和数 。

def harmonic(n):
    if n == 1: return 1.0          #终止条件
    return harmonic(n-1) + 1.0/n   #递归步骤
#测试代码
for i in range(1,10): #输出1~9阶的调和数
    print('H', i, ' =', harmonic(i))

在这里插入图片描述

6.3编写递归函数时需要注意的问题

虽然使用递归函数可以实现简洁、优雅的程序,但在编写递归雨数时应该注意如下几个问题。
(1)必须设置终止条件。
缺少终止条件的递归函数将导致无限递归函数调用,其最终结果是系统会耗尽内存,此时Python会抛出错误RuntimeError, 并报告错误信息“maximum recursion depth exceeded(超过最大递归深度)”。
在递归函数中一般需要设置终止条件。在sys模块中,函数getrecursionlimit()和setrecursionlimit()用于获取和设置最大递归次数。例如:

import sys
sys. getrecursionlimit()#获取最大递归次数:1000
sys. setrecursionl imit(2000)#设置最大递归次数为2000

(2)必须保证收敛。
递归调用所解决子问题的规模必须小于原始问题的规模,否则会导致无限递归函数调用

(3)必须保证内存和运算消耗控制在定范围内。
递归函数代码虽然看起来简单,但往往会导致过量的递归函数调用,从而消耗过量的内存(导致内存溢出),或过量的运算能力(运行时间过长)。

6.4 递归函数的应用:最大公约数

用于计算最大公约数问题的递归方法称为欧几里得算法,其描述如下:
如果p>q,则p和q的最大公约数等于q和p % q的最大公约数。
故可以使用递归函数实现,步骤如下。
(1)终止条件: gcd(p,q) = p #当q==0时
(2)递归步骤: gcd(q, p%q) #当q>1时
每次递归,p%q严格递减,故逐渐收敛于0。

[例28]使用递归函数计算最大公约数。

import sys
def gcd(p, q):  #使用递归函数计算p和q的最大公约数
    if q == 0: return p    #如果q=0,返回p
    return gcd(q, p % q)  #否则,递归调用gcd(q, p % q)
#测试代码
p = int(sys.argv[1])  #p=命令行第一个参数
q = int(sys.argv[2])  #q=命令行第二个参数
print(gcd(p, q))     #计算并输出p和q的最大公约数

在这里插入图片描述

6.5 递归函数的应用:汉诺塔

汉诺塔(Towers of Hanoi, 又称河内塔)源自于印度的古老传说:大梵天创造世界的时候,在世界中心贝拿勒斯的圣庙里做了3根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘,称之为汉诺塔。

大梵天命令婆罗门把圆盘从一根柱子上按大小顺序重新摆放到另一根柱子上,并且规定在3根柱子之间一次只能移动个圆盘,且小圆盘上不能放置大圆盘。这个游戏称为汉诺塔益智游戏。

汉诺塔益智游戏问题很容易使用递归函数实现。假设柱子的编号为a、b、c,定义函数hanoi(n, a, b, c)表示把n个圆盘从柱子a移到柱子c(可以经由柱子b),则有:
(1)终止条件。当n==1时,hanoi(n, a, b, c)为终止条件。即如果柱子a上只有一个圆盘,则可以直接将其移动到柱子c上。

(2)递归步骤。hanoi(n,a, b, c)可以分解为3个步骤,即hanoi(n-1,a,c,b)、hanoi(1,a,b,c)和hanoi(n-1,b,a.c)。 如果柱子a上有n个圆盘,可以看成柱子a上有一个圆盘(底盘)和(n- 1)个圆盘,首先需要把柱子a上面的(n-1)个圆盘移动到柱子b.即调用hanoi(n-1,a,c,b); 然后把柱子a上的最后一个圆盘移动到柱子c,即调用hanoi(1,a,b.c);再将柱子b上的(n- 1)个圆盘移动到柱子c,即调用hanoi(n-1,b,a,c)。

每次递归,n严格递减,故逐渐收敛于1.

[例29]使用递归 函数实现汉诺塔问题。

#将n个从小到大依次排列的圆盘从柱子a移动到柱子c上,柱子b作为中间缓冲
def hanoi(n,a,b,c):
    if n==1: print(a,'->',c) #只有一个圆盘,直接将圆盘从柱子a移动到柱子c上
    else:
        hanoi(n-1,a,c,b) #先将n-1个圆盘从柱子a移动到柱子b上(采用递归方式)
        hanoi(1,a,b,c)  #然后将最大的圆盘从柱子a移动到柱子c上
        hanoi(n-1,b,a,c) #再将n-1个圆盘从柱子b移动到柱子c上(采用递归方式)
#测试代码
hanoi(4,'A','B','C')

在这里插入图片描述

7、内置函数的使用

在python语言中提供了若干内置函数,用于实现常用的功能,可以直接使用。

7.1 内置函数一览表

内置函数 内置函数 内置函数 内置函数 内置函数
abs() delattr() hash() memoryview() set()
all() dict() help() min() setattr()
any() dir() hex() next() slicea()
ascii() divmod() id() object() sorted()
bin() enumerate() input() oct() staticmethod()
bool() eval() int() open() str()
breakpoint() exec() isinstance() ord() sum()
bytearray() filter() issubclass() pow() super()
bytes() float() iter() print() tuple()
callable() format() len() property() type()
chr() frozenset() list() range() vars()
classmethod() getattr() locals() repr() zip()
compile() globals() map() reversed() import()
complex() hasattr() max() round()

各个内置函数的具体功能和用法,可通过访问 https://docs.python.org/zh-cn/3/library/functions.html 进行查看。

需要注意的是,开发者不建议使用以上内置函数的名字作为标识符使用(作为某个变量、函数、类、模板或其他对象的名称),虽然这样做 Python 解释器不会报错,但这会导致同名的内置函数被覆盖,从而无法使用。

8、Python函数式编程基础

Python是面向对象的程序设计语言,也是面向过程的程序语言,同时也支持函数式编程。
Pyhon标准库functools 提供了若干关于函数的函数,提供了Haskell和Standard ML中的函数式程序设计工具。

8.1作为对象的函数

在Python语言中函数也是对象,故函数对象可以赋值给变量。
[例30]作为对象的函数。

f=abs
type(f)        #输出:<class 'builtin function or_ method'>
f(-123)       #返回绝对值输出:123

8.2 高阶函数

函数对象也可以作为参数传递给函数,还可以作为函数的返回值。参数为函数对象的函数或返回函数对象的函数称为高阶函数,即函数的函数。
[例31]高阶函数。

def compute(f, s):    #f为函数对象,s为系列对象
    return f(s)
compute(min,(1,5,3,2))

8.3map()函数

map() 函数的基本语法格式如下:

map(function, iterable)

其中,function 参数表示要传入一个函数,其可以是内置函数、自定义函数或者 lambda 匿名函数;iterable 表示一个或多个可迭代对象,可以是列表、字符串等。

map() 函数的功能是对可迭代对象中的每个元素,都调用指定的函数,并返回一个 map 对象。
注意,该函数返回的是一个 map 对象,不能直接输出,可以通过 for 循环或者 list() 函数来显示。

【例32】还是对列表中的每个元素乘以 2。

listDemo = [1, 2, 3, 4, 5]
new_list = map(lambda x: x * 2, listDemo)
print(list(new_list))

运行结果为:

[2, 4, 6, 8, 10]

【例33】map() 函数可传入多个可迭代对象作为参数。

listDemo1 = [1, 2, 3, 4, 5]
listDemo2 = [3, 4, 5, 6, 7]
new_list = map(lambda x,y: x + y, listDemo1,listDemo2)
print(list(new_list))

运行结果为:

[4, 6, 8, 10, 12]

注意,由于 map() 函数是直接由用 C 语言写的,运行时不需要通过 Python 解释器间接调用,并且内部做了诸多优化,所以相比其他方法,此方法的运行效率最高。

8.4filter()函数

filter()函数的基本语法格式如下:

filter(function, iterable)

此格式中,funcition 参数表示要传入一个函数,iterable 表示一个可迭代对象。

filter() 函数的功能是对 iterable 中的每个元素,都使用 function 函数判断,并返回 True 或者 False,最后将返回 True 的元素组成一个新的可遍历的集合。

【例34】返回一个列表中的所有偶数。

listDemo = [1, 2, 3, 4, 5]
new_list = filter(lambda x: x % 2 == 0, listDemo)
print(list(new_list))

运行结果为:

[2, 4]

【例35】filter() 函数可以接受多个可迭代对象。

listDemo = [1, 2, 3, 4, 5]
new_list = map(lambda x,y: x-y>0,[3,5,6],[1,5,8] )
print(list(new_list))

运行结果为:

[True, False, False]

8.5reduce()函数

reduce() 函数通常用来对一个集合做一些累积操作,其基本语法格式为:

reduce(function, iterable)

其中,function 规定必须是一个包含 2 个参数的函数;iterable 表示可迭代对象。

注意,由于 reduce() 函数在 Python 3.x 中已经被移除,放入了 functools 模块,因此在使用该函数之前,需先导入 functools 模块。

【例36】计算某个列表元素的乘积。

import functools
listDemo = [1, 2, 3, 4, 5]
product = functools.reduce(lambda x, y: x * y, listDemo)
print(product)

运行结果为:

120

小结:
通常来说,当对集合中的元素进行一些操作时,如果操作非常简单,比如相加、累积这种,那么应该优先考虑使用 map()、filter()、reduce() 实现。另外,在数据量非常多的情况下(比如机器学习的应用),一般更倾向于函数式编程的表示,因为效率更高。

当然,在数据量不多的情况下,使用 for 循环等方式也可以。不过,如果要对集合中的元素做一些比较复杂的操作,考虑到代码的可读性,通常会使用 for 循环。

8.6 lambda表达式(匿名函数)及用法

lambda 表达式(又称匿名函数)是现代编程语言争相引入的一种语法,如果说函数是命名的、方便复用的代码块,那么 lambda 表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。
回顾局部函数

def get_math_func(type) :
    # 定义一个计算平方的局部函数
    def square(n) :  # ①
        return n * n
    # 定义一个计算立方的局部函数
    def cube(n) :  # ②
        return n * n * n
    # 定义一个计算阶乘的局部函数
    def factorial(n) :   # ③
        result = 1
        for index in range(2 , n + 1):
            result *= index
        return result
    # 返回局部函数
    if type == "square" :
        return square
    if type == "cube" :
        return cube
    else:
        return factorial
# 调用get_math_func(),程序返回一个嵌套函数
math_func = get_math_func("cube") # 得到cube函数
print(math_func(5)) # 输出125
math_func = get_math_func("square") # 得到square函数
print(math_func(5)) # 输出25
math_func = get_math_func("other") # 得到factorial函数
print(math_func(5)) # 输出120

程序中,定义了一个 get_math_func() 函数,该函数将返回另一个函数。接下来在 get_math_func() 函数体内的 ①、②、③ 号代码分别定义了三个局部函数,最后 get_math_func() 函数会根据所传入的参数,使用这三个局部函数之一作为返回值。
在定义了会返回函数的 get_math_func() 函数之后,接下来程序调用 get_math_func() 函数时即可返回所需的函数

由于局部函数的作用域默认仅停留在其封闭函数之内,因此这三个局部函数的函数名的作用太有限了,即仅仅是在程序的 if 语句中作为返回值使用。一旦离开了 get_math_func() 函数体,这三个局部函数的函数名就失去了意义。

既然局部函数的函数名没有太大的意义,那么就考虑使用 lambda 表达式来简化局部函数的写法。
使用 lambda 表达式代替局部函数
如果使用 lambda 表达式来简化 get_math_func() 函数,则可以将程序改写成如下形式:

def get_math_func(type) :
    result=1
    # 该函数返回的是Lambda表达式
    if type == 'square':
        return lambda n: n * n  # ①
    elif type == 'cube':
        return lambda n: n * n * n  # ②
    else:
        return lambda n: (1 + n) * n / 2 # ③


# 调用get_math_func(),程序返回一个嵌套函数
math_func = get_math_func("cube")
print(math_func(5)) # 输出125
math_func = get_math_func("square")
print(math_func(5)) # 输出25
math_func = get_math_func("other")
print(math_func(5)) # 输出15.0

在上面代码中,return 后面的部分使用 lambda 关键字定义的就是 lambda 表达式,Python 要求 lambda 表达式只能是单行表达式。
注意:由于 lambda 表达式只能是单行表达式,不允许使用更复杂的函数形式,因此上面 ③ 号代码处改为计算 1+2+3+…+n 的总和。

lambda 表达式的语法格式如下:

lambda [parameter_list] : 表达式

从上面的语法格式可以看出 lambda 表达式的几个要点:
lambda 表达式必须使用 lambda 关键字定义。
在 lambda 关键字之后、冒号左边的是参数列表,可以没有参数,也可以有多个参数。如果有多个参数,则需要用逗号隔开,冒号右边是该 lambda 表达式的返回值。

实际上,lambda 表达式的本质就是匿名的、单行函数体的函数。因此,lambda 表达式可以写成函数的形式。

例如,对于如下 lambda 表达式:

lambda x , y:x + y

可改写为如下函数形式:

def add(x, y):
    return x+ y

上面定义函数时使用了简化语法:当函数体只有一行代码时,可以直接把函数体的代码放在与函数头同一行。

总体来说,函数比 lambda 表达式的适应性更强,lambda 表达式只能创建简单的函数对象(它只适合函数体为单行的情形)。但 lambda 表达式依然有如下两个用途:

  • 对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁。
  • 对于不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高了性能。

下面代码示范了通过 lambda 表达式来调用 Python 内置的 map() 函数:

# 传入计算平方的lambda表达式作为参数
x = map(lambda x: x*x , range(8))
print([e for e in x]) # [0, 1, 4, 9, 16, 25, 36, 49]
# 传入计算平方的lambda表达式作为参数
y = map(lambda x: x*x if x % 2 == 0 else 0, range(8))
print([e for e in y]) # [0, 0, 4, 0, 16, 0, 36, 0]

正如从上面代码所看到的,内置的 map() 函数的第一个参数需要传入函数,此处传入了函数的简化形式:lambda 表达式,这样程序更加简洁,而且性能更好。
小结:
lambda 表达式是 Python 编程的核心机制之一。Python 语言既支持面向过程编程,也支持面向对象编程。而 lambda 表达式是 Python 面向过程编程的语法基础,因此必须引起重视。
Python 的 lambda 表达式只是单行函数的简化版本,因此 lambda 表达式的功能比较简单。

8.7@函数装饰器及用法(超级详细)

8.7.1 引入

Python 内置的 3 种函数装饰器,分别是 @staticmethod、@classmethod 和 @property,其中 staticmethod、classmethod 和 property 都是 Python 的内置函数。那么,我们是否可以开发自定义的函数装饰器呢?答案是肯定的。

当程序使用“@函数”(比如函数 A)装饰另一个函数(比如函数 B)时,实际上完成如下两步:

  • 将被修饰的函数(函数 B)作为参数传给 @ 符号引用的函数(函数 A)。
  • 将函数 B 替换(装饰)成第 1 步的返回值。

从上面介绍不难看出,被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西(取决于装饰器的返回值)。其实所谓的装饰器,就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。

为了让大家厘清函数装饰器的作用,下面看一个非常简单的示例:

def funA(fn):
    print('A')
    fn() # 执行传入的fn参数
    return 'fkit'
'''
下面装饰效果相当于:funA(funB),
funB 将会替换(装饰)成 funA() 语句的返回值;
由于funA()函数返回 fkit,因此 funB 就是 fkit
'''
@funA
def funB():
    print('B')
print(funB) # fkit

上面程序使用 @funA 修饰 funB,这意味着程序要完成两步操作:

  • 将 funB 作为 funA() 的参数,也就是上面代码中 @funA 相当于执行 funA(funB)。
  • 将 funB 替换成 funA() 执行的结果,funA() 执行完成后返回 fkit,因此 funB 就不再是函数,而是被替换成一个字符串。

其实,简单地理解函数装饰器的作用,上面程序可以等价地转换成如下程序:

def funA(fn):
    print('A')
    fn() # 执行传入的fn参数
    return 'fkit'
def funB():
    print('B')
funB = funA(funB)
print(funB) # fkit

注意,此程序中的 funB = funA(funB) 就等同于上面程序中 @funA 所起的作用。

运行上面 2 段程序,可以看到相同的输出结果:

A
B
Fkit

通过这个例子,相信读者对函数装饰器的执行关系己经有了一个较为清晰的认识,但读者可能会产生另一个疑问,这个函数装饰器导致被修饰的函数变成了字符串,那么函数装饰器有什么用?

别忘记了,被修饰的函数总是被替换成 @ 符号所引用的函数的返回值,因此被修饰的函数会变成什么,完全由于 @ 符号所引用的函数的返回值决定,换句话说,如果 @ 符号所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。

8.7.2带参数的函数装饰器

如果原函数 funB() 中有参数需要传递给函数装饰器,应该如何实现?

一个简单的办法是,可以在对应的函数装饰器 funA() 上,添加相应的参数,例如:

def foo(fn):
    # 定义一个嵌套函数
    def bar(a):  
        fn(a * (a - 1))
        print("*" * 15)
        return fn(a * (a - 1))
    return bar
'''
下面装饰效果相当于:foo(my_test),
my_test将会替换(装饰)成该语句的返回值;
由于foo()函数返回bar函数,因此my_test就是bar
同时,my_test 的参数 a 对应 bar 函数的参数 a
'''
@foo
def my_test(a):
    print("==my_test函数==", a)
# 打印my_test函数,将看到实际上是bar函数
print(my_test)
# 下面代码看上去是调用my_test(),其实是调用bar()函数
my_test(10)

上面程序定义了一个装饰器函数 foo,该函数执行完成后并不是返回普通值,而是返回 bar 函数(这是关键),这意味着被该 @foo 修饰的 my_test() 函数最终都会被替换成 bar() 函数。

上面程序使用 @foo 修饰 my_test() 函数,因此程序同样会执行 foo(my_test),并将 my_test 替换成 foo() 函数的返回值:bar 函数。所以,上面程序第 18 行代码在打印 my_test 函数时,实际上输出的是 bar 函数,这说明 my_test 已经被替换成 bar 函数。接下来程序调用 my_test() 函数,实际上就是调用 bar() 函数。

运行上面程序,可以看到如下输出结果:

<function foo..bar at 0x0000012D7E246598>
== my_test函数== 90


== my_test函数== 90

在此基础上,还有一个问题,如果程序中另外还有一个函数,也需要使用 funA 装饰器,但是这个新的函数有 2 个参数,此时又该怎么办呢?例如:

@foo
def new_test(a,b):
....

在这种情况下,最简单的解决方式是用 *args 和 **kwargs 作为 foo 函数装饰器内部函数 bar() 的参数,*args 和 **kwargs 表示接受任意数量和类型的参数,因此函数装饰器可以写成下面的形式:

def foo(fn):
    # 定义一个嵌套函数
    def bar(*args,**kwargs):
        fn(*args,**kwargs)
    return bar
@foo
def my_test(a):
    print("==my_test函数==", a)
@foo
def new_test(a,b):
    print("==new_test函数==",a," ",b)
my_test(10)
new_test(6, 5)

运行结果为:

== my_test函数== 10
== new_test函数== 6 5

8.7.3带自定义参数的函数装饰器

其实,函数装饰器还有更大程度的灵活性。刚刚说了,装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。

举个例子,比如要定义一个参数,来表示装饰器内部函数被执行的次数,那么就可以写成下面这种形式:

def foo(num):
    def my_decorator(fn):
        def bar(*args,**kwargs):
            for i in range(num):
                fn(*args,**kwargs)
        return bar
    return my_decorator
@foo(3)
def my_test(a):
    print("==my_test函数==", a)
@foo(5)
def new_test(a,b):
    print("==new_test函数==",a," ",b)
my_test(10)
new_test(6, 5)

运行结果为:

== my_test函数== 10
== my_test函数== 10
== my_test函数== 10
== new_test函数== 6 5
== new_test函数== 6 5
== new_test函数== 6 5
== new_test函数== 6 5
== new_test函数== 6 5

8.7.4函数装饰器也可以嵌套

上面示例中,都是使用一个装饰器的情况,但实际上,Python 也支持多个装饰器,比如:

@decorator1
@decorator2
@decorator3
def func():
    ...

上面程序的执行顺序是里到外,所以它等效于下面这行代码:

decorator1( decorator2( decorator3(func) ) )

待续······

发布了261 篇原创文章 · 获赞 249 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_44721831/article/details/102883028