一、函数的定义
在 Python 中为了将代码的流程进行分解,可以通过函数对程序代码的逻辑进行过程化(函数是面向过程的)和结构化(一个函数是一个独立的处理结构)的封装,将整块具有独立功能的代码隔离并打包到一个单独的函数中,从而将一个系统分割为不同的部分。
我们可以把具有一定功能性(可以完成某项任务)的代码放到函数中,这样在程序中需要此函数的功能时就可以直接调用函数,而不必进行重复的代码拷贝 —— 这样既能节省空间,也有助于保持程序的一致性,因为当后期有修改需要时你只需改变此函数的代码而无须去寻找修改大量代码的拷贝。
函数是Python为了最大化的代码重用和最小化的代码冗余而提供的最基本的程序结构。
1、函数的定义
在某些编程语言(比如C/C++)里,函数的声明和定义是区分开的。一个函数声明包括函数名和参数的名字(传统上还有参数的类型和函数的类型也就是返回值的类型), 但不必给出函数的任何代码, 具体的代码通常属于函数定义的范畴。这样的设计往往是为了将函数的定义和声明放在不同的文件中。 Python将这两者视为一体,函数语句由声明的标题行以及随后的定义体组成。
我们使用 def 关键字来自定义函数。自定义的函数主要由函数头和可执行的函数体两部分组成。
函数头(首行)由 def 关键字以及紧随其后的函数名再加上放在小括号中的参数(可选)组成。首行语句由一个冒号(:)结束,后面仅跟着一个可选(强烈推荐)的文档字串,和必需的代码块。这个代码块通常都会缩进(或者就是紧跟在冒号后边的简单的一句)。而这个代码块就是函数的主体 —— 也就是每当调用函数时 Python 所执行的语句。
函数定义可以由一个或多个装饰器表达式包装。装饰器表达式是在函数定义时,在包含函数定义的作用域中的计算。结果必须是可调用的,它以函数对象作为唯一的参数来调用。返回的值绑定到函数名称而不是函数对象。多个装饰器以嵌套方式应用,关于装饰器的原理和应用我们会在下面的章节详细介绍。
下面的代码:
大致相当于:
函数一般会使用参数作为输入,使用return作为输出,关于参数的详细说明我们会在下面的章节具体介绍。函数具体的输入输出方式可以参考下面的图示:
2、函数的返回值
Python 里的函数使用 return 语句返回一个对象,返回的对象可以是一个容器类型,比如序列和字典。如果函数中没有 return 语句, 就会自动返回 None 对象。如果函数返回多个对象,python 会自动把他们包装在一个元组中来返回。
return 语句会直接终止函数的执行并返回后面的对象,所以 return 后面的代码不会执行。
许多静态类型的语言主张一个函数的类型就是其返回值的类型。但是在 python 中, 由于 python 是动态地确定类型而且函数能返回不同类型的对象,所以没有将函数和类型进行直接的的关联。
3、函数对象
def 是一个可执行语句。当 Python 解释器运行了 def 语句后,便创建了一个函数对象(函数的可执行代码的包装器)并将其赋值给了 def 关键字后面紧跟的变量名。就像所有的赋值一样,函数名变成了这个函数对象的引用。函数对象可以赋值给其他的变量名,甚至可以保存在列表之中。
我们可以把函数对象填入到数据结构中,就好像它们是整数或字符串一样。Python的复合类型可以包含任意类型的对象。
函数定义时不执行函数体只是生成函数对象并对其进行赋值,只有当函数被调用时函数对象(函数体)才被执行。
在典型的操作中,def 语句一般在模块文件中编写,并自然而然的在模块文件第一次被导入的时候生成定义的函数对象。
4、函数属性
可以向函数附加任意的用户定义的属性。
这样的属性可以用来直接把状态信息附加到函数对象,而不必使用全局、非本地和类等其他技术。和非本地不同,这样的属性可以在函数自身的任何地方访问。这种变量的名称对于一个函数来说是本地的,但是,其值在函数退出后仍然保留。属性与对象相关而不是与作用域相关,但直接效果是类似的。
5、函数注解
在 Python 3 中可以给函数对象附加注解信息 —— 与函数的参数和结果相关的任意的用户自定义的数据。Python为声明注解提供了特殊的语法,但是,它自身不做任何事情。注解完全是可选的,并且,出现的时候只是直接附加到函数对象的 __annotations__ 属性以供其他用户使用。
参数可以在参数名称后面带有 “ : expression ” 形式的注解。任何参数都可以具有注解,即使是 *args 或 \ kwargs 形式。函数可以在参数列表的后面带有 “ -> expression ” 形式的 “返回值” 注解。这些注解可以是任何有效的 Python 表达式,并且在执行函数定义时计算。注解可能以不同于它们在源代码中出现的顺序计算。注解的存在不改变函数的语义。注解的值可以通过函数对象的 __annotations__ 字典属性访问,以参数的名称作为键。
调用一个注解过的函数,像以前一样,不过,当注解出现的时候,Python 将他们收集到字典中并且将它们附加给函数对象自身。参数名编程键,如果写了返回值注解的话,它存储在键 "return" 下,而注解的值则赋给了注解表达式的结果。
由于注解只是附加到一个 Python 对象的 Python 对象,注解可以直接处理。
如果编写了注解的话,仍然可以对参数使用默认值 —— 注解出现在默认值之前。
你可以在函数头部的各部分之间使用空格,也可以不用,但省略他们对某些读者来说可能会提高代码的可读性。
注解可以用作参数类型或值的特定限制,并且较大的API中你可以使用这一功能作为标识函数接口信息的方式。
6、匿名函数:lambda
除了 def 语句之外,Python 还提供了一种生成函数对象的表达式形式:lambda 。
这个表达式创建了一个能够调用的函数,但是它返回了一个函数对象而不是将这个函数对象赋值给一个变量名。这也就是 lambda 有时叫做匿名(也就是没有函数名)函数的原因。
lambda的一般形式是:关键字 lambda,之后是一个或多个参数(相当于 def 语句头部内用括号括起来的参数列表),紧跟着的是一个冒号,之后是一个表达式,这个表达式的定义体必须和声明放在同一行。参数是可选的,如果使用的参数话,参数通常也是表达式的一部分。
lambda [argument1, argument2, .... argumentn]: expression
由 lambda 表达式所返回的函数对象与由 def 创建并赋值后的函数对象工作起来是完全一样的,但是 lambda 有一些不同之处让其在扮演特定的角色时很有用:
lambda 是一个表达式,而不是一个语句
lambda 表达式能够出现在 Python 语句语法上不允许 def 出现的地方。例如:在一个列表常量中或者函数调用的参数中。此外作为一个表达式,lambda 返回了一个值(一个新的函数对象),可以选择性地赋值给一个变量名。相反,def 语句总是得在头部将一个新的函数赋值给一个变量名,而不是将这个函数作为结果返回。
lambda 的主体是一个单个的表达式或语句,而不是一个代码块
lambda 通常要比 def 功能要小:你仅能够在 lambda 主体中封装有限的逻辑进去,连 if 这样的语句都不能够使用。这是有意设计的——它限制了程序的嵌套:lambda 是一个为编写简单的函数而设计的,而 def 用来处理更大的任务。
除了这些差别,def 和 lambda 都能够做同样种类的工作。
为什么使用 lambda
通常来说,lambda 起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的定义。它们完全是可选的(你总是能够使用 def 来替代它们),但是在你仅需要嵌入小段可执行代码的情况下它们会带来一个更简洁的代码结构。
lambda 通常用来编写跳转表,也就是行为的列表或字典,能够按照需要执行相应的动作。
当需要把小段的可执行代码编写进 def 语句从语法上不能编写进的地方时,lambda 表达式作为 def 的一种速写来说是最为有用的。一个 def 是不会在列表常量中工作的,因为他是一个语句,而不是一个表达式。
我们可以用 Python 中的字典或者其他的数据结构来构建更多种类的行为表,从而做同样的事情。
7、内部/内嵌函数
在函数体内创建另外一个函数(对象)是完全合法的。这种函数叫做内部/内嵌函数。因为现在 python 支持静态地嵌套域,内部函数实际上很有用的。
最明显的创造内部函数的方法是在外部函数的定义体内定义函数。
8、递归函数
Python 支持递归函数 —— 即直接或间接的调用自身以进行循环的函数。它允许程序遍历拥有任意的、不可预知的形状的结构。递归甚至是简单循环和迭代的替换,尽管它不一定是最简单的或最高效的一种。
循环 VS 递归
递归在 Python 中并不像 Prolog 或 Lisp 这样更加深奥的语言中那样常用,因为Python 强调像循环这样的简单的过程式语句,循环语句通常更为自然。
而且 for 循环为我们自动迭代,使得递归在大多数情况下不必使用(并且,很可能,递归在内存空间和执行时间方面效率更低)。
处理任意结构
递归可以要求遍历任意形状的结构。简单的循环语句在这里不起作用,因为这不是一个线性迭代。嵌套的循环语句也不够用,因为子列表可能嵌套到任意的深度并且以任意的形式嵌套。相反,下面的代码使用递归来对应这种一般性的嵌套,以便顺序访问子列表。
9、函数的设计理念
耦合性:只有在真正必要的情况下使用全局变量。
全局变量通常是一种蹩脚的函数间进行通信的办法。它们引发了依赖关系和计时的问题,会导致程序调试和修改的困难。
耦合性:不要改变可改变类型的参数,除非调用者希望这样做。
函数可以改变传入的可变类型对象,但是就像全局变量一样,这回导致很多调用者和被调用者之间的耦合性,这种耦合性会导致一个函数过于特殊和不友好。
耦合性:避免直接改变在另一个模块文件中的变量。
改变引入模块中的变量会导致模块文件间的耦合性,就像全局变量产生了函数间的耦合一样:模块难于理解和重用。
聚合性:每一个函数都应该有一个单一的、统一的目标。
在设计完美的情况下,每一个函数中都应该做一件事:这件事可以用一个简单说明句来总结。
简洁:每一个函数都应该尽可能的简洁。
Python 代码是以简单明了而著称的 ,一个过长或者有着深层嵌套的函数往往就成为设计缺陷的征兆。保持简单,保持简短。
通常来讲,我们应该竭力使函数和其他编程组件中的外部依赖性最小化。函数的自包含性越好,它越容易被理解、复用和修改。
10、和函数相关的内建函数
filter()
函数式编程的意思就是对序列应用一些函数的工具。例如,基于某一测试函数过滤出一些元素(filter),以及对每队元素都应用函数并运行到最后结果(reduce)。
map()
程序对列表和其他序列常常要做的一件事情就是对每一个元素进行一个操作并把其结果集合起来。
map() 函数会对一个序列对象中的每一个元素应用被传入的函数,并且返回一个包含了所有函数调用结果的一个列表。
因为 map() 是内置函数,它总是可用的,并总是以同样的方式工作,还有一些性能方面的优势(它要比自己编写的 for 循环更快)。
reduce()
reduce() 位于 functools 模块中,要更复杂一些。它接收一个迭代器来处理,但是,它自身不是一个迭代器,它返回一个单个的结果。