对于Python编程中一些重用与缩减的建议
返璞归真
许多流行的玩具都以这样一个概念为基础:简单的积木。这些简单的积木可通过多种方式组合在一起构造出全新的作品——有时甚至完全令人出乎意料。这一概念同样适用于现实生活中的建筑领域,将基本原材料组合在一起,形成有用的建筑物。平凡无奇的材料、技术和工具简化了新建筑物的建造过程,同样也简化了对新踏入此领域的人员的培训。
相同的基本概念也适用于计算机程序开发技术,包括以Python编程语言编写的程序。本文介绍了使用Python创建基本构件(buildingblock)的方法,可用于解决更为复杂的问题。这些基本构件可能小而简单,也可能庞大而复杂。无论采用哪种形式,我们这场游戏的目的就是定义基本构件,然后使用它们来创建专属于您的杰作。
函数:封装逻辑
在本系列的前几篇文章中,您通常不得不重复输入所有代码,即便它与上一行代码完全相同。此要求的惟一特例就是变量的使用:一旦初始化了变量的内容,之后就可以随时重用。显而易见,这一用法的普及对我们大有好处。
描述杰出程序员的最流行的箴言之一就是他们很懒惰。这并不表示杰出的程序员不努力工作——而是说他们喜欢灵活的工作方法,除非绝对必要,否则从不反复做任何相同的事情。这也就意味着在您需要编写代码之前,首先考虑如何实现重用。Python中有多种可实现重用的途径,但最简单的技术莫过于使用函数,也称为方法或子例程。
与绝大多数现代编程语言类似,Python支持使用方法将一组语句封装在一起,从而可在必要时重复使用。清单1给出了一段简单的伪代码,为您展示如何在Python中编写方法。
清单1.定义函数的伪代码
defmyFunction(optionalinputdata): initializeanylocaldata actualstatementsthatdothework optionallyreturnanyresults
如您所见,在Python中,函数的基本组成部分是包装器代码,指明将被重用的一系列Python语句。函数可接受输入参数,输入参数在紧接着函数名(在本例中为myFunction)之后的圆括号内提供。函数还可返回值(更为正式的说法是:对象),包括像tuple这样的Python容器。
在真正着手构建函数之前,让我们先来看看关于伪代码的一些简单却重要的要点:
- 请注意函数名中所用的字符大小写:大多数字符都是小写的,但在使用多个单词连接成一个函数名时,后接的各单词首字母应大写(例如,myFunction中的F)。这就是所谓的驼峰式大小写风格(camelcasing),是Python和其他编程语言中广泛采用的一种技术,可使函数的名称更易阅读。
- 函数定义中的程序语句采用了缩进式排版,函数体由Python语句块构成,它们也必须像循环或条件语句那样缩进。
- 函数定义的第一行也称为方法签名(methodsignature),以def开头(def是define这个单词的缩写)。
- 方法签名以冒号结尾,表示下面的代码行是函数体。
至此,您或许已认可了使用方法的好处。那么让我们投入进去,开始编写函数吧。“DiscoverPython,Part6:ProgramminginPython,Forthefunofit”中使用了一个for循环来创建乘法表。清单2展示了同样的概念,但本例中创建的是一个函数,用于封装乘法表计算背后的逻辑。
清单2.第一个函数
>>>deftimesTable(): ...forrowinrange(1,6): ...forcolinrange(1,6): ...print"%3d"%(row*col), ...print ... >>>timesTable() 12345 246810 3691215 48121620 510152025 >>>t=timesTable >>>type(t) <type'function'> >>>t <functiontimesTableat0x64c30> >>>t() 12345 246810 3691215 48121620 510152025
timesTable函数定义起来非常简单,它不接受任何输入参数,也不返回任何结果。函数体几乎与“DiscoverPython,Part6”中的语句完全相同(但该文章中的乘法表为从1到10)。为了调用方法,并使其发挥作用,只需输入函数名后接圆括号即可。本例中还输出了乘法表。
在Python中,函数是一类对象,与整型变量和容器对象相同。因而,您可以将函数指派给一个变量(切记,在Python中变量是动态类型化的)。在清单2中,我们将timesTable函数指派给变量t。接下来的两行代码表示变量t确实指向函数。最后,我们使用变量t调用timesTable函数。
函数:动态更改逻辑
清单2中的timesTable函数不复杂,但也不是特别有用。更有用的示例允许您指定用于生成乘法表的行数和列数——换言之,允许您在调用函数时动态地更改函数的操作方式。在函数定义中使用两个输入参数即可实现这一功能,如清单3所示。
清单3.更好的乘法表函数
>>>deftimesTable2(nrows=5,ncols=5): ...forrowinrange(1,nrows+1): ...forcolsinrange(1,ncols+1): ...print"%3d"%(row*cols), ...print ... >>>timesTable2(4,6) 123456 24681012 369121518 4812162024 >>>timesTable2() 12345 246810 3691215 48121620 510152025 >>>timesTable2(ncols=3) 123 246 369 4812 51015
两个乘法表函数的定义非常相近,但清单3中的函数有用得多(通过清单3中的3次调用即可看出这一点)。为函数添加此附加功能的方法非常简单:提供名为nrows和ncols的两个输入参数,允许在调用函数时更改乘法表的大小。这两个参数随后会被提供给生成乘法表的两个for循环。
关于timesTable2函数的另一要点就是两个输入参数有默认值。在函数签名中为参数提供默认值,方法是在参数名后添加等号和值,例如nrows=5。默认参数使程序获得了更高的灵活性,因为在您调用函数时,可以包含两个输入参数,也可以仅包含一个输入参数,甚至可以一个参数都不包含。但这种方法可能会导致某些问题。如果您在函数调用期间未指定全部参数,则必须显式地写出您所指定的参数的名称,以使Python解释器能够正确地调用函数。最后一个函数调用正体现了这一点,它显式地调用了带有ncols=3的timesTable2函数,函数创建了一个5行(默认值)3列(所提供的值)的乘法表。
函数:返回数据
使用方法时,人们最希望获得的结果并非总是乘法表。您可能希望完成一次计算,并将计算结果值返回给调用代码。有时要实现这两个目的,需要分别调用不返回任何数据的调用方法(子例程)和返回值的方法(函数)。但在Python中,您无需担心这些语义问题,因为通过使用return语句,几乎可以相同的方式实现这两个目的(参见清单4)。
清单4.在函数中返回一个值
>>>defstats(data): ...sum=0.0 ...forvalueindata: ...sum+=value ...return(sum/len(data)) ... >>>stats([1,2,3,4,5])#Findthemeanvaluefromalist 3.0 >>>stats((1,2,3,4,5))#Findthemeanvaluefromatuple 3.0 >>>stats() Traceback(mostrecentcalllast): File"<stdin>",line1,in? TypeError:stats()takesexactly1argument(0given) >>>stats("12345") Traceback(mostrecentcalllast): File"<stdin>",line1,in? File"<stdin>",line4,instats TypeError:unsupportedoperandtype(s)for+=:'float'and'str'
这个简单的函数遍历data(假设data为一个容纳有数字数据的Python容器),计算一组数据的平均值,然后返回值。函数定义接受一个输入参数。平均值通过return语句传回。当您调用带有包含数字1到5的list或tuple的函数时,返回值会显示在屏幕上。如果调用不带任何参数的函数、带非容器数据类型的函数或带内含非数字数据的容器的函数,就会导致出错。(在此类情况下抛出错误是很有意义的。更高级的处理方法应包含恰当的错误检查和处理,以应对这些情况,但这不在本文讨论范围内。)
此示例已经非常有用,但还可使它更强大,如清单5所示。在Python中,函数可返回任何有效的对象类型,包括容器类型在内。因此,您可以计算多个数量,并轻松地将多个结果返回给调用语句。
清单5.返回复合值
>>>defstats(data): ...sum=0.0 ...forvalueindata: ...sum+=value ...mean=sum/len(data) ...sum=0.0 ...forvalueindata: ...sum+=(value-mean)**2 ...variance=sum/(len(data)-1) ...return(mean,variance) ... >>>stats([1,2,3,4,5]) (3.0,2.5) >>>(m,v)=stats([1,2,3,4,5,6,7,8,9]) >>>printm,v 5.07.5
为了从一个函数中返回多个值,要将其括在一个括号中并以逗号分隔——换句话说,创建并返回一个tuple。新stats函数的函数体要略加修改,以计算数字序列的样本方差。最后,正如stats函数的两次调用所示,tuple值可作为一个tuple存取,也可将其解包为各自的分量。
模块:简化代码重用
至此,您或许已相信了代码重用的价值。但即便是使用函数,您依然需要在打算使用函数时重新输入函数体。例如,当您打开一个新的Python解释器时,必须键入之前所创建的所有函数。幸运的是,您可以使用模块将相关函数(和其他Python对象)封装在一起,将其保存在一个文件中,然后将这些已定义好的函数导入到新Python代码内,包含于Python解释器之中。
为介绍在Python中使用模块的方法,我们将重用清单5中的stats方法。有两个选择:您可以从与本文相关的压缩文件中提取名为test.py的文件,也可以在编辑器中键入函数,然后将文件保存为test.py。完成上一步后,在您保存test.py的目录中启动一个新的Python解释器,然后输入如清单6所示的语句。
清单6.使用模块
>>>importtest >>>test.stats([1,2,3,4,5,6,7,8,9]) (5.0,7.5) >>>fromtestimportstats >>>(m,v)=stats([1,2,3,4,5,6,7,8,9]) >>>printm,v 5.07.5
第一行importtest打开文件test.py并处理文件中的各条语句。这里仅定义了stats函数,但若需要,您还可定义更多的函数。调用stats函数时,应以模块名test作为函数前缀。之所以使用这种复杂的名称,是出于作用域方面的考虑,作用域表示一个程序内名称的有效范围。为告知Python您要调用的是哪个stats方法,就必须提供完整的名称。这一点非常重要,因为您可能拥有多个名称相同的对象。作用域规则可帮助Python判断您想使用的对象。
第三行fromtestimportstats也打开了文件test.py,但它隐式地将stats方法置入当前文件的作用域内,以使您能够直接调用stats函数(无需使用模块名)。明智地使用from...import...语法可使您的程序更简洁,但过度的使用也会导致混淆,甚至出现更糟糕的作用域冲突错误。不要滥用您的新武器!
模块库
使用Python编程语言的一个主要好处就是大型的内置式标准库,可作为Python模块访问。常用模块示例如下:
- math包含有用的数学函数。
- sys包含用于与Python解释器交互的数据和方法。
- array包含数组数据类型和相关函数。
- datetime包含有用的日期和时间处理函数。
由于这些都是内置模块,因此您可以通过帮助解释器来了解更多相关内容,如清单7所示。
清单7.获得关于math模块的帮助信息
>>>help(math) Traceback(mostrecentcalllast): File"<stdin>",line1,in? NameError:name'math'isnotdefined >>>importmath#Needtoimportmathmoduleinordertouseit >>>help(math) Helponmodulemath: NAME math FILE /System/Library/Frameworks/Python.framework/Versions/2.4/lib/ python2.4/lib-dynload/math.so DESCRIPTION Thismoduleisalwaysavailable.Itprovidesaccesstothe mathematicalfunctionsdefinedbytheCstandard. FUNCTIONS acos(...) acos(x) Returnthearccosine(measuredinradians)ofx. asin(...) asin(x) Returnthearcsine(measuredinradians)ofx. ...
math模块的帮助输出展示了所支持的大量数学函数,包括sqrt函数在内。您可以利用此函数将您的样本方差计算转换为样本标准差计算,如清单8所示。
清单8.使用多个模块
>>>frommathimportsqrt >>>fromtestimportstats >>>(m,v)=stats([1,2,3,4,5,6,7,8,9]) >>>printm,sqrt(v) 5.02.73861278753
如您所见,您可以将多个模块导入到一个Python程序中。在大型、内置的模块库与更大量的公用库(其中许多都是开放源码的)的共同协助下,您很快也会成为一名懒惰——也就是杰出——的程序员。
可执行文件
导入一个模块时,Python解释器会处理模块文件内的各行。实际上,您可以调用Python解释器使其仅处理包含于一个文件中的一个Python程序。在基于UNIX?的操作系统中,您可以轻松创建可执行的文件,如清单9所示。
清单9.一个完整的Python程序
#!/usr/bin/envpython defstats(data): sum=0.0 forvalueindata: sum+=value mean=sum/len(data) sum=0 forvalueindata: sum+=(value-mean)**2 variance=sum/(len(data)-1) return(mean,variance) (m,v)=stats([1,2,3,4,5,6,7,8,9]) print"Themeanandvarianceofthevalues"\ "from1to9inclusiveare",m,v
观察上例,您应该会产生几分好感,将Python程序置于文件内,并使其运行是如此简单。本例与test.py文件中的代码之间惟一的差异就是包含了第一行。在基于UNIX的操作系统中,本行会使Python解释器自动启动,并在终止前处理文件中的语句。本示例中的其他行定义了stats函数、调用了函数,并输出了结果。
要运行本文件中的语句,您需要启动一个Python解释器,并让它去读取和处理文件的内容。为实现这一目的,您必须首先将清单9中的示例输入到一个名为mystats.py的文件中,也可从与本文相关的压缩文件中提取文件。进入包含此文件的目录,然后按清单10中所示命令执行。注意对于Microsoft?Windows?操作系统而言,仅应使用第一条命令;其他命令是供UNIX系统(如Linux?或MacOSX)使用的。
清单10.执行Python程序
rb%pythonmystats.py Themeanandvarianceofthevaluesfrom1to9inclusiveare5.07.5 rb%chmod+xmystats.py rb%./mystats.py Themeanandvarianceofthevaluesfrom1to9inclusiveare5.07.5
清单10中的命令展示了运行一个包含于文件之中的Python程序的方法。第一条命令以文件名调用Python解释器,无论使用哪种系统安装Python、Python解释器位于哪个目录下,这种方法都有效。第二条命令chmod使包含Python程序的文件成为可执行文件。第三条命令告诉操作系统运行程序。这是通过使用env程序实现的,这是一种独立于操作系统的技术,用于定位和运行程序——本例中是Python解释器。
重用与缩减
本文介绍了如何在Python中编写可重用代码。讨论了如何在Python程序中使用方法或可重用块。方法可接受输入参数,也可返回数据,包括容器数据类型在内。这种功能使方法成为一种可处理大量问题的强大途径。本文还介绍了模块,模块可使您将相关方法及数据合并入一个有组织的层次结构中,而此结构可方便地在其他Python程序中重用。最后还介绍了如何将所有这些内容组合在一起以创建一个功能完整、独立的Python程序。您已经看到,代码的重用也就意味着您的工作量缩减。对于程序员而言,懒惰是一种优势而非陋习。