Python基础知识——函数的基本使用、函数的参数、名称空间与作用域、函数对象与闭包、 装饰器、迭代器、生成器与yield、函数递归、面向过程与函数式(map、reduce、filter)

1 函数的基本使用

一 引入

​ 基于前一部分的学习,我们已经能开发一些功能简单的小程序了,但随着程序功能的增多,代码量随之增大,此时仍不加区分地把所有功能的实现代码放到一起,将会使得程序的组织结构不清晰,可读性变差,且程序中需要频繁使用同一功能时,只能重复编写该功能的实现代码,日积月累,程序将变得冗长,并且当某一功能需要修改时,又不得不找出所有定义及使用这段功能的地方修改之,管理维护的难度极大,好吧,装了半天逼,到底该如何解决提出的这些问题呢?

我们完全可以从现实生活中找到简化程序设计的方案:比如一个修理工会事先准备好螺丝刀、锤子等工具,这样在进行修理的过程中,需要用到拧螺丝的功能时就直接拿来螺丝刀使用,需要用到锤击物体的功能时就直接拿来锤子使用,而无需临时制造。这个例子的核心在于’事先准备好工具’,遇到应用场景时’拿来就用’,。
在程序中,具备某一功能的‘工具’指的就是函数,‘事先准备工具’的过程即函数的定义,‘拿来就用’即函数的调用。

二 定义函数

函数的使用必须遵循’先定义,后调用’的原则。函数的定义就相当于事先将函数体代码保存起来,然后将内存地址赋值给函数名,函数名就是对这段代码的引用,这和变量的定义是相似的。没有事先定义函数而直接调用,就相当于在引用一个不存在的’变量名’。
定义函数的语法

def 函数名(参数1,参数2,...):
	"""文档描述"""
	函数体
	return
  1. def: 定义函数的关键字;
  2. 函数名:函数名指向函数内存地址,是对函数体代码的引用。函数的命名应该反映出函数的功能;
  3. 括号:括号内定义参数,参数是可有可无的,且无需指定参数的类型;
  4. 冒号:括号后要加冒号,然后在下一行开始缩进编写函数体的代码;
  5. “”“文档描述”“”: 描述函数功能,参数介绍等信息的文档,非必要,但是建议加上,从而增强函数的可读性;
  6. 函数体:由语句和表达式组成;
  7. return 值:定义函数的返回值,return是可有可无的。

参数是函数的调用者向函数体传值的媒介,若函数体代码逻辑依赖外部传来的参数时则需要定义为参函数,

def my_min(x,y):
    res=x if x < y else y
    return res

否则定义为无参函数

def interactive():
    user=input('user>>: ').strip()
    pwd=input('password>>: ').strip()
    return (user,pwd)

​ 函数体为pass代表什么都不做,称之为空函数。定义空函数通常是有用的,因为在程序设计的开始,往往是先想好程序都需要完成什么功能,然后把所有功能都列举出来用pass充当函数体“占位符”,这将使得程序的体系结构立见,清晰且可读性强。例如要编写一个ftp程序,我们可能想到的功能有用户认证,下载,上传,浏览,切换目录等功能,可以先做出如下定义:

def auth_user():
    """user authentication function"""
    pass

def download_file():
    """download file function"""
    pass

def upload_file():
    """upload file function"""
    pass

def ls():
    """list contents function"""
    pass

def cd():
    """change directory"""
    pass

之后我们便可以统筹安排编程任务,有选择性的去实现上述功能来替换掉pass,从而提高开发效率。

三 调用函数与函数返回值

​ 函数的使用分为定义阶段与调用阶段,定义函数时只检测语法,不执行函数体代码,函数名加括号即函数调用,只有调用函数时才会执行函数体代码

#定义阶段
def foo():
    print('in the foo')
    bar()

def bar():
    print('in the bar')

#调用阶段
foo()

执行结果:

in the foo
in the bar

定义阶段函数foo与bar均无语法错误,而在调用阶段调用foo()时,函数foo与bar都早已经存在于内存中了,所以不会有任何问题。

按照在程序出现的形式和位置,可将函数的调用形式分为三种:

#1、语句形式:
foo()

#2、表达式形式:
m=my_min(1,2) #将调用函数的返回值赋值给x
n=10*my_min(1,2) #将调用函数的返回值乘以10的结果赋值给n

#3、函数调用作为参数的形式:
## my_min(2,3)作为函数my_min的第二个参数,实现了取1,2,3中的较小者赋值给m
m=my_min(1,my_min(23)

若需要将函数体代码执行的结果返回给调用者,则需要用到return。return后无值或直接省略return,则默认返回None,return的返回值无类型限制,且可以将多个返回值放到一个元组内。

>>> def test(x,y,z):
...     return x,y,z #等同于return (x,y,z)
... 
>>> res=test(1,2,3)
>>> print(res)
(1, 2, 3)

return是一个函数结束的标志,函数内可以有多个return,但只执行一次函数就结束了,并把return后定义的值作为本次调用的结果返回。

2 函数的参数

一 形参与实参介绍

函数的参数分为形式参数和实际参数,简称形参和实参:
形参即在定义函数时,括号内声明的参数。形参本质就是一个变量名,用来接收外部传来的值。
实参即在调用函数时,括号内传入的值,值可以是常量、变量、表达式或三者的组合:

#1:实参是常量
res=my_min(1,2)

#2:实参是变量
a=1
b=2
res=my_min(a,b)

#3:实参是表达式
res=my_min(10*2,10*my_min(3,4))

#4:实参可以是常量、变量、表达式的任意组合
a=2
my_min(1,a,10*my_min(3,4))

在调用有参函数时,实参(值)会赋值给形参(变量名)。在Python中,变量名与值只是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除。

二 形参与实参的具体使用

2.1 位置参数

位置即顺序,位置参数指的是按顺序定义的参数,需要从两个角度去看:

  1. 在定义函数时,按照从左到右的顺序依次定义形参,称为位置形参,凡是按照这种形式定义的形参都必须被传值
>>> def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
...     print('Name:%s Age:%s Sex:%s' %(name,age,sex))
... 
>>> register() #TypeError:缺少3个位置参数  
  1. 在调用函数时,按照从左到右的顺序依次定义实参,称为位置实参,凡是按照这种形式定义的实参会按照从左到右的顺序与形参一一对应
>>> register('lili',18,'male') #对应关系为:name=’lili’,age=18,sex=’male’
Name:lili Age:18 Sex:male  

2.2 关键字参数

在调用函数时,实参可以是key=value的形式,称为关键字参数,凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值

>>> register(sex='male',name='lili',age=18)
Name:lili Age:18 Sex:male

需要注意在调用函数时,实参也可以是按位置或按关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值

>>> register('lili',sex='male',age=18) #正确使用
>>> register(name='lili',18,sex='male') #SyntaxError:关键字参数name=‘lili’在位置参数18之前
>>> register('lili',sex='male',age=18,name='jack') #TypeError:形参name被重复赋值

2.3 默认参数

在定义函数时,就已经为形参赋值,这类形参称之为默认参数,当函数有多个参数时,需要将值经常改变的参数定义成位置参数,而将值改变较少的参数定义成默认参数。例如编写一个注册学生信息的函数,如果大多数学生的性别都为男,那完全可以将形参sex定义成默认参数

>>> def register(name,age,sex='male'): #默认sex的值为male
...     print('Name:%s Age:%s Sex:%s' %(name,age,sex))
...

定义时就已经为参数sex赋值,意味着调用时可以不对sex赋值,这降低了函数调用的复杂度

>>> register('tom',17) #大多数情况,无需为sex传值,默认为male
Name:tom Age:17 Sex:male
>>> register('Lili',18,'female') #少数情况,可以为sex传值female
Name:Lili Age:18 Sex:female

需要注意:

  1. 默认参数必须在位置参数之后
  2. 默认参数的值仅在函数定义阶段被赋值一次
>>> x=1
>>> def foo(arg=x):
...     print(arg)
... 
>>> x=5 #定义阶段arg已被赋值为1,此处的修改与默认参数arg无任何关系
>>> foo()
1


1. 默认参数的值通常应设为不可变类型
>>> def foo(n,arg=[]):
...     arg.append(n)
...     return arg
... 
>>> foo(1)
[1]
>>> foo(2)
[1, 2]
>>> foo(3)
[1, 2, 3]

每次调用是在上一次的基础上向同一列表增加值,修改如下
>>> def foo(n,arg=None):
...     if arg is None:
...         arg=[]
...     arg.append(n)
...     return arg
... 
>>> foo(1)
[1]
>>> foo(2)
[2]
>>> foo(3)
[3]  

2.4 可变长度的参数(*与**的用法)

参数的长度可变指的是在调用函数时,实参的个数可以不固定,而在调用函数时,实参的定义无非是按位置或者按关键字两种形式,这就要求形参提供两种解决方案来分别处理两种形式的可变长度的参数

2.4.1 可变长度的位置参数

如果在最后一个形参名前加号,那么在调用函数时,溢出的位置实参,都会被接收,以元组的形式保存下来赋值给该形参

>>> def foo(x,y,z=1,*args): #在最后一个形参名args前加*号
...     print(x)
...     print(y)
...     print(z)
...     print(args)
... 
>>> foo(1,2,3,4,5,6,7)  #实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,即args=(4, 5, 6,7)

1
2
3
(4, 5, 6, 7)

如果我们事先生成了一个列表,仍然是可以传值给*args的

>>> def foo(x,y,*args):
...     print(x)
...     print(y)
...     print(args)
... 
>>> L=[3,4,5]
>>> foo(1,2,*L) ## *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)
1
2
(3, 4, 5)

注意:如果在传入L时没有加*,那L就只是一个普通的位置参数了

>>> foo(1,2,L) #仅多出一个位置实参L
1
2
([1, 2, 3],)

如果形参为常规的参数(位置或默认),实参仍可以是*的形式

>>> def foo(x,y,z=3):
...     print(x)
...     print(y)
...     print(z)
... 
>>> foo(*[1,2]) #等同于foo(1,2)
1
2
3

如果我们想要求多个值的和,*args就派上用场了

>>> def add(*args):
...     res=0
...     for i in args:
...         res+=i
...     return res
... 
>>> add(1,2,3,4,5)
15

2.4.2 可变长度的关键字参数

如果在最后一个形参名前加号,那么在调用函数时,溢出的关键字参数,都会被接收,以字典的形式保存下来赋值给该形参

>>> def foo(x,**kwargs): #在最后一个参数kwargs前加**
...     print(x)        
...     print(kwargs)   
... 
>>> foo(y=2,x=1,z=3) #溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
1
{'z': 3, 'y': 2}

如果我们事先生成了一个字典,仍然是可以传值给**kwargs的

>>> def foo(x,y,**kwargs):
...     print(x)
...     print(y)
...     print(kwargs)
... 
>>> dic={'a':1,'b':2} 
>>> foo(1,2,**dic) #**dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)
1
2
{'a': 1, 'b': 2}

注意:如果在传入dic时没有加**,那dic就只是一个普通的位置参数了

>>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个

如果形参为常规参数(位置或默认),实参仍可以是**的形式

>>> def foo(x,y,z=3):
...     print(x)
...     print(y)
...     print(z)
... 
>>> foo(**{'x':1,'y':2}) #等同于foo(y=2,x=1)
1
2
3

如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了,可以使用**kwargs为日后的扩展供良好的环境,同时保持了函数的简洁性。

>>> def auth(user,password,**kwargs): 
...     pass 
...

2.5 命名关键字参数

在定义了**kwargs参数后,函数调用者就可以传入任意的关键字参数key=value,如果函数体代码的执行需要依赖某个key,必须在函数内进行判断

>>> def register(name,age,**kwargs):
...     if 'sex' in kwargs:
...         #有sex参数
...         pass
...     if 'height' in kwargs:
...         #有height参数
...         pass
...

想要限定函数的调用者必须以key=value的形式传值,Python3提供了专门的语法:需要在定义形参时,用*作为一个分隔符号,*号之后的形参称为命名关键字参数。对于这类参数,在函数调用时,必须按照key=value的形式为其传值,且必须被传值

>>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
...     pass
... 
>>> register('lili',18,sex='male',height='1.8m') #正确使用
>>> register('lili',18,'male','1.8m') ## TypeError:未使用关键字的形式为sex和height传值
>>> register('lili',18,height='1.8m') ## TypeError没有为命名关键字参数height传值。

命名关键字参数也可以有默认值,从而简化调用

>>> def register(name,age,*,sex='male',height):
...     print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
... 
>>> register('lili',18,height='1.8m')
Name:lili,Age:18,Sex:male,Height:1.8m

需要强调的是:sex不是默认参数,height也不是位置参数,因为二者均在后,所以都是命名关键字参数,形参sex=’male’属于命名关键字参数的默认值,因而即便是放到形参height之前也不会有问题。另外,如果形参中已经有一个args了,命名关键字参数就不再需要一个单独的*作为分隔符号了

>>> def register(name,age,*args,sex='male',height):
...   print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
... 
>>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m

2.6 组合使用

综上所述所有参数可任意组合使用,但定义顺序必须是:位置参数、默认参数、args、命名关键字参数、**kwargs
可变参数
args与关键字参数kwargs通常是组合在一起使用的,如果一个函数的形参为*args与kwargs,那么代表该函数可以接收任何形式、任意长度的参数

>>> def wrapper(*args,**kwargs):
...     pass
...

在该函数内部还可以把接收到的参数传给另外一个函数(这在4.6小节装饰器的实现中大有用处)

>>> def func(x,y,z):
...     print(x,y,z)
... 
>>> def wrapper(*args,**kwargs):
...     func(*args,**kwargs)
...
>>> wrapper(1,z=3,y=2)
1 2 3

按照上述写法,在为函数wrapper传参时,其实遵循的是函数func的参数规则,调用函数wrapper的过程分析如下:

  1. 位置实参1被*接收,以元组的形式保存下来,赋值给args,即args=(1,),关键字实参z=3,y=2被**接收,以字典的形式保存下来,赋值给kwargs,即kwargs={‘y’: 2, ‘z’: 3}
  2. 执行func(args,kwargs),即func((1,),* {‘y’: 2, ‘z’: 3}),等同于func(1,z=3,y=2)
提示: *args、**kwargs中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定俗成的。

3 名称空间与作用域

一 名称空间

名称空间即存放名字与对象映射/绑定关系的地方。对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x表示清除该绑定关系。
​ 在程序执行期间最多会存在三种名称空间

1.1 内建名称空间

伴随python解释器的启动/关闭而产生/回收,因而是第一个被加载的名称空间,用来存放一些内置的名字,比如内建函数名

>>> max
<built-in function max> #built-in内建

1.2 全局名称空间

伴随python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下名字

import sys #模块名sys

x=1 #变量名x

if x == 1:
    y=2 #变量名y

def foo(x): #函数名foo
    y=1
    def bar():
        pass

Class Bar: #类名Bar
	pass

1.3 局部名称空间

伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中

def foo(x):
    y=3 #调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中

名称空间的加载顺序是:内置名称空间->全局名称空间->局部名称空间,而查找一个名字,必须从三个名称空间之一找到,查找顺序为:局部名称空间->全局名称空间->内置名称空间。

二 作用域

2.1 全局作用域与局部作用域

按照名字作用范围的不同可以将三个名称空间划分为两个区域:

  1. 全局作用域:位于全局名称空间、内建名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
  2. 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。

2.2 作用域与名字查找的优先级

​ 在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找:先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常

x=100 #全局作用域的名字x
def foo():
    x=300 #局部作用域的名字x
    print(x) #在局部找x
foo()#结果为300

在全局作用域查找名字时,起始位置便是全局作用域,所以先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常

x=100
def foo():
    x=300 #在函数调用时产生局部作用域的名字x
foo()
print(x) #在全局找x,结果为100

提示:可以调用内建函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。在全局作用域查看到的locals()的结果等于globals()

Python支持函数的嵌套定义,在内嵌的函数内查找名字时,会优先查找自己局部作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域

x=1
def outer():
    x=2
    def inner(): ## 函数名inner属于outer这一层作用域的名字
        x=3
        print('inner x:%s' %x)

    inner()
    print('outer x:%s' %x)

outer() 
#结果为
inner x:3
outer x:2

在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字

x=1
def foo():
    global x #声明x为全局名称空间的名字
    x=2
foo()
print(x) #结果为2

当实参的值为可变类型时,函数体内对该值的修改将直接反应到原值,

num_list=[1,2,3]
def foo(nums):
    nums.append(5)

foo(num_list)
print(num_list)
#结果为
[1, 2, 3, 5]

对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局)

def  f1():
    x=2
    def f2():
        nonlocal x
        x=3
    f2() #调用f2(),修改f1作用域中名字x的值
    print(x) #在f1作用域查看x

f1()

#结果
3

nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。

函数对象与闭包

一 函数对象

函数对象指的是函数可以被当做’数据’来处理,具体可以分为四个方面的使用,我们如下

1.1 函数可以被引用

>>> def add(x,y):
...     return x+y
... 
>>> func=add
>>> func(1,2)
3

1.2 函数可以作为容器类型的元素

>>> dic={'add':add,'max':max}
>>> dic
{'add': <function add at 0x100661e18>, 'max': <built-in function max>}
>>> dic['add'](1,2)
3

1.3 函数可以作为参数传入另外一个函数

>>> def foo(x,y,func):
...     return func(x,y)
...
>>> foo(1,2,add)
3

1.4 函数的返回值可以是一个函数

>>> def bar():
...     return add
...
>>> func=bar()
>>> func(1,2)
3

二 闭包函数

2.1 闭与包

基于函数对象的概念,可以将函数返回到任意位置去调用,但作用域的关系是在定义完函数时就已经被确定了的,与函数的调用位置无关。

x=1

def f1():
    def f2():
        print(x)

    return f2

def f3():
    x=3
    f2=f1() #调用f1()返回函数f2
    f2() #需要按照函数定义时的作用关系去执行,与调用位置无关

f3() #结果为1

也就是说函数被当做数据处理时,始终以自带的作用域为准。若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该’内嵌函数’就是闭包函数,简称闭包(Closures)

x=1
def outer():
    x=2
    def inner():
        print(x)
    return inner

func=outer()
func() ## 结果为2

可以通过函数的__closure__属性,查看到闭包函数所包裹的外部变量

>>> func.__closure__
(<cell at 0x10212af78: int object at 0x10028cca0>,)
>>> func.__closure__[0].cell_contents
2

“闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。

2.2 闭包的用途

目前为止,我们得到了两种为函数体传值的方式,一种是直接将值以参数的形式传入,另外一种就是将值包给函数

import requests

#方式一:
def get(url):
    return requests.get(url).text

#方式二:
def page(url):
    def get():
        return requests.get(url).text
    return get

提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,需要事先安装:pip3 install requests

对比两种方式,方式一在下载同一页面时需要重复传入url,而方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url

## 方式一下载同一页面
get('https://www.python.org')
get('https://www.python.org')
get('https://www.python.org')
……

## 方式二下载同一页面
python=page('https://www.python.org')
python()
python()
python()
……

闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在接下来的装饰器中也将大有用处

装饰器

一 装饰器介绍

1.1 为何要用装饰器

软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

1.2 什么是装饰器

’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。

二 装饰器的实现

函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

2.1 无参装饰器的实现

如果想为下述函数添加统计其执行时间的功能

import time

def index():
    time.sleep(3)
    print('Welcome to the index page’)
    return 200

index() #函数执行

遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样

start_time=time.time()
index() #函数执行
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))

考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入

def wrapper(func): ## 通过参数接收外部的值
    start_time=time.time()
    res=func()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    return res

但之后函数的调用方式都需要统一改成

wrapper(index)
wrapper(其他函数)

这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下

def timer(func):
    def wrapper(): ## 引用外部作用域的变量func
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下

index=timer(index)  #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
index() ## 执行的是wrapper(),在wrapper的函数体内再执行最原始的index

至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常

def home(name):
    time.sleep(5)
    print('Welcome to the home page',name)

home=timer(home)
home('egon')
#抛出异常
TypeError: wrapper() takes 0 positional arguments but 1 was given

之所以会抛出异常,是因为home(‘egon’)调用的其实是wrapper(‘egon’),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,便用上*args+**kwargs组合(见4.3小节),于是修正装饰器timer如下

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名

@timer ## index=timer(index)
def index():
    time.sleep(3)
    print('Welcome to the index page')
    return 200
@timer ## index=timer(home)
          def home(name):
    time.sleep(5)
    print('Welcome to the home page’,name)

如果我们有多个装饰器,可以叠加多个

@deco3
@deco2
@deco1
def index():
    pass

叠加多个装饰器也无特殊之处,上述代码语义如下:

index=deco3(deco2(deco1(index)))

2.2 有参装饰器的实现

了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下

def deco(func):
    def wrapper(*args,**kwargs):
        编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
    return wrapper

如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下

def deco(func):
        def wrapper(*args,**kwargs):
            if driver == 'file':
                编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
            elif driver == 'mysql':
                编写基于mysql认证,认证通过则执行res=func(*args,**kwargs),并返回res
        return wrapper

函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到

def auth(driver):
    def deco(func):
        ……
    return deco

此时我们就实现了一个有参装饰器,使用方式如下

#先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样
@auth(driver='file')
def index():
    pass

@auth(driver='mysql')
def home():
    pass

可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的__doc__属性,但对于被装饰之后的函数,查看文档注释

@timer
def home(name):
    '''
    home page function
    :param name: str
    :return: None
    '''
    time.sleep(5)
    print('Welcome to the home page',name)

print(help(home))
'''
打印结果:

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

None
'''

在被装饰之后home=wrapper,查看home.__name__也可以发现home的函数名确实是wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    wrapper.__doc__=func.__doc__
    wrapper.__name__=func.__name__
    return wrapper

按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

迭代器

一 迭代器介绍

迭代器即用来迭代取值的工具,而迭代是重复反馈过程的活动,其目的通常是为了逼近所需的目标或结果,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,单纯的重复并不是迭代

while True:
    msg = input('>>: ').strip()
    print(msg)

下述while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以取尽列表中的值

goods=['mac','lenovo','acer','dell','sony']

index=0
while index < len(goods):
    print(goods[index])
    index+=1

1.1 可迭代对象

通过索引的方式进行迭代取值,实现简单,但仅适用于序列类型:字符串,列表,元组。对于没有索引的字典、集合等非序列类型,必须找到一种不依赖索引来进行迭代取值的方式,这就用到了迭代器。
要想了解迭代器为何物,必须事先搞清楚一个很重要的概念:可迭代对象(Iterable)。从语法形式上讲,内置有__iter__方法的对象都是可迭代对象,字符串、列表、元组、字典、集合、打开的文件都是可迭代对象:

{'name':'egon'}.__iter__
{7,8,9}.__iter__
……

1.2 迭代器对象

调用obj.iter()方法返回的结果就是一个迭代器对象(Iterator)。迭代器对象是内置有__iter__和__next__方法的对象,打开的文件本身就是一个迭代器对象,执行迭代器对象.iter()方法得到的仍然是迭代器本身,而执行迭代器.next()方法就会计算出迭代器中的下一个值。 迭代器是Python提供的一种统一的、不依赖于索引的迭代取值方式,只要存在多个“值”,无论序列类型还是非序列类型都可以按照迭代器的方式取值

>>> s={1,2,3} ## 可迭代对象s
>>> i=iter(s)  ## 本质就是在调用s.__iter__(),返回s的迭代器对象i,
>>> next(i) ## 本质就是在调用i.__next__()
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)  #抛出StopIteration的异常,代表无值可取,迭代结束

二 for循环原理

有了迭代器后,我们便可以不依赖索引迭代取值了,使用while循环的实现方式如下

goods=['mac','lenovo','acer','dell','sony']
i=iter(goods) #每次都需要重新获取一个迭代器对象
while True:
    try:
        print(next(i))
    except StopIteration: #捕捉异常终止循环
        break

for循环又称为迭代循环,in后可以跟任意可迭代对象,上述while循环可以简写为

goods=['mac','lenovo','acer','dell','sony']
for item in goods:   
    print(item)

for 循环在工作时,首先会调用可迭代对象goods内置的__iter__方法拿到一个迭代器对象,然后再调用该迭代器对象的__next__方法将取到的值赋给item,执行循环体完成一次循环,周而复始,直到捕捉StopIteration异常,结束迭代。

三 迭代器的优缺点

基于索引的迭代取值,所有迭代的状态都保存在了索引中,而基于迭代器实现迭代的方式不再需要索引,所有迭代的状态就保存在迭代器中,然而这种处理方式优点与缺点并存:

3.1 优点:

1、为序列和非序列类型提供了一种统一的迭代取值方式。
2、惰性计算:迭代器对象表示的是一个数据流,可以只在需要时才去调用__next__来计算出一个值,就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。

3.2 缺点:

1、除非取尽,否则无法获取迭代器的长度
2、只能取下一个值,不能回到开始,更像是‘一次性的’,迭代器产生后的唯一目标就是重复执行next方法直到值取尽,否则就会停留在某个位置,等待下一次调用next;若是要再次迭代同个对象,你只能重新调用iter方法去创建一个新的迭代器对象,如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。

生成器与yield

若函数体包含yield关键字,再调用函数,并不会执行函数体代码,得到的返回值即生成器对象

>>> def my_range(start,stop,step=1):
...     print('start...')
...     while start < stop:
...         yield start
...         start+=step
...     print('end...')
... 
>>> g=my_range(0,3)
>>> g
<generator object my_range at 0x104105678>

生成器内置有__iter__和__next__方法,所以生成器本身就是一个迭代器

>>> g.__iter__
<method-wrapper '__iter__' of generator object at 0x1037d2af0>
>>> g.__next__
<method-wrapper '__next__' of generator object at 0x1037d2af0>

因而我们可以用next(生成器)触发生成器所对应函数的执行,

>>> next(g) ## 触发函数执行直到遇到yield则停止,将yield后的值返回,并在当前位置挂起函数
start...
0
>>> next(g) ## 再次调用next(g),函数从上次暂停的位置继续执行,直到重新遇到yield...
1
>>> next(g) ## 周而复始...
2
>>> next(g) ## 触发函数执行没有遇到yield则无值返回,即取值完毕抛出异常结束迭代
end...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

既然生成器对象属于迭代器,那么必然可以使用for循环迭代,如下:

>>> for i in countdown(3):
...     print(i)
... 
countdown start
3
2
1
Done!

有了yield关键字,我们就有了一种自定义迭代器的实现方式。yield可以用于返回值,但不同于return,函数一旦遇到return就结束了,而yield可以保存函数的运行状态挂起函数,用来返回多次值

二 yield表达式应用

在函数内可以采用表达式形式的yield

>>> def eater():
...     print('Ready to eat')
...     while True:
...         food=yield
...         print('get the food: %s, and start to eat' %food)
...

可以拿到函数的生成器对象持续为函数体send值,如下

>>> g=eater() ## 得到生成器对象
>>> g
<generator object eater at 0x101b6e2b0>
>>> next(e) ## 需要事先”初始化”一次,让函数挂起在food=yield,等待调用g.send()方法为其传值
Ready to eat
>>> g.send('包子')
get the food: 包子, and start to eat
>>> g.send('鸡腿')
get the food: 鸡腿, and start to eat

针对表达式形式的yield,生成器对象必须事先被初始化一次,让函数挂起在food=yield的位置,等待调用g.send()方法为函数体传值,g.send(None)等同于next(g)。

​ 我们可以编写装饰器来完成为所有表达式形式yield对应生成器的初始化操作,如下

def init(func):
    def wrapper(*args,**kwargs):
        g=func(*args,**kwargs)
        next(g)
        return g
    return wrapper

@init
def eater():
    print('Ready to eat')
    while True:
        food=yield
        print('get the food: %s, and start to eat' %food)

表达式形式的yield也可以用于返回多次值,即变量名=yield 值的形式,如下

>>> def eater():
...     print('Ready to eat')
...     food_list=[]
...     while True:
...         food=yield food_list
...         food_list.append(food)
... 
>>> e=eater()
>>> next(e)
Ready to eat
[]
>>> e.send('蒸羊羔')
['蒸羊羔']
>>> e.send('蒸熊掌')
['蒸羊羔', '蒸熊掌']
>>> e.send('蒸鹿尾儿')
['蒸羊羔', '蒸熊掌', '蒸鹿尾儿']

三 三元表达式、列表生成式、生成器表达式

3.1 三元表达式

三元表达式是python为我们提供的一种简化代码的解决方案,语法如下

res = 条件成立时返回的值 if 条件 else 条件不成立时返回的值

针对下述场景

def max2(x,y):
    if x > y:
        return x
    else:
        return y
    
res = max2(1,2)

用三元表达式可以一行解决

x=1
y=2
res = x if x > y else y ## 三元表达式

3.2 列表生成式

列表生成式是python为我们提供的一种简化代码的解决方案,用来快速生成列表,语法如下

[expression for item1 in iterable1 if condition1
for item2 in iterable2 if condition2
...
for itemN in iterableN if conditionN
]

#类似于
res=[]
for item1 in iterable1:
    if condition1:
        for item2 in iterable2:
            if condition2
                ...
                for itemN in iterableN:
                    if conditionN:
                        res.append(expression)

针对下述场景

egg_list=[]
for i in range(10):
    egg_list.append('鸡蛋%s' %i)

用列表生成式可以一行解决

egg_list=['鸡蛋%s' %i for i in range(10)]

3.3 生成器表达式

创建一个生成器对象有两种方式,一种是调用带yield关键字的函数,另一种就是生成器表达式,与列表生成式的语法格式相同,只需要将[]换成(),即:

(expression for item in iterable if condition)

对比列表生成式返回的是一个列表,生成器表达式返回的是一个生成器对象

>>> [x*x for x in range(3)]
[0, 1, 4]
>>> g=(x*x for x in range(3))
>>> g
<generator object <genexpr> at 0x101be0ba0>

对比列表生成式,生成器表达式的优点自然是节省内存(一次只产生一个值在内存中)

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g) #抛出异常StopIteration

如果我们要读取一个大文件的字节数,应该基于生成器表达式的方式完成

with open('db.txt','rb') as f:
    nums=(len(line) for line in f)
    total_size=sum(nums) ## 依次执行next(nums),然后累加到一起得到结果=

函数递归

一 函数递归调用介绍

函数不仅可以嵌套定义,还可以嵌套调用,即在调用一个函数的过程中,函数内部又调用另一个函数,而函数的递归调用指的是在调用一个函数的过程中又直接或间接地调用该函数本身

例如
在调用f1的过程中,又调用f1,这就是直接调用函数f1本身

def f1():
    print('from f1')
    f1()
f1()

配图:递归调用1

在调用f1的过程中,又调用f2,而在调用f2的过程中又调用f1,这就是间接调用函数f1本身

def f1():
    print('from f1')
    f2()

def f2():
    print('from f2')
    f1()
    
f1()

从上图可以看出,两种情况下的递归调用都是一个无限循环的过程,但在python对函数的递归调用的深度做了限制,因而并不会像大家所想的那样进入无限循环,会抛出异常,要避免出现这种情况,就必须让递归调用在满足某个特定条件下终止。
提示:

#1. 可以使用sys.getrecursionlimit()去查看递归深度,默认值为1000,虽然可以使用
sys.setrecursionlimit()去设定该值,但仍受限于主机操作系统栈大小的限制

#2. python不是一门函数式编程语言,无法对递归进行尾递归优化。

二 回溯与递推

下面我们用一个浅显的例子,为了让读者阐释递归的原理和使用:
例4.5
某公司四个员工坐在一起,问第四个人薪水,他说比第三个人多1000,问第三个人薪水,第他说比第二个人多1000,问第二个人薪水,他说比第一个人多1000,最后第一人说自己每月5000,请问第四个人的薪水是多少?
思路解析:
要知道第四个人的月薪,就必须知道第三个人的,第三个人的又取决于第二个人的,第二个人的又取决于第一个人的,而且每一个员工都比前一个多一千,数学表达式即:

salary(4)=salary(3)+1000
salary(3)=salary(2)+1000
salary(2)=salary(1)+1000
salary(1)=5000

总结为:
salary(n)=salary(n-1)+1000 (n>1)
salary(1)=5000 (n=1)

很明显这是一个递归的过程,可以将该过程分为两个阶段:回溯和递推。
​ 在回溯阶段,要求第n个员工的薪水,需要回溯得到(n-1)个员工的薪水,以此类推,直到得到第一个员工的薪水,此时,salary(1)已知,因而不必再向前回溯了。然后进入递推阶段:从第一个员工的薪水可以推算出第二个员工的薪水(6000),从第二个员工的薪水可以推算出第三个员工的薪水(7000),以此类推,一直推算出第第四个员工的薪水(8000)为止,递归结束。需要注意的一点是,递归一定要有一个结束条件,这里n=1就是结束条件。
配图:回溯与递推
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d4KuyfWW-1648137822723)(%E9%85%8D%E5%9B%BE/%E5%9B%9E%E6%BA%AF%E4%B8%8E%E9%80%92%E6%8E%A8.jpg)]
代码实现:

def salary(n):
    if n==1:
        return 5000
    return salary(n-1)+1000

s=salary(4)
print(s)

执行结果:

8000

程序分析:
在未满足n1的条件时,一直进行递归调用,即一直回溯,见图4.3的左半部分。而在满足n1的条件时,终止递归调用,即结束回溯,从而进入递推阶段,依次推导直到得到最终的结果。
递归本质就是在做重复的事情,所以理论上递归可以解决的问题循环也都可以解决,只不过在某些情况下,使用递归会更容易实现,比如有一个嵌套多层的列表,要求打印出所有的元素,代码实现如下

items=[[1,2],3,[4,[5,[6,7]]]]
def foo(items):
    for i in items:
        if isinstance(i,list): #满足未遍历完items以及if判断成立的条件时,一直进行递归调用
            foo(i) 
        else:
            print(i,end=' ')
    
foo(items) #打印结果1 2 3 4 5 6 7

使用递归,我们只需要分析出要重复执行的代码逻辑,然后提取进入下一次递归调用的条件或者说递归结束的条件即可,代码实现起来简洁清晰

面向过程与函数式

一 编程范式

​ 很多初学者在了解了一门编程语言的基本语法和使用之后,面对一个’开发需求‘时仍然会觉得无从下手、没有思路/套路,本节主题“编程范式”正是为了解决该问题,那到底什么是编程范式呢?
编程范式指的就是编程的套路,打个比方,如果把编程的过程比喻为练习武功,那编程范式指的就是武林中的各种流派,而在编程的世界里常见的流派有:面向过程、函数式、面向对象等,本节我们主要介绍前两者。
​ 在正式介绍前,我们需要强调:“功夫的流派没有高低之分,只有习武的人才有高低之分“,在编程世界里更是这样,各种编程范式在不同的场景下都各有优劣,谁好谁坏不能一概而论,下面就让我们来一一解读它们。

二 面向过程

​ ”面向过程“核心是“过程”二字,“过程”指的是解决问题的步骤,即先干什么再干什么…,基于面向过程开发程序就好比在设计一条流水线,是一种机械式的思维方式,这正好契合计算机的运行原理:任何程序的执行最终都需要转换成cpu的指令流水按过程调度执行,即无论采用什么语言、无论依据何种编程范式设计出的程序,最终的执行都是过程式的。

​ 详细的,若程序一开始是要着手解决一个大的问题,按照过程式的思路就是把这个大的问题分解成很多个小问题或子过程去实现,然后依次调用即可,这极大地降低了程序的复杂度。举例如下:
​ 写一个数据远程备份程序,分三步:本地数据打包,上传至云服务器,检测备份文件可用性

import os,time

## 一:基于本章所学,我们可以用函数去实现这一个个的步骤
## 1、本地数据打包
def data_backup(folder):
    print("找到备份目录: %s" %folder)
    print('正在备份...')
    zip_file='/tmp/backup_%s.zip' %time.strftime('%Y%m%d')
    print('备份成功,备份文件为: %s' %zip_file)
    return zip_file

#2、上传至云服务器
def cloud_upload(file):
    print("\nconnecting cloud storage center...")
    print("cloud storage connected")
    print("upload [%s] to cloud..." %file)
    link='https://www.xxx.com/bak/%s' %os.path.basename(file)
    print('close connection')
    return link

#3、检测备份文件可用性
def data_backup_check(link):
    print("\n下载文件: %s , 验证文件是否无损..." %link)


#二:依次调用
## 步骤一:本地数据打包
zip_file = data_backup(r"/Users/egon/欧美100G高清无码")

## 步骤二:上传至云服务器
link=cloud_upload(zip_file)

## 步骤三:检测备份文件的可用性
data_backup_check(link)

面向过程总结:
1、优点

将复杂的问题流程化,进而简单化

2、缺点

'''
程序的可扩展性极差,因为一套流水线或者流程就是用来解决一个问题,就好比生产汽水的流水线无法生产汽车一样,即便是能,也得是大改,而且改一个组件,与其相关的组件可能都需要修改,比如我们修改了cloud_upload的逻辑,那么依赖其结果才能正常执行的data_backup_check也需要修改,这就造成了连锁反应,而且这一问题会随着程序规模的增大而变得越发的糟糕。
'''

def cloud_upload(file): ## 加上异常处理,在出现异常的情况下,没有link返回
    try:
        print("\nconnecting cloud storage center...")
        print("cloud storage connected")
        print("upload [%s] to cloud..." %file)
        link='https://www.xxx.com/bak/%s' %os.path.basename(file)
        print('close connection')
        return link
    except Exception:
        print('upload error')
    finally:
        print('close connection.....')

def data_backup_check(link): ## 加上对参数link的判断
    if link:
        print("\n下载文件: %s , 验证文件是否无损..." %link)
    else:
        print('\n链接不存在')

3、应用场景

面向过程的程序设计一般用于那些功能一旦实现之后就很少需要改变的场景, 如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程去实现是极好的,但如果你要处理的任务是复杂的,且需要不断迭代和维护, 那还是用面向对象最为方便。

三 函数式

函数式编程并非用函数编程这么简单,而是将计算机的运算视为数学意义上的运算,比起面向过程,函数式更加注重的是执行结果而非执行的过程,代表语言有:Haskell、Erlang。而python并不是一门函数式编程语言,但是仍为我们提供了很多函数式编程好的特性,如lambda,map,reduce,filter

3.1 匿名函数与lambda

​ 对比使用def关键字创建的是有名字的函数,使用lambda关键字创建则是没有名字的函数,即匿名函数,语法如下

lambda 参数1,参数2,...: expression

举例

## 1、定义
lambda x,y,z:x+y+z

#等同于
def func(x,y,z):
	return x+y+z

## 2、调用
## 方式一:
res=(lambda x,y,z:x+y+z)(1,2,3)

## 方式二:
func=lambda x,y,z:x+y+z ## “匿名”的本质就是要没有名字,所以此处为匿名函数指定名字是没有意义的
res=func(1,2,3)

匿名函数与有名函数有相同的作用域,但是匿名意味着引用计数为0,使用一次就释放,所以匿名函数用于临时使用一次的场景,匿名函数通常与其他函数配合使用,我们以下述字典为例来介绍它

salaries={
	'siry':3000,
	'tom':7000,
	'lili':10000,
	'jack':2000
}

要想取得薪水的最大值和最小值,我们可以使用内置函数max和min(为了方便开发,python解释器已经为我们定义好了一系列常用的功能,称之为内置的函数,我们只需要拿来使用即可)

>>> max(salaries)
'tom'
>>> min(salaries)
'jack'

内置max和min都支持迭代器协议,工作原理都是迭代字典,取得是字典的键,因而比较的是键的最大和最小值,而我们想要的是比较值的最大值与最小值,于是做出如下改动

## 函数max会迭代字典salaries,每取出一个“人名”就会当做参数传给指定的匿名函数,然后将匿名函数的返回值当做比较依据,最终返回薪资最高的那个人的名字
>>> max(salaries,key=lambda k:salaries[k]) 
'lili'
## 原理同上
>>> min(salaries,key=lambda k:salaries[k])
'jack'

同理,我们直接对字典进行排序,默认也是按照字典的键去排序的

>>> sorted(salaries)
['jack', 'lili', 'siry', 'tom']

3.2 map、reduce、filter

函数map、reduce、filter都支持迭代器协议,用来处理可迭代对象,我们以一个可迭代对象array为例来介绍它们三个的用法

array=[1,2,3,4,5]

要求一:对array的每个元素做平方处理,可以使用map函数
map函数可以接收两个参数,一个是函数,另外一个是可迭代对象,具体用法如下

>>> res=map(lambda x:x**2,array)
>>> res
<map object at 0x1033f45f8>
>>>

解析:map会依次迭代array,得到的值依次传给匿名函数(也可以是有名函数),而map函数得到的结果仍然是迭代器。

>>> list(res) #使用list可以依次迭代res,取得的值作为列表元素
[1, 4, 9, 16, 25]

要求二:对array进行合并操作,比如求和运算,这就用到了reduce函数
reduce函数可以接收三个参数,一个是函数,第二个是可迭代对象,第三个是初始值

## reduce在python2中是内置函数,在python3中则被集成到模块functools中,需要导入才能使用
>>> from functools import reduce 
>>> res=reduce(lambda x,y:x+y,array)
>>> res
15

解析:
1 没有初始值,reduce函数会先迭代一次array得到的值作为初始值,作为第一个值数传给x,然后继续迭代一次array得到的值作为第二个值传给y,运算的结果为3
2 将上一次reduce运算的结果作为第一个值传给x,然后迭代一次array得到的结果作为第二个值传给y,依次类推,知道迭代完array的所有元素,得到最终的结果15
也可以为reduce指定初始值

>>> res=reduce(lambda x,y:x+y,array,100)
>>> res
115

要求三:对array进行过滤操作,这就用到了filter函数,比如过滤出大于3的元素

>>> res=filter(lambda x:x>3,array)

解析:filter函数会依次迭代array,得到的值依次传给匿名函数,如果匿名函数的返回值为真,则过滤出该元素,而filter函数得到的结果仍然是迭代器。

>>> list(res) 
[4, 5]

提示:我们介绍map、filter、reduce只是为了带大家了解函数式编程的大致思想,在实际开发中,我们完全可以用列表生成式或者生成器表达式来实现三者的功能。

posted @ 2022-02-27 22:20  喝茶看狗叫  阅读(12)  评论(0编辑  收藏  举报  来源