个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
本文主要介绍Python中函数的基本概念,作用域以及参数传递,函数语法以及def和return语句的操作,函数调用表达式的行为,以及Python函数中多态的概念和优点。强调了所有的在函数内部进行赋值的变量名都默认为本地变量,所有的本地变量都会在函数调用时出现,并在函数退出时消失。全文非常基础、易懂,阅读时间约5-10分钟。
目录
函数
简而言之,一个函数就是将一些语句集合在一起的部件,它们能够不止一次地在程序中运行。函数还能够计算出一个返回值,并能够改变作为函数输入的参数,而这些参数在代码运行时也许每次都不相同。以函数的形式去编写一个操作可以使它成为一个能够广泛应用的工具,让我们在不同的情形下都能够使用它。
更具体地说,函数是在编程过程中剪剪贴贴的替代——不再有一个操作的代码的多个冗余副本,而是将代码包含到一个单独的函数中。如果这个操作之后必须要修改,只需要修改其中的一份拷贝,而不是所有代码。
函数是Python为了代码最大程度的重用和最小化代码冗余而提供的最基本的程序结构。函数也是一种设计工具,使用它我们可以将复杂的系统分解为可管理的部件。
表16-1总结了这一部分中下文将会提到的与函数相关的主要语句和表达式。
为何使用函数
函数是一个通用的程序结构部件。
简而言之,函数主要扮演了两个角色。
最大化的代码重用和最小化代码冗余
Python的函数是一种简单的办法去打包逻辑算法,使其能够在之后不止在一处、不止一次地使用。
函数允许整合以及通用化代码,以便这些代码能够在之后多次使用。因为它们允许一处编写多处运行,Python的函数是这个语言中最基本的组成工具——它让我们在程序中减少代码的冗余成为现实,并为代码的维护节省了不少的力气。
流程分解
函数也提供了一种将一个系统分割为定义完好的不同部分的工具。
函数将会将整个任务分割成为独立的函数来完成整个流程中的每个子任务。独立的实现较小的任务要比一次完成整个流程要容易得多。一般来说,函数讲的是流程:告诉你怎样去做某事,而不是让你使用它去做的事。
编写函数
虽然没有正式介绍,但是在前面已经接触了一些函数。例如,为了创建文件对象,调用了内置函数open。同样地,使用了内置函数len去得到一个集合对象的元素的数目。
这里会解释在Python中如何去编写一个函数。我们编写的函数使用起来就像内置函数一样:它们通过表达式进行调用,传入一些值,并返回结果。
下面是关于Python函数背后的一些主要概念的简要介绍:
def是可执行的代码。Python的函数是由一个新的语句编写的,即def。def是一个可执行的语句——函数并不存在,直到Python运行了def后才存在。在典型的操作中,def语句在模块文件中编写,并自然而然地在模块文件第一次被导入的时候生成定义的函数。
def创建了一个对象并将其赋值给某一变量名。当Python运行到def语句时,它将会生成一个新的函数对象并将其赋值给这个函数名。就像所有的赋值一样,函数名变成了某一个函数的引用。函数对象可以赋值给其他的变量名,保存在列表之中。函数也可以通过lambda表达式创建。
lambda创建一个对象但将其作为结果返回。也可以用lambda表达式创建函数,允许我们把函数定义内联到语法上一条def语句不能工作的地方。(第19笔记介绍)
return将一个结果对象发送给调用者。当函数被调用时,其调用者停止运行直到这个函数完成了它的工作,之后函数才将控制权返回调用者。函数是通过return语句将计算得到的值传递给调用者的,返回值成为函数调用的结果。
yield向调用者发回一个结果对象,但是记住它离开的地方。像生成器这样的函数也可以通过yield语句来返回值,并挂起它们的状态以便稍后能够恢复状态。
global声明了一个模块级的变量并被赋值。在默认情况下,所有在一个函数中被赋值的对象,是这个函数的本地变量,并且仅在这个函数运行的过程中存在。为了分配一个可以在整个模块中都可以使用的变量名,函数需要在global语句中将它列举出来。通常情况下,变量名往往需要关注它的作用域(也就是说变量存储的地方),并且是通过实赋值语句将变量名绑定至作用域的。
nonlocal声明了将要赋值的一个封闭的函数变量。nonlocal语句允许一个函数来赋值一条语法封闭的def语句的作用域中已有的名称。当一个函数调用的时候,信息被记住了——而不必使用共享的全局名称。
函数是通过赋值(对象引用)传递的。在Python中,参数通过赋值传递给了函数(对象引用)。Python的模式中,调用者以及函数通过引用共享对象,但是不需要别名。改变函数中的参数名并不会改变调用者中的变量名,但是改变传递的可变对象可以改变调用者共享的那个对象。
参数、返回值以及变量并不是声明。在函数中并没有类型约束。从一开始函数就不需要声明:可以传递任意类型的参数给函数,函数也可以返回任意类型的对象。其结果就是,函数常常可以用在很多类型的对象身上,任意支持兼容接口(方法和表达式)的对象都能使用,无论它们是什么类型。
下面会有具体的示例
def语句
def语句将创建一个函数对象并将其赋值给一个变量名。Def语句一般的格式如下所示。
def包含了首行并有一个代码块跟随在后边,这个代码块通常都会缩进(在冒号后边)。这个代码块就成为了函数的主体——也就是每当调用函数时Python所执行的语句。
def的首行定义了函数名,赋值给了函数对象,并在括号中包含了0个或以上的参数(有些时候称为是形参)。
在函数调用的时候,在首行的参数名赋值给了括号中的传递来的对象。
函数主体往往都包含了一条return语句。
Python的return语句可以在函数主体中的任何地方出现。它表示函数调用的结束,并将结果返回至函数调用处。
return语句包含一个对象表达式,这个对象给出的函数的结果。return语句是可选的。如果它没有出现,那么函数将会在控制流执行完函数主体时结束。
从技术角度来讲,一个没有返回值的函数自动返回了none对象,但是这个值是往往被忽略掉的。
def语句是实时执行的
Python的def语句实际上是一个可执行的语句:当它运行的时候,它创建一个新的函数对象并将其赋值给一个变量名。
记住,Python中所有的语句都是实时运行的,没有像独立的编译时间这样的流程。
因为它是一个语句,一个def可以出现在任一语句可以出现的地方——甚至是嵌套在其他的语句中。
例如,尽管def往往是包含在模块文件中,并在模块导入时运行,函数还是可以通过嵌套在if语句中去实现不同的函数定义,这样也是完全合法的。
它在运行时简单地给一个变量名进行赋值。与C这样的编译语言不同,Python函数在程序运行之前并不需要全部定义。更确切地讲,def在运行时才进行评估,而在def之中的代码在函数调用后才会评估。
因为函数定义是实时发生的,所以对于函数名来说并没有什么特别之处。关键之处在于函数名所引用的那个对象。
将函数赋值给一个不同的变量名,并通过新的变量名进行了调用。
就像Python中其他语句的一样,函数仅仅是对象,在程
序执行时它清楚地记录在了内存之中。
实际上,除了调用以外,函数允许任意的属性附加到记录信息以供随后使用。
定义和调用示例
函数描绘了两个方面:定义(def创建了一个函数)以及调用(表达式告诉Python去运行函数主体)。
定义
在交互模式下输入的定义语句,它定义了一个名为times的函数,这个函数将返回两个参数的乘积。
def times(x,y): #创建并定义函数
return x * y #被唤起时调用执行
当Python运行到这里并执行了def语句时,它将会创建一个新的函数对象,封装这个函数的代码并将这个对象赋值给变量名times。
一个典型场景是,这样一个语句编写在一个模块文件之中,当这个文件导入的时候运行。对于这么小的一个程序,用交互提示模式已经足够了。
调用
在def运行之后,可以在程序中通过在函数名后增加括号调用(运行)这个函数。括号中可以包含一个或多个对象参数,这些参数将会传递(赋值)给函数头部的参数名。
times(3.14,4)
12.56
这个表达式传递了两个参数给times函数。参数是通过赋值传递的。函数头部的变量x赋值为3.14,y赋值为4,之后函数的主体开始运行
这个函数,其主体仅仅是一条return语句,这条语句将会返回结果作为函数调用表达式的值。在这里返回的对象将会自动打印出来,但是,如果稍后需要使用这个值,我们可以将其赋值给另一个变量。例如:
x = times(3.14,4) #保存对象结果
x
12.56
下面再试试第三种,传递两个完全不同种类的对象:
times('hi',4) #函数是无类型的
'hihihihi'
这次,函数的作用完全不同(Monty Python再次被引用)。在这第三次调用中,将一个字符串和一个整数传递给x和y,而不是两个数字。“*”对数字和序列都有效。因为在Python中,在这一函数里,我们从未对变量、参数或者返回值有过类似的声明,可以把times用作数字的乘法或是序列的重复。
也就是说,函数times的作用取决于传递给它的值。这是Python中的核心概念之一。
Python中的多态
times函数中表达式x*y的意义完全取决于x和y的对象类型,同样的函数,在一个实例下执行的是乘法,在另一个实例下执行的却是赋值。
Python将对某一对象在某种语法的合理性交由对象自身来判断。实际上,“*”在针对正被处理的对象进行了随机应变。
这种依赖类型的行为称为多态,含义就是一个操作的意义取决于被操作对象的类型。因为Python是动态类型语言,所以多态在Python中随处可见。实际上,在Python中每个操作都是多态的操作:print、index、*操作符,还有很多。
这是有意而为的,且从很大程度上算Python的简易性、灵活性的表现。作为函数,例如它可以自动地适用于所有类别的对象类型。只要对象支持所预期的接口(a.k.a.protocol),那么函数就能处理。
也就是说,如果对象传给函数的对象有预期的方法和表达式操作符,那么它们对于函数的逻辑来说就是有着即插即用的兼容性的。
即使是简单的times函数,任意两个支持*的对象都可以执行,无论它是哪种类型,也不管它是何时编写的。
除此之外,如果传递的对象不支持这种预期的接口,Python将会在*表达式运行时检测到错误,并自动抛出一个异常。
这也是Python和静态类型语言(如C++和Java)至关重要不同之处:在Python中,代码不应该关心特定的数据类型。如果不是这样,那么代码将只对编写时你所关心的那些类型有效,对以后的那些可能会编写的兼容对象类型并不支持,这样做会打乱代码的灵活性。
这种多态的编程模型意味着必须测试代码去检测错误,而不是开始提供编译器用来为我们检测类型错误的类型声明。
大体上来说,我们在Python中为对象编写接口,而不是数据类型。
寻找序列交集示例
之前的案例中编写了一个loop循环,搜索两个字符串公共元素。那段代码并不是想象的那么有用,因为这个程序被设置为只能列出定义好的变量并且不能继续使用。
当然,可以在需要它的每一个地方都使用拷贝粘贴的方法,但是这样的解决方案既不好也不通用——我们还是得编辑每一份拷贝的内容,将它换成不同的序列名称,并且改变不同拷贝所需要的算法。
定义
现在可能你已经猜到了解决这种困境的办法:将这个for循环封装在一个函数之中。这样做的好处如下。
- 把代码放在函数中让它能够成为一个想运行多少次就运行多少次的工具。
- 调用者可以传递任意类型的参数,函数对于任意两个希望寻找其交集的序列(或者其他可迭代的类型)都是通用的。
- ·当逻辑由一个函数进行封装的时候,一旦需要修改重复性的任务,只需要在函数里进行修改搜索交集的方式就可以了。
实际效果就是,将代码封装在函数中,使它成为一个通用搜索交集的工具。在模块文件中编写函数意味着它可以被计算机中的任意程序来导入和重用。
def intersect(seq1,seq2):
res = []
for x in seq1:
if x in seq2:
res.append(x)
return res
将第13学习笔记的的简单代码转化为这样的函数是很直接的。就是把原先的逻辑编写在def头部之后,并且让被操作的对象变成被传递进入的参数。为了实现这个函数的功能,增加了一条return语句来将最终结果的对象返回给调用者。
调用
在能够调用函数之前,必须先创建它。
可以先运行def语句,要么就是通过在交互模式下输入,要么就是通过在一个模块文件中编写好它,然后导入这个文件。
一旦运行了def,就可以通过在括号中传递两个序列对象从而调用这个函数:
s1 = 'spam'
s2 = 'scam'
intersect(s1,s2)
['s', 'a', 'm']
这里,传递了两个字符串,并且得到了一个包含着用逗号分隔的字符的列表。这个函数的算法相当的简单:“对于第一个参数中的所有元素,如果也出现在第二个参数之中,将它增加至结果之中”。
intersect函数相当慢(它执行嵌套循环),并不是真正的数学交集(结果中可能有重复的元素),并且也根本不必要(Python的集合数据类型提供了一个内置的交集操作)。
实际上,这个函数可以用一个单独的列表解析表达式来替代,因为它展示了经典的循环收集器代码模式:
[x for x in s1 if x in s2]
['s', 'a', 'm']
作为一个函数的基础示例,它完成了任务——这个单个的代码段可以应用于整个的对象类型范围。
多态复习
和所有的Python中的函数一样,intersect是多态的。也就是说,它可以支持多种类型,只要其支持扩展对象接口:
x = intersect([1,2,3],(1,3))
x
[1, 3]
这次,给函数传递了不同类型的对象[一个列表和一个元组(混合类型)],并且仍然是选择出共有的元素。
没有必要去定义预先定义参数的类型,这个intersect函数很容易对传递给它的任何序列对象进行迭代,只要这些序列支持预期的接口就行了。
对于intersect函数,这意味着第一个参数必须支持for循环,并且第二个参数支持成员测试。所有满足这两点的对象都能够正常工作,与它们的类型无关——这包括了物理存储的序列,例如,字符串和列表。所有在第14学习笔记见到过的迭代对象,包括文件和字典;甚至编写的支持操作符重载技术的任意基于类的对象。
强调:如果传入了不支持这些接口的对象(例如,数字),Python将会自动检测出不匹配,并抛出一个异常——这正是我们一般所想要的。
intersect(2,3)
Traceback (most recent call last):
File "<pyshell#34>", line 1, in <module>
intersect(2,3)
File "<pyshell#22>", line 3, in intersect
for x in seq1:
TypeError: 'int' object is not iterable
如果希望明确地编写类型检测的话,我们可以利用它来自己实现。通过不编写类型测试,并且允许Python检测不匹配,我们都减少了自己动手编写代码的数量,并且增强了代码的灵活性。
本地变量
这个例子中最有趣的部分是其名称。
证明了,intersect函数中的res变量在Python中叫做本地变量——这个变量只是在def内的函数中是可见的,并且仅在函数运行时是存在的。
实际上,由于所有的在函数内部进行赋值的变量名都默认为本地变量,所以intersect函数内的所有的变量均为本地变量。
·res是明显的被赋值过的,所以它是一个本地变量。
·参数也是通过赋值被传入的,所以seq1和seq2也是本地变量。
·for循环将元素赋值给了一个变量,所以变量x也是本地变量。
所有的本地变量都会在函数调用时出现,并在函数退出时消失——intersect函数末尾的return语句返回结果对象,但是变量res却消失了。