以前知道廖前辈的这个网站,但是今天是第一次拜读,我是通过《Python核心编程(第三版)》这本书入门Python的,感觉廖前辈的教程更加容易,总结的也很到位。下面记录的是此番学习Python时带给我的领悟,以及一些值得关注的东西。我将它们写入博客《廖雪峰Python的研读笔记》系列。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
高阶函数英文叫Higher-order function。一个函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。
Python内建了map()
和reduce()
函数。
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算。
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
这里给出一个综合的例子,使用map/reduce实现类似int()
函数的功能:
from functools import reduce
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
return reduce(fn, map(char2num, s))
练习
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']。
'''
def normalize(name):
L = name[0].upper()
L = L + name[1:].lower()
return L
if __name__ == '__main__':
L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
Python提供的sum()函数可以接受一个list并求和,请编写一个prod()函数,可以接受一个list并利用reduce()求积。
'''
def prod(L):
return reduce(lambda x, y: x * y, L)
if __name__ == '__main__':
print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
print('3 * 5 =', prod([3, 5]))
print('3 =', prod([3]))
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
利用map和reduce编写一个str2float函数,把字符串'123.456'转换成浮点数123.456。
'''
def str2float(s):
CH2INT = {
'1' : 1,
'2' : 2,
'3' : 3,
'4' : 4,
'5' : 5,
'6' : 6,
'7' : 7,
'8' : 8,
'9' : 9,
'0' : 0,
'.' : -1,
}
str2float.point_factor = 0 # Python2中使用这样的方式实现nonlocal功能
def to_float(a, b):
if a == -1: # 特殊处理以'.'开头的字符串
str2float.point_factor = 0.1
return b * str2float.point_factor
if b == -1: # 处理过程中遇到'.'的情况
str2float.point_factor = 1
return a
if str2float.point_factor == 0:
return a * 10 + b # 整数部分处理方法
else:
str2float.point_factor = str2float.point_factor * 0.1
return a + b * str2float.point_factor
return reduce(to_float, map(lambda x: CH2INT[x], s))
if __name__ == '__main__':
TEST_LIST = [
'0',
'123.456',
'123.456000',
'0.1234',
'.1234',
'120.0034',
]
for i in TEST_LIST:
print 'str2float(\'%s\') = %s' % (i, str2float(i))
Python内建的filter()
函数用于过滤序列。filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
filter()
函数返回的是一个Iterator
,使用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
练习
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()滤掉非回数。
'''
def is_palindrome(n):
s = str(n)
l = len(s)
i = 0
while i < l / 2:
if s[i] != s[-1 - i]:
return False
i = i + 1
return True
if __name__ == '__main__':
output = filter(is_palindrome, range(1, 10000))
print(list(output))
Python内置的sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序。
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
由此可见,用sorted()
排序的关键在于实现一个映射函数。
练习
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
假设我们用一组tuple表示学生名字和成绩:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
请用sorted()对上述列表分别按名字排序,再按成绩从高到低排序。
'''
if __name__ == '__main__':
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
print sorted(L, key = lambda x : x[0])
print sorted(L, key = lambda x : x[1], reverse = True)
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数,而调用函数f
时,才真正计算求和的结果:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
.sum at 0x101c6ed90>
>>> f()
25
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
f1()
和f2()
的调用结果互不影响。
闭包
注意到返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
小结
一个函数可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便,且不用担心函数名冲突。
关键字lambda
表示匿名函数,冒号前面的部分表示函数参数,后面的部分表示计算公式,也就是函数体。
此外,lambda
表达式返回一个函数,匿名函数也是函数对象,可以将其保存在变量中或作为返回值返回:
>>> f = lambda x: x * x
>>> f
lambda> at 0x101c6ef28>
>>> f(5)
25
def build(x, y):
return lambda: x * x + y * y
小结
Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。
假设我们要增强函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
不带参数的装饰器
本质上,decorator
就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator
,可以定义如下:
def log(func):
def wrapper(*args, **kw):
print('start calling %s():' % func.__name__)
return func(*args, **kw)
return wrapper
观察上面的log
,因为它是一个decorator
,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@
语法,把decorator
置于函数的定义处:
@log
def my_add(x, y):
return x + y
把@log
放到my_add()
函数的定义处,相当于执行了语句my_add = log(my_add)
。
由于log()
是一个decorator
,返回一个函数,所以,原来的my_add()
函数仍然存在,只是现在同名的my_add
变量指向了新的函数,于是调用my_add()
将执行新函数,即在log()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。
带参数的装饰器
如果decorator
本身需要传入参数,那就需要编写一个返回decorator
的高阶函数,写出来会更复杂。比如,要自定义log
的文本:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
这个3层嵌套的decorator
用法如下:
@log('start executing')
def my_add(x, y):
return x + y
和两层嵌套的decorator
相比,3层嵌套相当于my_add = log('start executing')(my_add)
。
剖析上面的语句,首先执行log('start executing')
,返回的是decorator
函数,再调用返回的函数,参数是my_add
函数,返回值最终是wrapper
函数。
执行结果:
>>> my_add(1, 2)
start executing my_add():
3
还原函数名称
以上两种decorator
的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator
装饰之后的函数,它们的__name__
已经从原来的’my_add’变成了’wrapper’:
>>> my_add.__name__
'wrapper'
因为返回的那个wrapper()
函数名字就是’wrapper’,所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的。
所以,一个完整的decorator
的写法如下:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('start calling %s():' % func.__name__)
return func(*args, **kw)
return wrapper
或者针对带参数的decorator
:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
小结
在面向对象(OOP)的设计模式中,decorator
被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator
外,直接从语法层次支持decorator
。Python的decorator
可以用函数实现,也可以用类实现。
decorator
可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。
练习
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
请编写一个decorator,能在函数调用的前后打印出'begin call'和'end call'的日志,并计算函数执行时间。
'''
import time
from functools import wraps
import random
def fn_timer(func):
@wraps(func)
def func_timer(*args, **kwargs):
func_name = func.__name__
print 'begin call %s()' % func_name
t0 = time.time()
result = func(*args, **kwargs)
t1 = time.time()
print 'end call %s()' % func_name
print "Total time running %s: %s seconds" % (func_name, str(t1-t0))
return result
return func_timer
@fn_timer
def random_sort(n):
return sorted([random.random() for i in range(n)])
if __name__ == "__main__":
random_sort(2000000)
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。
functools.partial
可以帮我们创建一个偏函数的,从而把一个函数的某些参数固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
上面新的int2
函数,仅仅是把base
参数重新设定默认值为2
,但也可以在函数调用时传入其他值:
>>> int2('1000000', base=10)
1000000
对于可以接收*args
参数的函数来说,比如max
,当我们使用max2 = functools.partial(max, 10)
定义新函数max2
后,10
会作为*args
的一部分自动加到左边,这样当调用max2(5, 6, 7)
时,函数将返回最大值10
。
小结
当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。