函数设计概念
当开始使用函数时,就开始面对如何将组件聚合在一起的选择了。例如,如何将任务分解成为更有针对性的函数(导致了聚合性)、函数将如何通信(耦合性)等。
几点原则:
1.耦合性:只有在真正必要的情况下使用全局变量。全局变量通常是一种蹩脚的函数间进行通信的办法。它们引发了依赖关系和计时的问题,会导致程序调试和修改的困难
2.耦合性:不要改变可变类型的参数,除非调用者希望这样做。
3.聚合性:每一个函数都应该有一个单一的、统一的目标。在设计完美的情况下,每一个函数都应该做一件事:这件事可以用一个简单说明句来总结。如果这个句子很宽泛(例如,“这个函数实现了整个程序”),或者包含了很多的排比(例如,“这个函数让员工产生并提交了一个比萨订单”),你也许就应该想想是不是要将它分解成多个更简单的函数了。
4.大小:每一个函数应该相对较小。
5.耦合:避免直接改变在另一个模块文件中的变量
============================================================================
递归函数
递归函数即直接或间接地调用自身已进行循环的函数。
--------------------------------------------------------------------------------------------------------------------------
用递归求和
对一个数字列表求和,可以使用内置的sum函数,或者自己编写一个更加定制化的版本:
>>> def mysum(L):
if not L:
return 0
else:
return L[0] + mysum(L[1:])
>>> mysum([1,2,3,4])
10
在每一层中,这个函数都递归地调用自己来计算列表剩余的值的和,这个和随后加到前面的一项中。
当列表变为空的时候,递归循环结束并返回0。
--------------------------------------------------------------------------------------------------------------------------
编码替代方案
有趣的是,我们也可以使用Python的三元if/else表达式在这里保存某些代码资产。也可以使用Python3.0的扩展序列赋值来使得第一个/其他的解包更简单
>>> def mysum1(L):
return 0 if not L else L[0] +mysum1(L[1:])
>>> def mysum2(L):
return L[0] if len(L)==1 else L[0]+mysum2(L[1:])
>>> def mysum3(L):
first,*rest = L
return first if not rest else first+ mysum3(rest)
>>> L = [1,2,3,4]
>>> mysum1(L),mysum2(L),mysum3(L)
(10, 10, 10)
上述三个例子中的后面两个会由于空的列表而失败,这是需要注意的地方。
但考虑到支持+的任何对象类型的序列,而不只是数字,后面两种方法更有通用性。
>>> mysum1(['a','b','c','d'])
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
mysum1(['a','b','c','d'])
File "<pyshell#30>", line 2, in mysum1
return 0 if not L else L[0] +mysum1(L[1:])
File "<pyshell#30>", line 2, in mysum1
return 0 if not L else L[0] +mysum1(L[1:])
File "<pyshell#30>", line 2, in mysum1
return 0 if not L else L[0] +mysum1(L[1:])
File "<pyshell#30>", line 2, in mysum1
return 0 if not L else L[0] +mysum1(L[1:])
TypeError: Can't convert 'int' object to str implicitly
>>> mysum2(['a','b','c','d'])
'abcd'
>>> mysum3(['a','b','c','d'])
'abcd'
----------
---------------------
-------------------------------
-------------------------------
-----------------------------
循环语句VS递归
尽管递归对于上述的求和的例子有效,但在这种环境中,它可能过于追求技巧了。实际上,递归在Python中并不是很常用,因为Python更强调像循环这样的简单的过程式语句,循环语句通过更为自然。例如,while循环常常使得事情更为具体一下:
>>> L = [1,2,3,4,5]
>>> mysum = 0
>>> while L:
mysum+=L[0]
L = L[1:]
>>> mysum
15
更好的情况是,for循环为我们自动迭代,使得递归在大多数情况下不必使用(并且,很可能,递归在内存空间和执行时间方面效率较低)
>>> L = [1,2,3,4,5]
>>> mysum = 0
>>> for x in L:
mysum+=x
>>> mysum
15
----------
---------------------
-------------------------------
-------------------------------
-----------------------------
处理任意结构
另一方面,递归可以要求遍历任意形状的结构。作为递归在这种环境中的应用的一个简单例子,考虑像下面这样的一个任务:计算一个嵌套的子列表结构中所有数字的总和:
[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]]
>>> sumtree(L)
36
>>> sumtree([1,[2,[3,[4,[5,[6,[7,[8]]]]]]]])
36
>>> sumtree([[[[[[[[1],2],3],4],5],6],7],8])
36
留意这段脚本末尾的测试案例。尽管这个例子是人为编写的,它是更大的程序的代表,例如,继承树和模块导入链可以战术类似的通用结构。
============================================================================
函数对象:属性和注解
--------------------------------------------------------------------------------------------------------------------------
间接函数调用
由于Python函数时对象,我们可以编写通用的处理它们的程序。函数对象可以赋值给其他的名字、传递给其他函数、嵌入到数据结构、从一个函数返回给另一个函数,等等,就好像它们是简单的数字或字符串。
对于一条def语句中的名称,真的没有什么特别的:它们只是当前作用域中的一个变量赋值,就好像它出现在一个=符号的左边。在def运行之后,函数名直接是一个对象的引用——我们可以自由地把这个对象赋值给其他的名称并且通过任何引用调用它:
>>> def echo(message):
print(message)
>>> echo('Direct Call')
Direct Call
>>> x = echo
>>> x('Indirect Call!')
Indirect Call!
由于参数通过赋值对象来传递,这就像是把函数作为参数传递给其他函数一样容易。随后,被调用者可能通过把参数添加到括号中来调用传入的函数:
>>> def indirect(func,arg):
func(arg)
>>> indirect(echo,'Argument call')
Argument call
我们甚至可以把函数对象的内容填入到数据结构中,就好像它们是整数或字符串一样。
例如,下面的程序把函数两次嵌套到一个元祖列表中,作为一种动作表。由于像这样的Python复合类型可以包含任意类型的对象,这里也没有什么特殊的:
>>> schedule = [(echo,'Spam!'),(echo,'Ham!')]
>>> for (func,arg) in schedule:
func(arg)
Spam!
Ham!
这段代码只是遍历schedule列表,每次遍历的时候使用一个参数来调用echo函数。
函数也可以创建并返回以便之后使用:
>>> def make(label):
def echo(message):
print(label+':'+message)
return echo
>>> F = make('Spam')
>>> F('Ham!')
Spam:Ham!
>>> F('Eggs')
Spam:Eggs
----------
---------------------
-------------------------------
-------------------------------
-----------------------------
函数内省
由于函数是对象,我们可以用常规的对象工具来处理函数。实际上,函数比我们所预料的更灵活。例如,一旦我们创建了一个函数,可以像往常一样调用它:
>>> def func(a):
b = 'spam'
return b*a
>>> func(8)
'spamspamspamspamspamspamspamspam'
但是,调用表达式只是定义在函数对象上工作的一个操作。我们也可以通用地检查它们的属性:
>>> func.__name__
'func'
>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
内省工具允许我们探索实现细节——例如,函数已经附加了代码对象,代码对象提供了函数的本地变量和参数等方面的细节:
>>> func.__code__
<code object func at 0x0328B570, file "<pyshell#95>", line 1>
>>> dir(func.__code__)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__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
----------
---------------------
-------------------------------
-------------------------------
-----------------------------
函数属性
函数对象不仅限于上述列出的系统定义的属性。也可以向函数附加任意的用户定义的属性:
>>> 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__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'handles']
这样的属性可以用来直接把状态信息附加到函数对象,而不必使用全局、非本地和类等其他技术。和非本地不同,这样的属性可以在函数自身的任何地方访问。
============================================================================
Python3.0中的函数注解
在Python3.0中,也可以给函数对象附加注解信息——与函数的参数和结果相关的任意的用户定义的数据。Python为声明注解提供了特殊的语法,但是,它自身不做任何事情;注解完全是可选的。并且,出现的时候只是直接附加到函数对象的__annotations__属性以供其他用户使用。
在《参数》中介绍了Python3.0的keyword-only参数,注解则进一步使函数头部语法通用化。
考虑如下不带注解的函数:
>>> def func(a,b,c):
return a+b+c
>>> func(1,2,3)
6
从语法上讲,函数注解编写在def头部行,就像与参数和返回值相关的任意表达式一样。对于参数,它们出现在紧随参数名之后的冒号之后:对于返回值,它们编写于紧跟在参数列表之后的一个->之后。例如,这段代码,注解了前面函数的3个参数及其返回值:
>>> def func(a:'spam',b:(1,10),c:float)->int:
return a+b+c
>>> func(1,2,3)
6
调用一个注解过的函数,像以前一样。不过,当注解出现的时候,Python将它们收集到字典中,并且将它们附加给函数对象自身。
参数名变成键,如果编写了返回值注解的话,它存储在键“return”下,而注解键的值则赋给了注解表达式的结果。
>>> func.__annotations__
{'a': 'spam', 'b': (1, 10), 'return': <class 'int'>, 'c': <class 'float'>}
由于注解只是附加到一个Python对象的Python对象,注解可以直接处理。下面的例子只是注解了3个参数中的2个,并且通用地遍历附加的注解:
>>> def func(a:'spam',b,c:99):
return a+b+c
>>> func(1,2,3)
6
>>> func.__annotations__
{'a': 'spam', 'c': 99}
>>> for arg in func.__annotations__:
print(arg,'=>',func.__annotations__[arg])
a => spam
c => 99
这里,有两点需要注意。首先,如果编写了注解的话,仍然可以对参数使用默认值——注解(及其:字符)出现在默认值(及其=字符)之前。例如,下面的a:'spam'=4意味着参数a的默认值是4,并且用字符串‘spam’注解它:
>>> def func(a:'spam'=4,b:(1,10)=5,c:float=6)->int:
return a+b+c
>>> func(1,2,3)
6
>>> func()
15
>>> func(1,c=10)
16
>>> func.__annotations__
{'a': 'spam', 'b': (1, 10), 'return': <class 'int'>, 'c': <class 'float'>}
注解是Python3.0的新功能,并且其一些潜在的用途还没有介绍。很容易想象,注解可以用作参数类型或值的特性限制,并且较大的API可能使用这一功能作为注册函数接口信息的方式。
和Python一样,注解是一种功能伴随着你的想象来变化的工具。
最后,注意,注解只在def语句中有效,在lambda表达式中无效。