最开始接触 Python 是因为女票选了 Python 的课,为了秀一下“男友力”,我花了2个钟头读完 Python 3.6 的文档,做了她一学期的私人答疑。嗯,一点点秀。
在使用Python的过程中,其 解释性的语言,众多彩蛋语法糖,优雅的数据结构,优秀的包管理工具,大量的例程和学习资料,让我一直把 Python 作为我的 首要编程语言。
本文是我在研究 Python 语言本身 高级特性 时所做的笔记,会慢慢更新。
如果你恰好也和我一样喜欢 Python。希望本文能对你的学习有所帮助!
下面是快捷方式(点击直达)
map()
、reduce()
、filter()
、sorted()
、返回函数
、闭包
、匿名函数
、装饰器
、偏函数
map()
map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回。
例如,对于list [1, 2, 3, 4, 5, 6, 7, 8, 9]
如果希望把list的每个元素都作平方,就可以用map()函数:
因此,我们只需要传入函数f(x)=x*x,就可以利用map()函数完成这个计算:1
2
3def f(x):
return x*x
print(map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
输出结果:[1, 4, 9, 10, 25, 36, 49, 64, 81]
注意:map()函数不改变原有的 list,而是返回一个新的 list。
利用map()函数,可以把一个 list 转换为另一个 list,只需要传入转换函数。
由于list包含的元素可以是任何类型,因此,map() 不仅仅可以处理只包含数值的 list,事实上它可以处理包含任意类型的 list,只要传入的函数f可以处理这种数据类型。
reduce()
reduce()函数也是Python内置的一个高阶函数。reduce()函数接收的参数和 map()类似,一个函数 f
,一个list
,但行为和 map()不同,reduce()传入的函数 f 必须接收两个参数,reduce()对list的每个元素反复调用函数f,并返回最终结果值。
例如,编写一个f函数,接收x和y,返回x和y的和:1
2def f(x, y):
return x + y
调用 reduce(f, [1, 3, 5, 7, 9])时,reduce函数将做如下计算:
- 先计算头两个元素:f(1, 3),结果为4;
- 再把结果和第3个元素计算:f(4, 5),结果为9;
- 再把结果和第4个元素计算:f(9,7),结果为16;
- 再把结果和第5个元素计算:f(16, 9),结果为25;
- 由于没有更多的元素了,计算结束,返回结果25。
上述计算实际上是对 list的所有元素求和。虽然Python内置了求和函数sum(),但是,利用reduce()求和也很简单。
reduce()还可以接收第3个可选参数,作为计算的初始值。如果把初始值设为100,计算:1
reduce(f, [1, 3, 5, 7, 9], 100)
结果将变为125
,因为第一轮计算是:
计算初始值和第一个元素:f(100, 1)
,结果为101
。
filter()
filter()函数是 Python 内置的另一个有用的高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是对每个元素进行判断,返回 True或 False,filter()根据判断结果自动过滤掉不符合条件的元素,返回由符合条件元素组成的新list。
例如,要从一个list [1, 4, 6, 7, 9, 12, 17]中删除偶数,保留奇数,首先,要编写一个判断奇数的函数:1
2def is_odd(x):
return x % 2 == 1
然后,利用filter()过滤掉偶数:1
filter(is_odd, [1, 4, 6, 7, 9, 12, 17])
结果: [1, 7, 9, 17]
利用filter(),可以完成很多有用的功能,例如,删除 None 或者空字符串:1
2
3def is_not_empty(s):
return s and len(s.strip()) > 0
filter(is_not_empty, ['test', None, '', 'str', ' ', 'END'])
结果:[‘test’, ‘str’, ‘END’]
注意: s.strip(rm) 删除 s 字符串中开头、结尾处的 rm 序列的字符。
当rm为空时,默认删除空白符(包括’\n’, ‘\r’, ‘\t’, ‘ ‘),如下:1
2a = ' 123'
a.strip()
结果: ‘123’1
2a='\t\t123\r\n'
a.strip()
结果:‘123’
sorted()
Python内置的 sorted()函数可对list进行排序:1
2
3>>> sorted([36, 5, 12, 9, 21])
[5, 9, 12, 21, 36]
但 sorted()也是一个高阶函数,它可以接收一个比较函数来实现自定义排序,比较函数的定义是,传入两个待比较的元素 x, y,如果 x 应该排在 y 的前面,返回 -1,如果 x 应该排在 y 的后面,返回 1。如果 x 和 y 相等,返回 0。
因此,如果我们要实现倒序排序,只需要编写一个reversed_cmp函数:1
2
3
4
5
6def reversed_cmp(x, y):
if x > y:
return -1
if x < y:
return 1
return 0
这样,调用 sorted() 并传入 reversed_cmp 就可以实现倒序排序:1
2>>> sorted([36, 5, 12, 9, 21], reversed_cmp)
[36, 21, 12, 9, 5]
sorted()也可以对字符串进行排序,字符串默认按照ASCII大小来比较:1
2'bob', 'about', 'Zoo', 'Credit']) > sorted([
['Credit', 'Zoo', 'about', 'bob']
‘Zoo’排在’about’之前是因为’Z’的ASCII码比’a’小。
sort()
sort()是list内置
的方法,也可以和python内置的全局sorted()方法一样来对可迭代的序列排序生成新的序列。
key参数/函数
从python2.4开始,list.sort()和sorted()函数增加了key参数来指定一个函数,此函数将在每个元素比较前被调用。 例如通过key指定的函数来忽略字符串的大小写:1
2>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较。这个技术是快速的因为key指定的函数将准确地对每个元素调用。
更广泛的使用情况是用复杂对象的某些值来对复杂对象的序列排序,例如:
1 | > student_tuples = [ |
对对象的属性进行索引:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Student:
def __init__(self, name, grade, age):
self.name = name
self.grade = grade
self.age = age
def __repr__(self):
return repr((self.name, self.grade, self.age))
student_objects = [
Student('john', 'A', 15),
Student('jane', 'B', 12),
Student('dave', 'B', 10),
]
sorted(student_objects, key=lambda student: student.age) # sort by age
结果:[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
key函数不仅可以访问需要排序元素的内部数据,还可以访问外部的资源,例如,如果学生的成绩是存储在dictionary中的,则 可以使用此dictionary来对学生名字的list排序 ,如下:
1 | 'dave', 'john', 'jane'] > students = [ |
Operator 模块函数
上面的key参数的使用非常广泛,因此python提供了一些方便的函数来使得访问方法更加容易和快速。operator模块
有itemgetter
,attrgetter
,从2.6开始还增加了methodcaller方法
。使用这些方法,上面的操作将变得更加简洁和快速:
1 | > from operator import itemgetter, attrgetter |
operator模块还允许 多级 的排序,例如,先以grade,然后再以age来排序:1
2
3
4
51,2)) > sorted(student_tuples, key=itemgetter(
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
'grade', 'age')) > sorted(student_objects, key=attrgetter(
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
升序和降序
list.sort()和sorted()都接受一个参数reverse
(True or False)来表示升序或降序排序。例如对上面的student降序排序如下:
1 | >>> sorted(student_tuples, key=itemgetter(2), reverse=True) |
排序的稳定性和复杂排序
从python2.2开始,排序被保证为稳定的。意思是说多个元素如果有相同的key,则排序前后他们的先后顺序不变。1
2
3
4'red', 1), ('blue', 1), ('red', 2), ('blue', 2)] > data = [(
0)) > sorted(data, key=itemgetter(
[('blue', 1), ('blue', 2), ('red', 1), ('red', 2)]
注意在排序后’blue’的顺序被保持了,即(’blue’, 1)在(’blue’, 2)的前面。
更复杂地你可以构建多个步骤
来进行更复杂的排序,例如对student数据先以grade降序排列,然后再以age升序排列。1
2
3
4>>> s = sorted(student_objects, key=attrgetter('age')) # sort on secondary key
>>> sorted(s, key=attrgetter('grade'), reverse=True) # now sort on primary key, descending
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
最老土的排序方法-DSU
我们称其为DSU(Decorate-Sort-Undecorate),原因为排序的过程需要下列三步:
- 对原始的list进行装饰,使得新list的值可以用来控制排序;
- 对装饰后的list排序;
- 将装饰删除,将排序后的装饰list重新构建为原来类型的list;
例如,使用DSU方法来对student数据根据grade排序:1
2
3
4
5for i, student in enumerate(student_objects)] decorated = [(student.grade, i, student)
decorated.sort()
for grade, i, student in decorated] # undecorate [student
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
上面的比较能够工作,原因是 __tuples是可以用来比较的__
tuples间的比较首先比较tuples的第一个元素,如果第一个相同再比较第二个元素,以此类推。
并不是所有的情况下都需要在以上的tuples中包含索引,但是包含索引可以有以下好处:
- 排序是
稳定的
,如果两个元素有相同的key,则他们的原始先后顺序保持不变; - 原始的元素不必用来做比较,因为tuples的第一和第二元素用来比较已经是足够了。
此方法被RandalL.在perl中广泛推广后,他的另一个名字为也被称为Schwartzian transform。
对大的list或list的元素计算起来太过复杂的情况下,在python2.4前,DSU很可能是最快的排序方法。但是在2.4之后,上面解释的key函数提供了类似的功能。
其他语言普遍使用的排序方法-cmp函数
在python2.4前,sorted()和list.sort()函数没有提供key参数,但是提供了cmp参数来让用户指定比较函数。此方法在其他语言中也普遍存在。
在python3.0中,cmp参数被彻底的移除了,从而简化和统一语言,减少了高级比较和cmp方法的冲突。
在python2.x中cmp参数指定的函数用来进行元素间的比较。此函数需要2个参数,然后返回负数表示小于,0表示等于,正数表示大于。例如:1
2
3
4def numeric_compare(x, y): >
return x - y
5, 2, 4, 1, 3], cmp=numeric_compare) > sorted([
[1, 2, 3, 4, 5]
或者你可以反序排序:1
2
3
4def reverse_numeric(x, y): >
return y - x
5, 2, 4, 1, 3], cmp=reverse_numeric) > sorted([
[5, 4, 3, 2, 1]
当我们将现有的2.x的代码移植到3.x时, 需要将cmp函数转化为key函数
以下的wrapper很有帮助:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 def cmp_to_key(mycmp):
'Convert a cmp= function into a key= function'
class K(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K当需要将cmp转化为key时,只需要:
1
2 >>> sorted([5, 2, 4, 1, 3], key=cmp_to_key(reverse_numeric))
[5, 4, 3, 2, 1]
从python2.7,cmp_to_key()函数被增加到了functools模块中。
其实排序在内部是调用元素的cmp来进行的,所以我们可以为元素类型增加cmp方法使得元素可比较,例如:1
2
3_ = lambda self, other: self.age < other.age > Student.__lt_
> sorted(student_objects)
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
返回函数
Python的函数不但可以返回int、str、list、dict等数据类型,还可以返回函数!
例如,定义一个函数 f(),我们让它返回一个函数 g,可以这样写:1
2
3
4
5
6
7def f():
print('call f()...')
# 定义函数g:
def g():
print('call g()...')
# 返回函数g:
return g
仔细观察上面的函数定义,我们在函数 f 内部又定义了一个函数 g。由于函数 g 也是一个对象,函数名 g 就是指向函数 g 的变量,所以,最外层函数 f 可以返回变量 g,也就是函数 g 本身。
调用函数 f,我们会得到 f 返回的一个函数:1
2
3
4
5
6# 调用f() > x = f()
call f()...
# 变量x是f()返回的函数: > x
<function g at 0x1037bf320>
# x指向函数,因此可以调用 > x()
call g()... # 调用x()就是执行g()函数定义的代码
请注意区分返回函数和返回值:1
2
3
4def myabs():
return abs # 返回函数
def myabs2(x):
return abs(x) # 返回函数调用的结果,返回值是一个数值
返回函数可以把一些计算延迟执行。例如,如果定义一个普通的求和函数:1
2def calc_sum(lst):
return sum(lst)
调用calc_sum()函数时,将立刻计算并得到结果:1
2>>> calc_sum([1, 2, 3, 4])
10
但是,如果返回一个函数,就可以“延迟计算”:1
2
3
4
5def calc_sum(lst):
def lazy_sum():
return sum(lst)
return lazy_sum
# 调用calc_sum()并没有计算出结果,而是返回函数:
1 | >> f = calc_sum([1, 2, 3, 4]) |
1 | > f() |
由于可以返回函数,我们在后续代码里就可以决定到底要不要调用该函数。
编写一个函数calc_prod(lst),它接收一个list,返回一个函数,返回函数可以计算参数的乘积。
1 | def calc_prod(lst): |
闭包
在函数内部定义的函数和外部定义的函数是一样的,只是他们无法被外部访问:
1 | def g(): |
将 g 的定义移入函数 f 内部,防止其他代码调用 g:
1 | def f(): |
但是,考察上一小节定义的 calc_sum 函数:
1 | def calc_sum(lst): |
注意: 发现没法把 lazy_sum 移到 calc_sum 的外部,因为它引用了 calc_sum 的参数 lst。
像这种内层函数引用了外层函数的变量(参数也算变量),然后返回内层函数的情况,称为闭包(Closure)。
闭包的特点是返回的函数还引用了外层函数的局部变量,所以,要正确使用闭包,就要确保引用的局部变量在函数返回后不能变。举例如下:
1 | # 希望一次返回3个函数,分别计算1x1,2x2,3x3: |
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果全部都是 9(请自己动手验证)。
原因就是当count()函数返回了3个函数时,这3个函数所引用的变量 i
的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时他们并未计算 i*i,当 f1 被调用时:
1 | > f1() |
因此,返回函数不要引用任何循环变量,或者后续会发生变化的变量。
返回闭包不能引用循环变量,请改写count()函数,让它正确返回能计算1x1、2x2、3x3的函数。
考察下面的函数 f:1
2
3
4def f(j):
def g():
return j*j
return g
它可以正确地返回一个闭包g,g所引用的变量j不是循环变量,因此将正常执行。
在count函数的循环内部,如果借助f函数,就可以避免引用循环变量i。
参考代码:
1 | def count(): |
匿名函数
高阶函数可以接收函数做参数,有些时候,我们不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算 f(x)=x2 时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:
1 | >>> map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]) |
通过对比可以看出,匿名函数 lambda x: x * x
实际上就是:
1 | def f(x): |
关键字lambda
表示匿名函数,冒号前面的 x 表示函数参数
。
匿名函数有个限制,就是只能有一个表达式,不写return,返回值就是该表达式的结果。
使用匿名函数,可以不必定义函数名
,直接创建一个函数对象,很多时候可以简化代码:
1 | >>> sorted([1, 3, 9, 5, 0], lambda x,y: -cmp(x,y)) |
返回函数的时候,也可以返回匿名函数:
1 | >> myabs = lambda x: -x if x < 0 else x |
装饰器
不改变函数本身,但是改变函数功能。
使用高阶函数:
1 | def f1(x): |
使用
decorator
用Python提供的@
语法,这样可以避免手动编写f = decorate(f)
这样的代码。
装饰器的作用:
- 打印日志:@log
- 检测性能:@performance
- 数据库失误:@transaction
- URL路由:@post(‘/register’)
无参数decorator
考察一个@log的定义:
1 | def log(f): |
对于阶乘函数,@log工作得很好:
1 |
|
结果:
1 | call factorial()... |
但是,对于参数不是一个的函数,调用将报错:
1 |
|
结果:
1 | Traceback (most recent call last): |
因为 add() 函数需要传入两个参数,但是 @log 写死了只含一个参数的返回函数。
要让 @log 自适应任何参数定义的函数,可以利用Python的 args 和 *kw,保证任意个数的参数总是能正常调用:
1 | def log(f): |
现在,对于任意函数,@log 都能正常工作。
带参数decorator
考察上一节的 @log 装饰器:
1 | def log(f): |
发现对于被装饰的函数,log打印的语句是不能变的(除了函数名)。
如果有的函数非常重要,希望打印出’[INFO] call xxx()…’,有的函数不太重要,希望打印出’[DEBUG] call xxx()…’,这时,log函数本身就需要传入’INFO’或’DEBUG’这样的参数,类似这样:
1 |
|
把上面的定义翻译成高阶函数的调用,就是:
my_func = log('DEBUG')(my_func)
上面的语句看上去还是比较绕,再展开一下:
1 | log_decorator = log('DEBUG') |
上面的语句又相当于:
1 | log_decorator = log('DEBUG') |
所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数:
1 | def log(prefix): |
执行结果:
1 | [DEBUG] test()... |
对于这种3层嵌套的decorator定义,你可以先把它拆开:
1 | # 标准decorator: |
拆开以后会发现,调用会失败,因为在3层嵌套的decorator定义中,最内层的wrapper引用了最外层的参数prefix,所以,把一个闭包拆成普通的函数调用会比较困难。不支持闭包的编程语言要实现同样的功能就需要更多的代码。
例程:函数运行时间打印
1 | import time |
完善decorator
@decorator可以动态实现函数功能的增加,但是,经过@decorator“改造”后的函数,和原函数相比,除了功能多一点外,有没有其它不同的地方?
在没有decorator的情况下,打印函数名:
1 | def f1(x): |
输出: f1
有decorator的情况下,再打印函数名:
1 | def log(f): |
输出: wrapper
可见,由于decorator返回的新函数函数名已经不是’f2’,而是@log内部定义的’wrapper’。 这对于那些依赖函数名的代码就会失效。 decorator还改变了函数的__doc__
等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中:
1 | def log(f): |
这样写decorator很不方便,因为我们也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的functools
可以用来自动化完成这个“复制”的任务:
1 | import functools |
最后需要指出,由于我们把原函数签名改成了(args, *kw),因此, 无法获得原函数的原始参数信息 。即便我们采用固定参数来装饰只有一个参数的函数:
1 | def log(f): |
也可能改变原函数的参数名,因为新函数的参数名始终是 ‘x’,原函数定义的参数名不一定叫 ‘x’。
例程:函数运行时间打印
1 | import time |
偏函数
当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化调用者的负担。
比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
1 | '12345') > int( |
但int()函数还提供额外的base参数
,默认值为10。如果传入base参数,就可以做 N 进制的转换:
1 | '12345', base=8) > int( |
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到, 可以定义一个int2()的函数,默认把base=2传进去 :
1 | def int2(x, base=2): |
这样,我们转换二进制就非常方便了:
1 | '1000000') > int2( |
functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
1 | > import functools |
所以,functools.partial可以把一个参数多的函数变成一个参数少的新函数,少的参数需要在创建时指定默认值,这样,新函数调用的难度就降低了。
例程:用functools.partial简化自定义排序函数
1 | import functools |