Python基础系列之--匿名函数闭包【11】

存在即合理
正如明白了x += 1,就会尽量避免x = x + 1;

反观一搜一大把关于闭包的文章,无外乎只是告诉三点

  • 晦涩难懂的概念
  • 给几个简单的实例
  • 结束
    或许还会给个建议 少用

真的是这样么?
我搜索了大量的资料来写这篇文章,只是想尽力把自己说服、说明了。

下边有三到题目以及答案

  • 如果你的预期和答案不符
  • 如果你看不懂
  • 如果你好奇的想一探究竟

请继续

否则就可以关闭这个文章了

文章长且内容多

# 第一题,是用来做什么的?
>>> def isPrime(start, stop):
... return filter(lambda x : not [x % i for i in range(2, x) if x % i == 0], range(start, stop + 1))
# 第二题
i = 0
def new_counter():
i = 10
def count(x=1):
nonlocal i
i += x
return i
return count
a_count = new_counter()
print(a_count())
print(a_count())
print(a_count(3))
# 第三题
def makeActions():
acts = []
for i in range(3):
acts.append(lambda x: i ** x)
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

答案

# 第二题
11
12
15
# 第三题
4
4
8

警示:认真的读下去,可能会花费你不少的时间

目标

  • 掌握匿名函数
  • 掌握高阶函数与匿名函数的用法
  • 尽量搞明白闭包

目录结构

第一部分 匿名函数

1 定义

lambda 函数也叫匿名函数,及即没有具体名称的函数,它允许快速定义单行函数,类似于 C 语言的宏,可以用在任何需要函数的地方。

lambda与def对比

区别 def lambda
名称
返回值 函数对象但不给标识符 任意
作用 简单的 简单/复杂
作用范围 不能共享 可共享

2 表达式

# 函数
def funName():
# 表达式
return "返回值"
# 匿名函数
lambda argument1,argument2,...argumentN:expression using arguments

lambda 参数: 表达式

实现同样代码不同方式的实例:

# 函数
>>> def mul(x, y):
... return x*y
...
>>> mul(2, 3)
6
# 匿名函数
>>> lam = lambda x, y: x*y
>>> lam
<function <lambda> at 0x101b11268>
>>> lam(2, 3)
6
>>> lam(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'y'

3 匿名函数与高阶函数

Python内置对数据处理的函数,效率比for要高,恰好,我们可以结合lambda函数,一块把二者学习了

# 罗列了常用的三种
1. map(function, iterable[, iterable, ...]) -> list

2. filter(function or None, iterable) -> list, tuple, or string

3. reduce(function, sequence[, initial]) -> value

简要介绍:

  • map映射: 对iterable中的item依次执行function(item),执行结果输出为list
  • filter过滤: 对iterable中的item依次执行function(item),将执行结果为True(!=0)的item组成一个List/String/Tuple(取决于iterable的类型)返回,False则退出(0),进行过滤。
  • reduce: iterable中的item顺序迭代调用function,函数必须要有2个参数。要是有第3个参数,则表示初始值,可以继续调用初始值,返回一个值

图解:

map
filter
reduce

实例列表

# ------- map ------- #
>>> map(str, [1, 2, 3, 4])
['1', '2', '3', '4']
>>> map(int, ['1', '2', '3', '4'])
[1, 2, 3, 4]
>>> map(lambda x: x * x, [1, 2, 3, 4]) # 使用 lambda
[1, 4, 9, 16]

# ------- reduce ------- #
# reduce 操作实例
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4]) # 相当于 ((1 * 2) * 3) * 4
24
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4], 5) # ((((5 * 1) * 2) * 3)) * 4
120
>>> f = lambda a, b: a if (a > b) else b # 两两比较,取最大值 【三目运算也在阶段汇总与补充中说明】
>>> reduce(f, [5, 8, 1, 10])
10

# -------- filter ------- #
>>> list = filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5, 6])
>>> print(*list)
2 4 6

两个综合实例

这个就是开头的那个实例: 找出指定返回内的质子,涉及的内容有点多,包含之前章节中的推导式range函数函数not等相关内容,试着先看下,是否能看明白。

# 质数的定义 只有1和它本身两个因数
>>> def isPrime(start, stop):
... # 取出质数,x从range(start,stop+1) 取的数
... return filter(lambda x : not [x % i for i in range(2, x) if x % i == 0], range(start, stop + 1))
...
>>> isPrime(11, 12)
>>> print(result)
<filter object at 0x101b12400>
>>> print(list(result))
[11]

整个过程的步骤有三七步骤

试了下画个流程图,来作为补充,但是我怕画出来,更难理解了;
所以就用文字描述下,希望描述的清楚:

  • 1 range(start, stop + 1) 生成 [)左闭右开list_1数组
  • 2 依次将list_1中的数【x】传给匿名函数【循环该操作】
    • 3 生成[2, x)的list_2
    • 4 遍历list_2, 依次用 x%list_2[i]取余;*【循环该操作】**
    • 5 如果数组长度等于0,回到2
    • 6 如果余数等于0,返回余数,存进list_new;
    • 7 等整个2走完,判断list_new是否存在数据,得出当前x是否是质数,是否从list_1中过滤掉
    • 8 进入list_1下一个元素的判断

先熟悉下not的用法,【在阶段汇总与补充中说明】

>>> not []
True
>>> not [1]
False
>>> not [0]
False

下边这个求阶乘的,没啥难度,只要还记得

# 思考下这个,在思考下上边关于reduce的图例
reduce(lambda x, y: x * y, [1, 2, 3, 4], 5) # ((((5 * 1) * 2) * 3)) * 4

类比一下

# 计算 5!+4!+3!+2!+1!
>>> def addFactorial(n):
... result = []
... for i in map(lambda x:x + 1, range(n)):
... a = reduce(lambda x, y:x * y, map(lambda x:x + 1, range(i)))
... result.append(a)
... return reduce(lambda x, y:x + y, result)
...
>>> addFactorial(2)
3
>>> addFactorial(3)
9

第二部分 闭包

接下来的内容涉及函数相关内容,之前的两篇文章
Python 基础系列–函数【9】
Python 基础系列之作用域【10】

函数概述图

函数是对象,函数可以作为返回值,

在说闭包定义之前,我们先看下开头的那两段代码

1. 初始闭包

函数是对象,函数可以作为返回值。

# 第二题
i = 0
def new_counter():
i = 10
def count(x=1):
nonlocal i
i += x
return i
return count
a_count = new_counter()
print(a_count())
print(a_count())
print(a_count(3))

闭包

上侧的例子主要实现的是一个计数器的功能每点击一次,在原基础上加1;

结合上图,大致了解下流程:

  • 函数定于与返回:
    • 外层函数new_counter返回了内层函数count作为结果;
    • 内层函数count,中的参数:x有个默认值1;
    • 内层函数count中的 i 利用了nonlocal,获取到10;
  • a_count = new_counter()
    • 将变量a_count指向new_counter()返回的count()
    • count()的type为func,并且i持有10对应的内存地址
  • 第一次调用a_count()时
    • 没有参数,x默认为1;
    • i持有10,经过➕后返回11
  • 第二次调用a_count()时
    • 没有参数,x默认为1;
    • i持有11,经过➕后返回12
  • 第二次调用a_count()时
    • 没有参数,x=3;
    • i持有12,经过➕后返回15

此时我不确定看了上图和说明,是否能够看懂闭包的用法

2 闭包的定义

比较晦涩的专业术语,但还是建议认真读读

都是大神发明的东西

真的很巧妙

下边的文字值得读 N 遍

维基百科上对闭包的解释就很经典:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 Peter J. Landin 在1964年将术语闭包定义为一种包含环境成分和控制成分的实体。

  • 闭包概念:
    • 闭包就是有权访问另一个函数作用域中变量的函数.
  • 分析这句话:
    1. 闭包是定义在函数中的函数.
    2. 闭包能访问包含函数的变量.
    3. 闭包携带状态包
      • 即使包含函数执行完了, 被闭包引用的变量也得不到释放.

接下来看第二个例子

3. 闭包的作用域

# 第三题
def makeActions():
acts = []
for i in range(3):
acts.append(lambda x: i ** x)
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

闭包2

当执行结束acts = makeActions()这一行代码。

  1. makeActions()函数内部【i】持有了 2,并且不释放;
  2. acts实际包含了三个函数类型的内容
  3. 当调用acts[x](2), 实际执行函数为lambda x: i ** x,而i持有2
  4. 故当 y 一致时,x 的位置变化不影响结果,(acts[x] (y))

4 加深一点

如果我们就是要错误的结果,【0,1,8】呢?
如何去实现?

还记得在前面🈶个状态的概念吗?

  • 携带状态的闭包的概念
    • 即使包含函数执行完了, 被闭包引用的变量也得不到释放.

思路:假如我们把列表中的位置对应i的状态保存起来,那就不就完事了么?

如果真的从头看到现在,并且一路思考,想必已经花费你不少时间了。

此时的你应该完全理解文中阐述的内容了。

相对真正掌握知识而言,付出的时间的价值是翻倍的

def makeActions():
acts = []
for i in range(3):
def g(param):
newParam = lambda x: param ** x
return newParam
acts.append(g(i))
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

第一次保留的状态
三种状态

5 再深入一些

这段代码能让你想到什么❓
最直观的就是:这个是个闭包

但是:这段代码万万不能让我联想到:直线方程斜率截距坐标系等等的概念。

# 第一种写法
def line_conf(a, b):
def line(x):
return a*x + b
return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5), line2(5))

# 第二种写法
def line_conf(a, b, x):
return a*x + b
line_conf(1, 1, 5)
line_conf(4, 5, 5)

当我们明白了:函数是对象函数可以作为返回值闭包携带状态等等的概念后,可能会思考

  1. 为何会有这种东西存在❓
  2. 究竟其用途是什么❓

关于这个疑问,在所搜大量数据的时候,发现了知乎上有个有意思的问答

以下摘录其中的一部分

  1. 简短答案:前者更具有可读性和可移植性。

  2. 较完整答案:闭包(closure)和类(class)有相通之处,带有面向对象的封装思维。而面向对象编程正是为了更佳的可读性和更关键的可移植性;不过这个例子没太体现出面向对象的额外优势。

  3. 升级答案:题主问出这个问题,很可能是现在流行的编程教材知其然不知其所然的风格带来的恶果。如果未来带着这个想法进入IT行业,会非常不适应公司的代码规范等基础要求;即便不入行只是自己写写程序用,也会和很多优秀、可复用等的理念失之交臂。

  4. 我认为一本优秀的教材除了讲基础语法,应该尽量教你代码为什么这么写好那么写不好、那么那么写现在能用但多半以后会出事

以上为摘录其中的一部分

我们所讨厌的行为,却是自己的所作所为。

多问问什么,也许有一天,那些望而生畏会变成理所当然。

参考链接
Python 函数之lambda、map、filter和reduce
内置函数的使用