在 Python 中函数(function)是一等对象(first-class objects)。编程语言理论学家把一等对象定义为满足下述条件的程序实体:
- 运行时创建
- 能赋值给变量或者数据结构中的元素
- 能作为参数传递给函数
- 能作为函数的返回结果
在 Python 中,整数、字符串和字典等都是一等对象。Python 中所有函数都是一等对象。
5.1 把函数视为对象
Python 函数是对象,可以通过以下示例证明(函数对象本身是 function 类的实例)。
def factorial(n):
'''return n!'''
return 1 if n < 2 else n * factorial(n-1)
print(factorial(42))
print(factorial.__doc__) # __doc__ 用于生成对象的帮助文本。
print(type(factorial)) # factorial 是 function 类的一个实例
1405006117752879898543142606244511569936384000000000
return n!
<class 'function'>
- 可以将函数赋值给变量,然后通过变量名调用
- 函数可以为参数传递给高阶函数(如map函数)
fact = factorial
print(fact)
print(fact(5))
print(map(factorial, range(11)))
"""
map(function, iterable, ...)
以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。
"""
<function factorial at 0x0000020FF3E2EA68>
120
<map object at 0x0000020FF32B9DC8>
通过一等函数就可以进行函数式风格编程(programming in a functional style)。
函数式风格编程
假如要实现二叉树镜像反转
命令式编程:首先判断节点是否为空;然后翻转左树;然后翻转右树;最后左右互换。命令式编程的理论模型——图灵机的特点:一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。
函数式风格编程:所谓“翻转二叉树”,可以看做是要得到一颗和原来二叉树对称的新二叉树。对一颗二叉树而言,它的镜像树就是左右节点递归镜像的树。通过描述一个 旧树->新树 的映射,而不是描述「从旧树得到新树应该怎样做」来达到目的。
函数式风格编程优点:减少代码量、增加复用(函数式的代码是“对映射的描述”,除了描述二叉树这样的数据结构,任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射。
5.2 高阶函数
高阶函数(higher-order function):接受函数为参数,或者把函数作为结果返回的函数就是高阶函数。如:map、sorted 等内置函数。
函数式编程范式中,最为人熟知的是 map、filter、reduce 。map 和 filter 由于列表推导式和生成器表达式的出现变得没有那么重要了,通常列表推导式和生成器表达式更易读,也更推荐使用。
- reduce 函数(Python 2 中是内置函数,Python 3 中被放入了 functools 模块,常用于求和)
- sum函数,相比reduce函数,内置函数sum 在可读性和性能上做了改善
from functools import reduce
from operator import add
print(reduce(add, range(100)))
print(sum(range(100)))
4950
4950
- all 函数(内置归约函数),用于判断给定的可迭代参数 iterable 中的所有元素是否都为 TRUE,如果是返回 True,否则返回 False。元素除了是 0、空、None、False 外都算 True。
- any函数(内置归约函数),用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。元素除了是 0、空、FALSE 外都算 TRUE。
def any(iterable): # any函数等价于
for element in iterable:
if element:
return True
return False
5.3 匿名函数
Python 中匿名函数是通过关键字 lambda 创建。由于 Python 的简单句法限制了 lambda 函数的定义提只能使用纯表达式。换而言之,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等 Python 语句。
lambda 函数只是语法糖:与def语句一样,lambda 表达式会创建函数对象。
匿名函数最适合使用在参数列表中。它除了作为参数传给高阶函数之外,Python 很少使用匿名函数。匿名函数可读性差,且不好写。
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=lambda word: word[::-1])) # 使用lambda表达式反转拼写,然后依此给单词列表排序
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
5.4 七大可调用对象
可调用对象是指该对象可以调用运算符(即())。
- 用户自定义的函数:用 def 语句或 lambda 表达式创建。
- 内置函数:用 C 语言实现的函数,如 len 或 time.strftime。
- 内置方法:使用 C 语言实现的方法,如 dict.get。
- 方法:在类的定义体中定义的函数。
- 类:调用类时会运行类的 __new__ 方法创建一个实例,然后运行 __init__ 方法初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。
- 类的实例:如果类定义了 __call__ 方法,那么它的实例可以当做函数调用。
- 生成器函数:使用 yield 关键字的函数或方法,调用生成器函数返回的是生成器对象。
- Python 中可以用内置函数 callable() 判断对象能否调用。
5.5 用户定义的可调用类型
任何 Python 对象都可以表现得像函数,只要实现实例方法 __call__。
# bingocall.py:调用 BingoCage实例,从打乱的列表中取出一个元素
import random
class BingoCage(object):
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexErrror:
raise LookupError('pick from empty BingoCage')
def __call__(self):
return self.pick()
bingo = BingoCage(range(3))
print(bingo.pick())
print(bingo())
print(callable(bingo))
0
2
True
5.6 函数内省
可以用 dir()函数查看函数对象的属性。
# 列出常规对象没有而函数专有的属性
class C(object):
pass
def func():
pass
obj = C()
print(sorted(set(dir(func)) - set(dir(obj))))
5.7 从位置参数到仅限关键字参数
略
5.8 获取关于参数的信息
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
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()
print(clip.__defaults__)
print(clip.__code__)
print(clip.__code__.co_varnames)
print(clip.__code__.co_argcount)
(80,)
<code object clip at 0x0000020FF3AEC660, file "<ipython-input-21-164c7f53ec1b>", line 1>
('text', 'max_len', 'end', 'space_before', 'space_after')
2
使用 inspect 模块更好的查看函数的信息
from inspect import signature
sig =signature(clip)
print(sig)
print(str(sig))
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
(text, max_len=80)
(text, max_len=80)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80
5.9 函数注解
Python 3 中可以为函数声明中的参数和返回值附加元数据。函数声明中的各个参数可以在 : 之后增加注解表达式(annotation expression),如果参数有表达式,注解放在参数名和 = 之间,如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 ->和一个表达式。表达式可以是任何类型,注解中最常用的类型是类(如 str 或 int)和字符串(如 ‘int > 0’)。
注解不会做任何处理,只是存储在函数的 __annotations__ 属性(一个字典)中。Python 对注解所做的唯一事是把他们存储在函数的 __annotations__属性里。Python 不做检查、不做强制、不做验证,什么操作都不做。也就是说注解对于 Python 解释器没有任何意义。不过,注解可以供 IDE、框架和装饰器等工具使用,为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。
# 有注解的 clip 函数
def clip(text:str, max_len:'int > 0'=80) -> str:
"""在max_len前面或者后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()
print(clip.__annotations__)
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}
5.10 支持函数式编程的包
operator 和 functools 等包的支持使得 Python 函数式编程风格也可以信手拈来。
operator 模块为多个算术运算提供了对应的函数,从而避免了编写 lambda a, b: a*b 这种匿名函数。其中的 itemgetter 和 attrgetter 函数会自行构建函数,从序列中取出元素或者读取对象属性。如:itemgetter(1) 的作用与 lambda fields: fields[1] 一样。
functools 模块提供了一系列高阶函数,如 reduce。此外还有一个非常有用的函数 partial 及其变体,partialmethod。
简单的说 functools.partial 是一个高阶函数,它的作用是把一个函数的一些参数固定,并返回一个新的函数对象。这样做的好处是,参数更少,调用更加方便。如下面这个例子:
# 使用 partial 把两个参数函数改编成需要单参数的函数并返回,原函数不改变。
from operator import mul
from functools import partial
triple = partial(mul, 3) # 固定一个参数为3,并返回新的改编后的函数
print(triple(7)) # 即计算 3*7
print(list(map(triple, range(1, 10))))
print(mul(4, 5)) # 原函数不受影响
21
[3, 6, 9, 12, 15, 18, 21, 24, 27]
20