文章目录
1. 编程范式(此处主要介绍命令式和函数式编程)
1.1 概述
编程范式(Programming Paradigm)是某类典型编程风格或者说是编程方式。根据不同的分类方式,可以对编程范式进行不同形式的划分。常见的有以下两种划分结果:
- 命令式编程(Imperative Programming)、声明式编程(Declarative Programming)和函数式编程(Functional Programming)
- 过程式编程(procedural Programming)、函数式编程和面向对象编程(Object-oriented programming,OOP)
说明:
- 过程式编程和面向对象编程概念较为简单,此处不详细说明。命令式编程和函数式编程下面将会分小节详细介绍。
- 声明式编程:专注于“做什么”而不是“如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程。比如SQL语句。
- 过程式编程也可看作是命令式编程,两者可视为等同,前者的表述更强调与面向对象编程的对比,后者更强调执行的步骤。
注意:
- 对于编程范式的划分还有更多不同的方式,但不同的分类方式都各具争议,如有人也会把函数式归结为声明式的子集,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
- 编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种语言可以适用多种编程范式。
1.2 命令式编程
程序的状态:包含了当前定义的全部变量,即程序定义的全部变量来表示当前状态。程序调试过程中的每一步就是保存了程序的当前状态。有了程序的状态,我们的程序才能不断往前推进。前进的过程就是状态改变的过程。
命令式编程:通过不断修改变量的值,保存当前运行的状态来步步推进。总结就是命令式编程的代码由一系列改变全局状态的语句构成。
命令式编程的运作方式具有图灵机特性,且和冯诺依曼体系的对应关系非常密切。甚至可以说,命令式程序就是一个冯诺依曼机的指令序列:
- 变量 →→ 内存
- 变量引用 →→ 输入设备
- 变量赋值 →→ 输出设备
- 控制结构 →→ 跳转操作
此外,命令式程序执行的效率取决于执行命令的数量,因此才会出现大O表示法等等表示时间空间复杂度的符号。
1.3 函数式编程
1.3.1 函数式编程的本质
函数式编程中的函数,这个术语不是指命令式编程中的函数(可以认为C++程序中的函数本质是一段子程序Subroutine),而是指数学中的函数,即自变量的映射(一种东西和另一种东西之间的对应关系)。也就是说,一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。
纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。
函数式语言的如条件语句,循环语句也不是命令式编程语言中的控制语句,而是函数的语法糖,比如在Scala语言中,if else不是语句而是三元运算符,是有返回值的。
严格意义上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式控制结构进行编程。
1.3.2 函数式编程的优点与不足
函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。
引用透明,即函数运行的结果只依赖于输入的参数,而不依赖于外部状态
No Side Effect,即函数的所有功能就仅仅是返回一个新的值而已,没有其他行为,尤其是不得修改外部变量,因而各个独立的部分的执行顺序可以随意打乱,带来执行顺序上的自由,也因此使得一系列新的特性得以实现:无锁的并发;惰性求值;编译器级别的性能优化等
- 函数不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。
- 由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。
注意:由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。
1.3.3 函数式编程总结
- 前提:函数是一等对象
- 原则:No Side Effect(副作用)
Python中的函数式编程实现:
- 工具:built-in高阶函数;lambda函数;operator模块;functools模块
- 模式:闭包与装饰器
- 替代:用List Comprehension可轻松替代map和filter(reduce替代起来比较困难)
总结:
- 命令式编程里一次变量值的修改,在函数式编程里变成了一个函数的转换。需要注意的是函数式编程中的变量是不能修改的。
- 函数式编程将计算过程抽象成表达式求值。表达式由纯数学函数构成,而这些数学函数是第一类对象且没有副作用。
- 虽然Python支持部分函数式编程的功能,但Python本质上是面向对象的编程语言,它是用面向对象的方式来实现的函数式编程。
2. 一等对象
Python中的一等对象(first-class object)定义:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
Python中,所有函数都是一等对象
3. 高阶函数
定义:接受函数为参数,或把函数作为返回结果的函数
下面介绍一些常用的高阶函数。
3.1 map函数
- 描述
map() 会根据提供的函数对指定序列做映射。
第一个参数 function 对第二个参数序列或迭代器中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表或map对象。 - 语法
map(function, iterable, ...)
- 返回值
Python 2.x 返回列表。
Python 3.x 返回map对象。 - 实例
>>> def square(x): return x * x
...
>>> xx = map(square, range(10))
>>> xx
<map object at 0x7f993713bef0>
>>> xx = list(xx)
>>> xx
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
上述代码可用[x * x for x in range(10)]
代替。实际上,map可以使用列表解析式来代替。
3.2 filter函数
- 描述
filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。
该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。 - 语法
filter(function, iterable)
- 返回值
Pyhton2.7 返回列表,Python3.x 返回 filter 类 - 实例
>>> x = [(), [], {}, None, '', False, 0, True, 1, 2, -3]
>>> x_result = filter(bool, x)
>>> x_result
<filter object at 0x7f993713beb8>
>>> x_result = list(x_result)
>>> x_result
[True, 1, 2, -3]
上述代码可用[i for i in x if bool(i)]
代替。实际上,和map相同,filter也可以使用列表解析式来代替。
因此,对序列中的每一个元素执行某一行为的时候均可以用列表解析式来代替。
3.3 reduce函数
- 描述
函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
注意:在 Python3 中,reduce() 函数已经被从全局名字空间里移除了,它现在被放置在 functools 模块里,如果想要使用它,则需要通过引入 functools 模块来调用 reduce() 函数 - 语法
reduce(function, iterable[, initializer])
- 实例
>>> def multiply(a,b): return a * b
...
>>> from functools import reduce
>>> reduce(multiply, range(1,5))
24
reduce函数很难用列表解析式来代替,因为其执行的行为需要同时对序列中的两个元素进行处理。
3.4 sorted函数
- 描述
sorted() 函数对所有可迭代的对象进行排序操作。 - 语法
sorted(iterable[, cmp[, key[, reverse]]])
参数说明:
- iterable – 可迭代对象。
- cmp – 比较的函数,这个具有两个参数,参数的值都是从可迭代对象中取出,此函数必须遵守的规则为,大于则返回1,小于则返回-1,等于则返回0。
- key – 用列表元素的某个属性或传入函数进行作为关键字。接受的函数只能有一个参数,且其返回值表示此元素的权值,sorted将按照权值大小进行排序。
- reverse – 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
- 实例
>>> sorted([x * (-1) ** x for x in range(10)])
[-9, -7, -5, -3, -1, 0, 2, 4, 6, 8]
>>> sorted([x * (-1) ** x for x in range(10)], reverse=True)
[8, 6, 4, 2, 0, -1, -3, -5, -7, -9]
>>> sorted([x * (-1) ** x for x in range(10)], key=abs)
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
>>> sorted([x * (-1) ** x for x in range(10)], reverse=True, key=abs)
[-9, 8, -7, 6, -5, 4, -3, 2, -1, 0]
max/min也有类似的功能,其语法为max/min(iterable, key, default)
。执行过程为:遍历一遍这个迭代器,然后将迭代器的每一个返回值当做参数传给key=func 中的func(一般用lambda表达式定义) ,然后将func的执行结果传给key,然后以key为标准进行大小的判断。
实际上,Python中有很多类似执行过程的函数,其参数列表中都有key参数。
3.5 partial函数
- 描述
可以用来固定一个函数的参数默认值,并返回固定参数后的新函数。 - 语法
partial(func, *args, **keywords)
参数说明:
- func: 需要被扩展的函数,返回的函数其实是一个类 func 的函数
- *args: 需要被固定的位置参数
- **kwargs: 需要被固定的关键字参数
如果在原来的函数 func 中关键字不存在,将会扩展,如果存在,则会覆盖
- 实例
>>> from functools import partial
>>> abs_sorted = partial(sorted, key=abs)
>>> abs_sorted([x * (-1) ** x for x in range(10)])
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
>>> abs_reverse_sorted = partial(sorted, key=abs, reverse=True)
>>> abs_reverse_sorted([x * (-1) ** x for x in range(10)])
[-9, 8, -7, 6, -5, 4, -3, 2, -1, 0]
>>> from functools import partial
>>> def add(*args, **kwargs):
... # 打印位置参数
... for n in args:
... print(n)
... print("-"*20)
... # 打印关键字参数
... for k, v in kwargs.items():
... print('%s:%s' % (k, v))
... # 暂不做返回,只看下参数效果,理解 partial 用法
...
>>> add_partial = partial(add, 10, k1=10, k2=20)
>>> add_partial(1, 2, 3, k3=20)
10
1
2
3
--------------------
k1:10
k2:20
k3:20
>>> add_partial(1, 2, 3, k1=20) #会覆盖已固定的关键字参数
10
1
2
3
--------------------
k1:20
k2:20
4. 匿名函数
定义:使用lambda表达式创建的函数,函数本身没有名字。通常用于需要一个函数,但是不想去命名一个新函数的情况下。
4.1 语句(Statement)和表达式(Expression)
Expressions get a value; Statements do something.
两种的区别就是表达式可以求值,但是语句不可以。
因此,表达式可以出现在“值”出现的地方,如:
result = add( x + 1, y)
而把一个语句作为参数则不可以
result = add( if x == 1:pass , y)
关系:
Compound Statements > Simple Statements > Expressions
复合语句由多个单个语句组成,而单个语句可以是一个表达式也可以不是表达式。
4.2 lambda表达式
- 特点:只能使用纯表达式,不能赋值,不能使用while和try等块语句
- 语法: lambda [arg1 [,arg2 [,arg3……]]]: expression
与def
的区别:
- 写法上:
- def可以用代码块,一个代码块包含多个语句
- lambda只能用单行表达式,而表达式仅仅是单个语句中的一种
- 结果上:
- def语句一定会增加一个函数名称
- lambda不会,这就降低了变量名污染的风险
Tips:
- 能用一个表达式直接放到return里返回的函数都可以用lambda 速写
>>> def multiply(a,b): return a * b
...
>>> multiply(3,4)
12
>>> multiply_by_lambda = lambda x,y: x * y #此处将lambda表达式进行赋值仅用来讲解,实际中常与高阶函数结合,作为函数参数传入
>>> multiply_by_lambda(3,4)
12
- List + lambda 或 Tuple + lambda 可以得到行为列表或元组
>>> f_list = [lambda x: x + 1, lambda x: x ** 2, lambda x: x ** 3]
>>> [f_list[j](10) for j in range(3)]
[11, 100, 1000]
在AI领域里,这种写法常用于处理数据,比如按预定的一系列模式处理数据
5. 入门小坑——惰性计算*
>>> f_list = [lambda x: x**i for i in range(5)] #可理解为[(lambda x: x**i) for i in range(5)]
>>> [f_list[j](10) for j in range(5)]
上述代码输出结果为:
[10000, 10000, 10000, 10000, 10000]
这和python的惰性求值有关。惰性求值,也就是延迟求值,表达式不会在它被绑定到变量之后就立即求值,而是等用到时再求值。
此例中只有当lambda表达式被调用时才将i的值传给它,也就是循环的终值4,所以调用结果均为10000。
- 惰性计算的好处
因为迭代器中所有的数据都被放在内存中以供使用,所以会对内存造成很大的压力。例如:
>>> for num in range(10**1000):
string = ' ' + ' '
....
OverflowError: range() result has too many items
生成器具备迭代器的所有特性,但是它仅供迭代一次,因为它采用的惰性计算的优化策略,就是说它只有当被使用到的时候才把数据取出或者计算出,放到内存中,访问之后随机销毁。
在使用上,它和迭代器的主要区别在于仅能进行一次迭代访问。
>>> iterator = [x for x in range(7)]
>>> Iterator
[0, 1, 2, 3, 4, 5, 6]
>>> generator = (x for x in range(7))
( generator object (genexpr) at 0X00000000025DBA20)
惰性求值不仅在内存层级对空间有着优化的效果,在计算时间上也有着一定的提高。由于避免了不必要的计算,节省了计算资源。