3-4-函数的高级话题
1. 函数设计概念
通用指导方针:
- 耦合性:对于输入使用参数并且对于输出使用 return 语句。
- 耦合性:只有在真正必要的情况下使用全局变量。
- 耦合性:不要改变可变类型的参数,除非调用者希望这样做。
- 聚合性:每一个函数都应该有一个单一的、统一的目标。
- 大小:每一个函数应该相对较小。
- 耦合性:避免直接改变在另一个模块文件中的变量。
2. 递归函数
直接或间接地调用自身以进行循环的函数。它允许程序遍历拥有任意的、不可预知的形状的结构。
递归在大多数情况下不必使用,很可能递归在内存空间和执行时间方面效率较低。
2.1 用递归求和
def mysum(L):
if not L:
return 0
else:
return L[0] + mysum(L[1:])
mysum([1, 2, 3, 4, 5])
15
2.2 处理任意结构
递归可以要求遍历任意形状的结构,考虑计算一个嵌套的子列表结构中所有数字的总和:[1, [2, [3, 4], 5], 6, [7, 8]]
def sumtree(L):
tot = 0
for x in L: # 这一级的每一项
if not isinstance(x, list):
tot += x # 如果不是列表,直接加和
else:
tot += sumtree(x) # 对子列表执行递归
return tot
L = [1, [2, [3, 4], 5], 6, [7, 8]]
print(sumtree(L))
36
3. 函数对象:属性和注解
3.1 间接函数调用
函数对象可以赋值给其他的名字、传递给其他函数、嵌入到数据结构、从一个函数返回给另一个函数等等。
函数对象还恰好支持一个特殊操作:它们可以由一个函数表达式后面的括号中的列表参数调用。
在 def 运行之后,函数名直接是一个对象的引用——我们可以自由地把这个对象赋给其他的名称并且通过任何引用调用它:
def echo(message): # echo 名称赋值给函数对象
print(message)
echo('Direct call') # 通过初始名称调用对象
Direct call
x = echo # x 也引用 echo 函数
x('Indirect call') # 通过在 x 后面添加 () 调用对象
Indirect call
由于参数通过赋值对象来传递,这就像是把函数作为参数传递给其他函数一样容易。随后,被调用者可能通过把参数添加到括号中来调用传入的函数:
def indirect(func, arg):
func(arg) # 通过添加 () 调用传递的对象
indirect(echo, 'Argument call!')
Argument call!
甚至可以把函数对象的内容填入到数据结构中,就好像它们是整数或字符串一样:
schedule = [(echo, 'Spam!'), (echo, 'Ham!')]
for (func, arg) in schedule:
func(arg)
Spam!
Ham!
3.2 函数内省
由于函数是对象,我们可以用常规的对象工具来处理函数,也可以通用地检查它们的属性:
def func(a):
b = 'spam'
return b * a
func.__name__
'func'
dir(func)
['__annotations__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
内省工具允许我们探索实现细节,例如,函数已经附加了代码对象,代码对象提供了函数的本地变量和参数等方面的细节:
func.__code__
<code object func at 0x000001A1989E1A50, file "<ipython-input-7-0066278b97df>", line 1>
dir(func.__code__)
['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'co_argcount',
'co_cellvars',
'co_code',
'co_consts',
'co_filename',
'co_firstlineno',
'co_flags',
'co_freevars',
'co_kwonlyargcount',
'co_lnotab',
'co_name',
'co_names',
'co_nlocals',
'co_stacksize',
'co_varnames']
func.__code__.co_varnames
('a', 'b')
func.__code__.co_argcount
1
3.3 函数属性
函数对象不仅限于前面列出的系统定义的属性,也可能向函数附加任意的用户定义的属性:
func
<function __main__.func>
func.count = 0
func.count += 1
func.count
1
func.handles = 'Button-Press'
func.handles
'Button-Press'
dir(func)
['__annotations__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'count',
'handles']
3.4 Python 3.X 中的函数注解
在 Python 3.X 中,也可以给函数对象附加注解信息——与函数的参数和结果相关的任意的用户定义的数据。
注解本身不做任何事情,完全是可选的,并且出现的时候只是直接附加到函数对象的 __annotations__
属性以供其他用户使用。
从语法上讲,函数注解编写在 def 头部行,就像与参数和返回值相关的任意表达式一样。对于参数,它们出现在紧随参数名之后的冒号之后;对于返回值,它们编写于紧跟在参数列表之后的一个 ->
之后。
def func(a: 'spam', b: (1, 10), c: float) -> int:
return a + b + c
func(1, 2, 3)
6
Python 将注解收集到字典中并且将它们附加给函数对象自身。
func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': float, 'return': int}
如果编写了注解,仍然可以对参数使用默认值:
def func(a: 'spam' = 4, b: (1, 10) = 5, c: float = 6) -> int:
return a + b + c
func(1, 2, 3)
6
3.5 Python 3.5 中引入的 Type Hints
函数注解的语义未定义,经验表明,大多数函数注解用于为函数参数和返回值提供类型提示。很明显,如果标准库包含类型注解的基本定义和工具,那么对 Python 用户来说将是有益的。因此 Python 3.5 中引入了 Type Hints,可以在注解中声明类型:
def greeting(name: str) -> any:
return 'Hello' + name
尽管在运行时可以使用 __annotations__
属性,但是不会进行自动类型检查。
4. 匿名函数:lambda
lambda 表达式创建了一个之后能够调用的函数,但是它返回了一个函数而不是将这个函数赋值给一个变量名。出于对可读性的要求,通常来说,最好避免使用嵌套的 lambda。
4.1 lambda 表达式
lambda 的一般形式是关键字 lambda,之后是一个或多个参数,紧跟的是一个冒号,之后是一个表达式:
lambda argument1, argument2, ... argumentN : expression using arguments
lambda 与 def 的一些不同之处让其在扮演特定的角色时很有用。
- **lambda 是一个表达式,而不是一个语句。**因为这一点,lambda 能够出现在 Python 语法不允许 def 出现的地方——例如,在一个列表常量中或者函数调用的参数中。lambda 返回了一个值(一个新的函数),可以选择性地赋值给一个变量名。
- **lambda 的主体是一个单个的表达式,而不是一个代码块。**它限制了程序的嵌套,lambda 是一个为编写简单的函数二设计的。
f = lambda x, y, z: x + y + z
f(2, 3, 4)
9
x = (lambda a='fee', b='fie', c='foe': a + b + c)
x('wee')
'weefiefoe'
在 lambda 主体中的代码像在 def 内的代码一样都遵循相同的作用域查找法则。
def knights():
title = 'Sir'
action = (lambda x: title + ' ' + x)
return action
act = knights()
act('robin')
'Sir robin'
4.2 为什么使用 lambda
lambda 起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的定义。
lambda 通常用来编写跳转表(jump table),也就是行为的列表或字典,能够按照需要执行相应的动作:
L = [lambda x: x ** 2,
lambda x: x ** 3,
lambda x: x ** 4]
for f in L:
print(f(2))
print(L[0](3))
4
8
16
9
多路分支
我们可以用 Python 中的字典或者其他数据结构来构建更多种类的行为表,从而做同样的事情:
key = 'got'
{'already': (lambda: 2 + 2),
'got': (lambda: 2 * 4),
'one': (lambda: 2 ** 6)}[key]()
8
通过键索引来取回其中一个函数,括号使取出的函数被调用,这样编写代码可以使字典成为更加通用的多路分支工具。
5. 函数式编程工具
5.1 在可迭代对象中映射函数:map
程序对列表和其他序列常常要做的一件事就是对每一个元素进行一个操作并把其结果集合起来。
map 函数会对一个序列对象中的每一个元素应用被传入的函数,并且返回一个包含了所有函数调用结果的一个列表:
counters = [1, 2, 3, 4]
def inc(x): return x + 10
list(map(inc, counters))
[11, 12, 13, 14]
由于 map 期待传入一个函数,它恰好是 lambda 通常出现的地方之一:
list(map((lambda x: x + 3), counters))
[4, 5, 6, 7]
map 还有更高级的使用方法。例如,提供了多个序列作为参数,它能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表。
list(map(pow, [1, 2, 3], [2, 3, 4])) # 1**2, 2**3, 3**4
[1, 8, 81]
map 调用类似于列表解析式:
[inc(x) for x in [1, 2, 3, 4]]
[11, 12, 13, 14]
在某些情况下,map 可能比列表解析式运行更快,编写的代码也可能更少,但是 map 对每一个元素都应用了函数调用而不是任意的表达式,因此它成为了不太通用的工具,经常需要额外的帮助函数或 lambda 表达式。
5.2 在可迭代对象中选择元素:filter
函数式编程就是对序列应用一些函数的工具,filter 基于某一测试函数过滤出一些元素,序列中的元素若其返回值为真的话,将会被键入到结果的列表中。例如,从一个序列中挑选出大于 0 的元素:
list(range(-5, 5))
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
list(filter((lambda x: x > 0), range(-5, 5)))
[1, 2, 3, 4]
filter 也可以通过列表解析式语法进行模拟:
[x for x in range(-5, 5) if x > 0]
[1, 2, 3, 4]
5.3 在可迭代对象中组合元素:reduce
reduce 在 Python 3.X 中位于 functools 模块中,它接受一个可迭代对象来处理,但是本身不是一个可迭代对象,它返回一个单个的结果。
from functools import reduce
reduce((lambda x, y: x + y), [1, 2, 3, 4])
10
reduce((lambda x, y: x * y), [1, 2, 3, 4])
24
每一步,reduce 传递了当前的和或乘积以及列表中下一个的元素,传给列出的 lambda 函数。
reduce 还允许一个可选的第三个参数放置于序列的各项之前,从而当序列为空时充当一个默认的结果。