python函数式编程(二):匿名函数、闭包、生成器

东尧爱分享

这是东尧每天一篇文章的第27天

东尧写文章的目标:分享东尧的经验和思考,帮你获取物质和精神两方面幸福。

1

匿名函数lambda

当我们需要实现一些很小的功能的时候,如果使用def声明一个函数就会显得比较复杂了,这种情况下就可以使用lambda来为我们创建一些小的功能函数。

语法:

lambda[参数列表]: 表达式

说明:参数列表是可选的,就是说可以不填写参数,表达式只能写一个,不用写return,该表达式的结果就是函数的返回值。

boo = lambda : 'hello' # 没有参数的lambda

print(boo())

boo2 = lambda x, y: x + y # 带参数的lambda

print(boo2(29,2))

结果:

hello

31

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

>>> f = lambda x: x * x

>>> f

at 0x101c6ef28>

>>> f(5)

25

2

函数里面定义函数

python允许函数里面再定义函数,在函数里面定义的函数只能在函数体里面使用,而不能在外面使用, 要想在外面使用里面定义的函数,那么必须把里面的函数返回出去才能使用。

def foo():

def coo():

print('我是内部函数')

coo()

foo()

结果:

我是内部函数

内部函数是不能在外部使用的:

3

变量作用域

变量的作用域是定义为其声明在程序里的可用范围,就是我们所说的变量的可见性,也就是说这个变量在哪里可以用,哪里不能用。

python变量的作用域主要有两种:全局作用域和局部作用域,拥有全局作用域的变量又叫全局变量,拥有局部作用域的变量叫局部变量

python变量的作用域是对于单个python文件而言的,因为当涉及到多个文件或者模块的时候,需要引入另一个概念“命名空间”。(讲到python模块和包的时候再讲)

一般的,在单个python文件里,如果变量声明在函数体之外的,都是全局变量,全局变量的作用域在声明它的那一行开始,一直到文件的结束。如果是在函数体内声明的变量叫局部变量,局部变量只能在函数体内使用。如果函数体内的局部变量名称与全局变量的名称相同,那么局部变量将会覆盖全局变量,也就是说会优先使用局部的,而不会使用全局的。

a = 10 # 全局变量

def change():

a = 20 # 局部变量

b = 30 # 局部变量

print('内部a的值:',a)

change()

print('外部a的值:',a)

结果:

内部a的值: 20

外部a的值: 10

需要注意的是,局部变量不能在函数体外使用,否则会报错:

循环语句和条件判断语句中定义的变量也是全局变量, 但是不建议这么做,在循环语句和条件判断语句外使用在其中定义的变量会报一个警告信息(变量可能为定义就被引用,主要原因在于条件语句和循环语句都是需要满足一定的前提下才会执行的,因此就会存在条件不满足的情况,所以才会报警告)。

a = 0

while a

a += 1

b = 20

for x in range(3): y = x + 1

if a > 0:

num = 100 # 如果num在else语句中也定义了,那么就没事

else:

snum = 100

# num = 10

print(a)

print(b)

print(x)

上面的例子中,只有满足了一定的条件,条件判断或循环体内的变量才会被定义成功,如果不满足条件,则会报错。比如当a>10的时候是不会执行while内的语句的,那么b也就不会被赋值为20,所以在最后打印b的值的时候,就会报错。其余例子也是如此。

要想在函数内明确使用的是全局变量而不是局部变量,那么就需要使用global关键字声明函数内的这个变量是全局变量不是局部变量。

global语句语法global 变量名

变量名可以是一个或多个,每个名字直接用逗号隔开。例如我们刚才看到的例子,当函数内部声明是global a后,函数内对a产生的一系列操作都会应用到函数内外,因为a是全局变量:

a = 10 # 全局变量

def change():

global a

a = 20 # 局部变量

print('内部a的值:',a)

change()

print('外部a的值:',a)

结果:

内部a的值: 20

外部a的值: 20

使用globle变量声明效果对比:

4

闭包

如果在一个内部函数里,对外在作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。定义在外部函数内的但由内部函数引用或者使用的变量被称为自由变量(比如以下案例的变量x)。

def func(start):

x = [start]

def add_one():

x[0] += 1

return x[0]

return add_one

add = func(5)

print(add())

print(add())

print(add())

结果:

6

7

8

闭包在实际的运用比较少,这里大家只是作为了解就可以了。如果还不理解的也没有关系,先把这个知识点放下,等自己的经验积累到一定程度之后再回头来进行理解。

5

递归

递归就是函数直接或间接地调用自身以进行循环的函数。递归是颇为高级的话题,并且在python中相对少见,但是它是一项应该了解的有用的技术,而且在我们的爬虫开发中还是经常用到的。

递归案例:计算阶乘

一个数的阶乘等于它和之前所有数的乘积,比如3的阶乘等于3*2*1

def factorial(n):

if n == 1:

return 1

return n * factorial(n-1)

print(factorial(5))

结果:

120

让我们来看下程序运行的过程:

执行流程

5 * factorial(4)

4 * factorial(3)

3 * factorial(2)

2 * factorial(1)

1

开始返回

1 * 2

1 * 2 * 3

1 * 2 * 3 * 4

1 * 2 * 3 * 4 * 5

最后由“1 * 2 * 3 * 4 * 5”得到结果120。

但是用递归来实现阶乘,其实并不是一个很好的方法,因为完全可以用循环来实现。用递归实现只是为了说明递归的原理。

# 循环实现阶乘

def factorial(n):

result = 1

while n > 1:

result *= n

n -= 1

return result

print(factorial(5))

还有一个比较明显的,但是又不是很好的使用阶乘来解决问题的案例,那就是计算斐波那契数列的第N项。斐波那契数列就是从第3项开始,每一项都等于前两项之和(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181),如果设F(n)为该数列的第n项(n∈N*),那么这句话可以写成如下形式:F(n)=F(n-1)+F(n-2)

def fibonacc(n):

if n

return 1

return fibonacc(n-1) + fibonacc(n-2)

用递归的方式计算斐波那契数列是非常不明智的,而且也是非常耗时的。因为有很多的项都重复计算了(冗余计算),比如在计算fibonacc(10)时fibonacc(3)的值被计算了21次,在计算fibonacc(30)的时候,fibonacc(3)被计算了317811次,而这些计算的结果都是一样的。因此用递归来计算斐波那契数列项是一项非常不明智的行为,而这些复杂的计算仍然可以通过循环来实现。

# 使用循环实现斐波那契数列

def fibonacc(n):

result = prev_result = next_result = 1

while n > 2:

next_result = prev_result

prev_result = result

result = prev_result + next_result

n -= 1

return result

递归在爬虫中的应用,在爬虫中无法避免的就是进行网络请求,那么网络请求无法避免的就是可能会有各种请求失败问题,那么对于请求失败的话我们需要进行重新请求,如果使用while循环的话,代码看起来就没那么友好了。所以一般情况下都是使用递归的方式进行的。

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

6

生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator,可以通过next()函数获得generator的下一个返回值:

>>> L = [x * x for x in range(10)]

>>> L

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> g = (x * x for x in range(10))

>>> g

at 0x1022ef630>

>>> next(g)

>>> next(g)

1

我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

定义generator还有另一种方法:如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。

def foo():

yield 1

yield 2

s = foo()

print(s)

print(next(s))

print(next(s))

结果:

带yield语句的函数就是一个生成器。return语句只返回一次,但是一个生成器能暂停执行并返回一个中间结果--那就是yield语句的功能,返回一个值给调用者并暂停执行。当调用生成器的next()方法时,它会准确的从离开的地方继续。

当函数没有更多的返回值时就会抛出StopIteration异常:

generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。

要理解generator的工作原理,它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就是结束generator的指令,for循环随之结束。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180504G0FL7500?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券