在 Python 中,函数是一等对象
。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
在运行时创建 能赋值给变量或数据结构中的元素 能作为参数传给函数 能作为函数的返回结果 在 Python 中,整数、字符串和字典都是一等对象——没什么特别的。如果在 Python 之前,你使用的语言并未把函数当作一等公民,那么本章以及第三部分余下的内容将重点讨论把函数作为对象的影响和实际应用。
1. 把函数视作对象
创建了一个函数,然后调用它,读取它的 __doc__
属性,并且确定函数对象本身是 function 类的实例。
"""
n阶递归函数
"""
def factorial ( n) :
""":return n!"""
return 1 if n < 2 else n * factorial( n - 1 )
if __name__ == '__main__' :
print ( factorial( 42 ) )
print ( factorial. __doc__)
print ( type ( factorial) )
示例说明 factorial
是 function
类的实例。延续上例继续说明展示函数的一等
本性:
'''
我们可以把 factorial 函数赋值给变量 fact,然后通过变量名调用。
我们还能把它作为参数传给map 函数。map 函数返回一个可迭代对象,
里面的元素是把第一个参数(一个函数)应用到第二个参数(一个可迭代对象,
这里是range(11))中各个元素上得到的结果。
'''
fact = factorial
print ( fact)
print ( fact( 5 ) )
print ( map ( fact, range ( 11 ) ) )
print ( list ( map ( fact, range ( 11 ) ) ) )
有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数。
2. 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数
(higher-order function)。map 函数就是一例。此外,内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序。例如,若想根据单词的长度排序,只需把 len 函数传给 key 参数:
"""
根据单词长度给一个列表排序
"""
if __name__ == '__main__' :
fruits = [ 'strawberry' , 'fig' , 'apple' , 'cherry' , 'raspberry' , 'banana' ]
print ( sorted ( fruits, key= len ) )
任何单参数函数都能作为 key 参数的值。例如,为了创建押韵词典,可以把各个单词反过来拼写,然后排序:
'''
注意:列表里的单词没有变,我们只是把反向拼写当作排序条件
'''
def reversed ( word) :
return word[ : : - 1 ]
if __name__ == '__main__' :
fruits = [ 'strawberry' , 'fig' , 'apple' , 'cherry' , 'raspberry' , 'banana' ]
print ( reversed ( "testing" ) )
print ( sorted ( fruits, key= reversed ) )
在函数式编程范式中,最为人熟知的高阶函数有map、filter、reduce。不过多数使用场景下都有更好的替代品。 map、filter和reduce的现代替代品:在 Python 3 中,map 和 filter 还是内置函数,但是由于引入了列表推导
和生成器表达式
,它们变得没那么重要了。列表推导或生成器表达式具有 map 和 filter 两个函数的功能,而且更易于阅读:
'''
计算阶乘列表:map 和 filter 与列表推导比较
'''
def fact ( n) :
return 1 if n < 2 else n * fact( n - 1 )
if __name__ == '__main__' :
print ( list ( map ( fact, range ( 6 ) ) ) )
print ( [ fact( n) for n in range ( 6 ) ] )
print ( list ( map ( fact, filter ( lambda n: n % 2 , range ( 6 ) ) ) ) )
print ( [ fact( n) for n in range ( 6 ) if n % 2 ] )
示例说明使用列表推导式替换 map 和 filter 后代码变得更加易于理解和阅读。 在 Python 2 中,reduce 是内置函数,但是在 Python 3 中放到functools 模块里了。这个函数最常用于求和,自 2003 年发布的Python 2.3 开始,最好使用内置的 sum 函数。在可读性和性能方面,这是一项重大改善:
"""
使用 reduce 和 sum 计算 0~99 之和
"""
from functools import reduce
from operator import add
if __name__ == '__main__' :
print ( reduce ( add, range ( 100 ) ) )
print ( sum ( range ( 100 ) ) )
all 和 any 也是内置的归约函数。 all(iterable)
如果 iterable 的每个元素都是真值,返回 True;all([]) 返回 True。any(iterable)
只要 iterable 中有元素是真值,就返回 True;any([]) 返回False。为了使用高阶函数,有时创建一次性的小型函数更便利。这便是匿名函数存在的原因。
3. 匿名函数
lambda 关键字在 Python 表达式内创建匿名函数。
然而,Python 简单的句法限制了 lambda 函数的定义体只能使用纯表达式。换句话说,lambda 函数的定义体中不能赋值,也不能使用 while和 try 等 Python 语句。在参数列表中最适合使用匿名函数
。示例如下:
'''
使用 lambda 表达式反转拼写,然后依此给单词列表排序
使用 lambda 表达式重写了上面示例中排序押韵单词的示例,这样就省掉了 reverse 函数。
'''
if __name__ == '__main__' :
fruits = [ 'strawberry' , 'fig' , 'apple' , 'cherry' , 'raspberry' , 'banana' ]
print ( sorted ( fruits, key= lambda word: word[ : : - 1 ] ) )
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。
由于句法上的限制,非平凡的 lambda 表达式要么难以阅读,要么无法写出。lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。这是 Python 中几种可调用对象的一种。下一节会说明所有可调用对象。
4. 可调用对象
除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable()
函数。Python 数据模型文档列出了 7 种可调用对象。
用户定义的函数
:使用 def 语句或 lambda 表达式创建。内置函数
:使用 C 语言(CPython)实现的函数,如 len 或time.strftime。内置方法
:使用 C 语言实现的方法,如 dict.get。方法
:在类的定义体中定义的函数。类
:调用类时会运行类的 __new__
方法创建一个实例,然后运行__init__
方法,初始化实例,最后把实例返回给调用方。因为 Python没有 new 运算符,所以调用类相当于调用函数。类的实例
:如果类定义了__call__
方法,那么它的实例可以作为函数调用。生成器函数
:使用 yield
关键字的函数或方法。调用生成器函数返回的是生成器对象。
Python 中有各种各样可调用的类型,因此判断对象能否调用,最安全的方法是使用内置的 callable()
函数:
if __name__ == '__main__' :
print ( [ callable ( obj) for obj in ( abs , str , 13 ) ] )
5. 用户定义的可调用类型
不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函数。为此,只需实现实例方法 __call__
。如下示例:
import random
class BingoCage :
def __init__ ( self, items) :
self. _items = list ( items)
random. shuffle( self. _items)
def pick ( self) :
try :
return self. _items. pop( )
except IndexError:
raise LookupError( 'pick from empty BingoCage' )
def __call__ ( self, * args, ** kwargs) :
return self. pick( )
if __name__ == '__main__' :
bingo = BingoCage( range ( 3 ) )
print ( bingo( ) )
print ( bingo. pick( ) )
print ( callable ( bingo) )
实现 __call__方法的类是创建函数类对象的简便方式
,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间“记住”某些事 [ 例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ]。下面讨论把函数视作对象处理的另一方面:运行时内省
。
6. 函数内省
除了 __doc__
,函数对象还有很多属性。使用 dir 函数可以探知函数的所有属性。本节讨论与把函数视作对象相关的几个属性,下面重点说明函数专有而用户定义的一般对象没有的属性。计算两个属性集合的差集便能得到函数专有属性列表:
class C :
pass
def func ( ) :
pass
if __name__ == '__main__' :
obj = C( )
print ( sorted ( set ( dir ( func) ) - set ( dir ( obj) ) ) )
名称 类型 说明 __annotations__
dict 参数和返回值的注解 __call__
method_ wrapper 实现()运算符:即可调用函数对象协议 __closure__
tuple 函数闭包,即自由变量的绑定(通常是 None) __code__
code 编译成字节码的函数元数据和函数定义体 __defaults__
tuple 形式参数的默认值 __get__
method_wrapper 实现只读描述符协议 __globals__
dict 函数所在模块中的全局变量 __kwdefaults__
dict 仅限关键字形式参数的默认值 __name__
str 函数名称 __qualname__
str 函数的限定名称,如 Random.choice
为了深入了解这些属性,我们要先探讨 Python 为声明函数形参和传入实参所提供的强大句法。
7. 从定位参数到仅限关键字参数
Python 最好的特性之一是提供了极为灵活的参数处理机制,而且 Python3 进一步提供了仅限关键字参数(keyword-only argument)
。与之密切相关的是,调用函数时使用 * 和 **“展开”可迭代对象,映射到单个参数。如下示例:
'''
tag 函数用于生成 HTML 标签;使用名为 cls 的关键字参数传入“class”属性,
这是一种变通方法,因为“ class”是 Python的关键字
'''
def tag ( name, * content, cls= None , ** attrs) :
"""生成一个或多个html标签"""
if cls is not None :
attrs[ 'class' ] = cls
if attrs:
attr_str = '' . join( ' %s=%s' % ( attr, value) for attr, value in sorted ( attrs. items( ) ) )
else :
attr_str = ''
if content:
return "\n" . join( '<%s%s>%s</%s>' % ( name, attr_str, c, name) for c in content)
else :
return '<%s%s />' % ( name, attr_str)
if __name__ == '__main__' :
print ( tag( 'br' ) )
print ( tag( 'p' , 'hello' ) )
print ( tag( 'p' , 'hello' , 'world' ) )
print ( tag( 'p' , 'hello' , id = 33 ) )
print ( tag( 'p' , 'hello' , 'world' , cls= "sidebar" ) )
print ( tag( content= "testing" , name= "img" ) )
kwargs = { 'name' : 'img' , 'title' : 'Sunset Boulevard' , 'src' : 'sunset.jpg' , 'cls' : 'framed' }
print ( tag( ** kwargs) )
仅限关键字参数
是 Python 3 新增的特性。在上例中,cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面
。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个 *,如下所示:
def f ( a, * , b) :
return a, b
print ( f( 1 , b= 2 ) )
注意,仅限关键字参数不一定要有默认值,可以像上例中 b 那样,强制必须传入实参。 下面说明函数参数的内省。
8. 获取关于参数的信息
HTTP 微框架 Bobo 中有个使用函数内省的好例子。下例是对 Bobo教程中“Hello world”应用的改编,说明了内省怎么使用。
'''
Bobo 知道 hello 需要 person 参数,并且从 HTTP 请求中获取它
'''
import bobo
@bobo. query( '/' )
def hello ( person) :
return 'Hello {}' . format ( person)
bobo.query 装饰器把一个普通的函数(如 hello)与框架的请求处理机制集成起来了。这里的关键是,Bobo 会内省 hello 函数,发现它需要一个名为person 的参数,然后从请求中获取那个名称对应的参数,将其传给hello 函数,因此程序根本不用触碰请求对象。 执行 bobo -f hello.py
命令,访问http://localhost:8080/
看到的消息是Missing form variable person
,HTTP 状态码是 403: 这是因为,Bobo 知道调用 hello 函数必须传入 person 参数,但是在请求中找不到同名参数。 传入person参数示例: Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?函数对象有个 __defaults__
属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在__kwdefaults__
属性中。然而,参数的名称在__code__
属性中,它的值是一个 code 对象引用,自身也有很多属性。 为了说明这些属性的用途,下面在 clip.py 模块中定义 clip 函数:
def clip ( text, max_len= 80 ) :
"""在max_len前面或后面的第一个空格处截断文本"""
end = None
if len ( text) > max_len:
space_before = text. rfind( ' ' , 0 , max_len)
if space_before >= 0 :
end = space_before
space_after = text. rfind( ' ' , max_len)
if space_after >= 0 :
end = space_after
if end is None :
end = len ( text)
return text[ : end] . rstrip( )
if __name__ == '__main__' :
print ( clip. __defaults__)
print ( clip. __code__)
print ( clip. __code__. co_varnames)
print ( clip. __code__. co_argcount)
可以看出,这种组织信息的方式并不是最便利的。参数名称在__code__.co_varnames
中,不过里面还有函数定义体中创建的局部变量。因此,参数名称是前 N 个字符串,N 的值由__code__.co_argcount
确定。这里不包含前缀为 *或 ** 的变长参数。参数的默认值只能通过它们在 __defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。clip 函数有两个参数,text 和 max_len,其中一个有默认值,即 80,因此它必然属于最后一个参数,即 max_len。这有违常理。 我们有更好的方式——使用 inspect
模块。
from inspect import signature
from clip import clip
if __name__ == '__main__' :
sig = signature( clip)
print ( sig)
for name, param in sig. parameters. items( ) :
print ( param. kind, ':' , name, '= ' , param. default)
这样看起来就好多了。inspect.signature
函数返回一个inspect.Signature 对象, 它有一个parameters
属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty
值表示没有默认值。考虑到 None 是有效的默认值
(也经常这么做),而且这么做是合理的。 除了 name、default 和 kind,inspect.Parameter 对象还有一个annotation
(注解)属性,它的值通常是 inspect._empty,但是可能包含 Python 3 新的注解句法提供的函数签名元数据。 inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑 定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。
框架可以使用这个方法在真正调用函数前验证参数。示例如下:
from inspect import signature
from tag import tag
if __name__ == '__main__' :
sig = signature( tag)
my_tag = { 'name' : 'img' , 'title' : 'Sunset Boulevard' , 'src' : 'sunset.jpg' , 'cls' : 'framed' }
bound_args = sig. bind( ** my_tag)
print ( bound_args)
for name, value in bound_args. arguments. items( ) :
print ( name, '=' , value)
del my_tag[ 'name' ]
try :
bound_args = sig. bind( ** my_tag)
print ( bound_args)
except TypeError as e:
print ( e)
这个示例在 inspect 模块的帮助下,展示了 Python 数据模型把实参绑定给函数调用中的形参的机制,这与解释器使用的机制相同。 框架和 IDE 等工具可以使用这些信息验证代码。Python 3 的另一个特性——函数注解
——增进了这些信息的用途
9. 函数注解
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据
。修改前面的clip函数,让其支持支持注解:
def clip ( text: str , max_len: 'int > 0' = 80 ) - > str :
函数声明中的各个参数可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。那个表达式可以是任何类型。注解中最常用的类型是类(如 str 或 int)和字符串(如 ‘int >0’)。 注解不会做任何处理,只是存储在函数的 __annotations__ 属性(一个字典)中
:
print ( clip. __annotations__)
Python 对注解所做的唯一的事情是,把它们存储在函数的__annotations__
属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。 inspect.signature函数可以直接查看参数以及注解。示例如下:
sig = signature( clip)
print ( sig)
这样在框架中就可以提取绑定想要的参数,并获取相应的注解。 函数注解的最大影响或许不是让 Bobo、flask 等框架自动设置,而是为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。 深入分析函数之后,本章余下的内容介绍标准库中为函数式编程提供支持的常用包。
10. 支持函数式编程的包。
虽然 Guido 明确表明,Python 的目标不是变成函数式编程语言,但是得益于 operator
和 functools
等包的支持,函数式编程风格也可以信手拈来。
10.1 operator模块
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。我们可以使用 reduce 函数,但是需要一个函数计算序列中两个元素之积。示例 展示如何使用 lambda 表达式解决这个问题。
from functools import reduce
'''
使用 reduce 函数和一个匿名函数计算阶乘
'''
def fact ( n) :
"""n!"""
return reduce ( lambda a, b: a * b, range ( 1 , n + 1 ) )
operator 模块为多个算术运算符提供了对应的函数,从而避免编写lambda a, b: a*b 这种普通的匿名函数。如下示例:
from functools import reduce
from operator import mul
def fact ( n) :
"""n!"""
return reduce ( mul, range ( 1 , n + 1 ) )
operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式:因此,itemgetter
和 attrgetter
其实会自行构建函数。如下示例:
from operator import itemgetter
'''
使用 itemgetter 排序一个元组列表。
itemgetter(1) 的作用与 lambdafields: fields[1] 一样:
创建一个接受集合的函数,返回索引位 1上的元素。
'''
if __name__ == '__main__' :
metro_data = [
( 'Tokyo' , 'JP' , 36.933 , ( 35.689722 , 139.691667 ) ) ,
( 'Delhi NCR' , 'IN' , 21.935 , ( 28.613889 , 77.208889 ) ) ,
( 'Mexico City' , 'MX' , 20.142 , ( 19.433333 , - 99.133333 ) ) ,
( 'New York-Newark' , 'US' , 20.104 , ( 40.808611 , - 74.020386 ) ) ,
( 'Sao Paulo' , 'BR' , 19.649 , ( - 23.547778 , - 46.635833 ) ) ,
]
for city in sorted ( metro_data, key= itemgetter( 1 ) ) :
print ( city)
如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组:
cc_name = itemgetter( 1 , 0 )
for city in metro_data:
print ( cc_name( city) )
itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现 __getitem__
方法的类。 attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。此外,如果参数名中包含 .(点号),attrgetter会深入嵌套对象,获取指定的属性。
示例:
from collections import namedtuple
from operator import attrgetter
if __name__ == '__main__' :
metro_data = [
( 'Tokyo' , 'JP' , 36.933 , ( 35.689722 , 139.691667 ) ) ,
( 'Delhi NCR' , 'IN' , 21.935 , ( 28.613889 , 77.208889 ) ) ,
( 'Mexico City' , 'MX' , 20.142 , ( 19.433333 , - 99.133333 ) ) ,
( 'New York-Newark' , 'US' , 20.104 , ( 40.808611 , - 74.020386 ) ) ,
( 'Sao Paulo' , 'BR' , 19.649 , ( - 23.547778 , - 46.635833 ) ) ,
]
lat_long = namedtuple( "LatLong" , 'lat long' )
Metropolis = namedtuple( "Metropolis" , 'name cc pop coord' )
metro_areas = [ Metropolis( name, cc, pop, lat_long( lat, long ) ) for name, cc, pop, ( lat, long ) in metro_data]
print ( metro_areas[ 0 ] )
print ( metro_areas[ 0 ] . coord. lat)
name_lat = attrgetter( 'name' , 'coord.lat' )
for city in sorted ( metro_areas, key= attrgetter( 'coord.lat' ) ) :
print ( name_lat( city) )
在 operator 模块余下的函数中,我们最后介绍一下methodcaller
。它的作用与 attrgetter 和 itemgetter 类似,它会自行创建函数。methodcaller 创建的函数会在对象上调用参数指定的方法
。示例如下:
from operator import methodcaller
if __name__ == '__main__' :
s = 'the time has come'
upcase = methodcaller( 'upper' )
hiphenate = methodcaller( 'replace' , ' ' , '-' )
print ( upcase( s) )
print ( hiphenate( s) )
结果表明,methodcaller 可以冻结某些参数,也就是部分应用(partial application),这与functools.partial 函数的作用类似。
10.2 使用functools.partial冻结参数
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少。示例:
from operator import mul
from functools import partial
'''
使用 partial 把一个两参数函数改编成需要单参数的可调用对象
'''
if __name__ == '__main__' :
triple = partial( mul, 3 )
print ( triple( 5 ) )
print ( list ( map ( triple, range ( 1 , 10 ) ) ) )
把 partial 应用到之前定义的tag函数,冻结一个定位参数和一个关键字参数:
picture = partial( tag, 'img' , cls= 'pic-frame' )
print ( tag)
print ( picture( src= "xxx.jpg" ) )
print ( picture)
print ( picture. func)
print ( picture. args)
print ( picture. keywords)
小结
本章的目标是探讨 Python 函数的一等本性。这意味着,我们可以把函数赋值给变量、传给其他函数、存储在数据结构中,以及访问函数的属性,供框架和一些工具使用。高阶函数是函数式编程的重要组成部分,即使现在不像以前那样经常使用 map、filter 和 reduce 函数了,但是还有列表推导以及 sum、all和 any 等内置的归约函数。 Python 有 7 种可调用对象,从 lambda 表达式创建的简单函数到实现__call__
方法的类实例。这些可调用对象都能通过内置的callable() 函数检测。每一种可调用对象都支持使用相同的丰富句法声明形式参数,包括仅限关键字参数和注解。 Python 函数及其注解有丰富的属性,在 inspect 模块的帮助下,可以读取它们。例如,Signature.bind 方法使用灵活的规则把实参绑定到形参上,这与 Python 使用的规则一样。 最后,本章介绍了 operator 模块中的一些函数,以及functools.partial 函数,有了这些函数,函数式编程就不太需要功能有限的 lambda 表达式了。