目录
这本书的目的是教会你怎样像一个电脑科学家一样思考。这种思考方式包含了数学、工程和自然学科的特点。和数学家一样,计算机科学家使用规范的语言来表达思想(具体的计算)。像工程学家一样,他们设计事物,把组件组装成系统,评估可选方案并作出权衡。像自然科学家一样,他们观察复杂系统的行为,作出假设并验证猜测。
一个计算机科学家最重要的技能就是解决问题,解决问题表示能够阐述问题, 创造性的考虑解决方案,并准确而清晰的表达出来。学习编程是一个锻炼解决问题能力的很好的机会。这就是为什么这章叫做编程之道。
某种层面上来说,学会编程本身就是一样很有用的技能。另一层面上讲,你将使用编程作为达到目的的手段,在我们学习的过程中,这一目的将会变得更加明确。
程序是一系列指定如何计算的特定的指令序列。要计算的可以是一些数学问题,比如求解一个方程组或者一个多项式的根;也可以是符号运算,比如文本查找和替换;也可以是图表处理,比如图像处理或者视频播放。
也许不同的语言细节不同,但都有输入输出、数学运算、条件结构、循环结构等基本结构。不管相信与否,每种语言,不管多复杂,都由这些基本结构组成。所以可以把编程理解为一种把大的复杂的任务分解为一个个很小的子任务,直到这些子任务小到能被这些基本结构所执行的一个过程。
按照传统,使用新语言编写的第一个程序是“Hello World!”,它所做的只是显示“Hello World!”这几个字符,在Python中,它可以这样实现:
>>> print('Hello, World!')
这是打印语句的一个示例,尽管它不打印任何东西,只是把结果显示在屏幕上,本例中,显示结果为:
Hello, World!
单引号中的内容就是要显示的文本,它本身并不显示在结果中。
圆括号表示print是一个函数,这在第三章中详细讲解。
Python中提供一些特殊的符号,称为操作符,来进行加法和减法这样的运算。
操作符+,-,*分别代表加减乘运算,示例如下:
>>> 40 + 2
42
>>> 43 - 1
42
>>> 6 * 7
42
操作符 / 表示除法:
>>> 84 / 2
42.0
之所以这里是 42.0 而不是42,我们在下一节中解释。操作符 ** 表示次方运算:
>>> 6**2 + 6
42
一些语言中,使用^作为次方运算,但 Python 中它表示位运算符,叫做 XOR。比如:
>>> 6 ^ 2
4
本书中对位操作符不做介绍,具体可参阅 http://wiki.python.org/moin/BitwiseOperators。
像字符和数字这种量是程序运行所需要的最基本的东西,到目前为止我们所见到的量包括2, 42.0, “hello world!”。
这些量属于不同的类型:2是整整数,42.0是浮点数,“hello world!”是一个字符串。
当不确定一个量所属的类型时,可以在Python解释器中查询:
>>> type(2)
<class 'int'>
>>> type(42.0)
<class 'float'>
>>> type('Hello, World!')
<class 'str'>
在上述结果中,关键字“class”表是类别的意思。每种类型都是一类值的一个分分类。毫无疑问,整数属于 int 类型,字符串属于 str 类型,浮点数属于 float 类型。
那像 '2’和 '42.0’这种量呢?它们看起来像数数值,但又和字符串一样在引号中。
>>> type('2')
<class 'str'>
>>> type('42.0')
<class 'str'>
它们是字符串。
返回目录
编程语言一个最强大的特性就是能操作变量variables,变量用来指代一个值。
赋值语句创建一个新的变量并且给它赋一个初始值:
>>> message = 'and now for something completely different'
>>> n = 17
>>> pi = 3.141592653589793
这个例子中有三个赋值语句,第一个将一个字符串赋值给一个名为message的变量,第二个将整数17赋值给变量n,第三个将π的值赋给pi。
通常在纸上表示一个变量时,写下它的名字然后用一个箭头指向它的值。这种图就叫做状态图,因为它展示了一个变量的内容。图2.1是上例的状态图。
程序员通常给变量取一些有明确意义的名字,表明变量的用途。变量名可以由字母和数字组成,长度可以随意,但要注意不能以数字开头。使用大写字母变量名是合法的,但一般都使用小写字母。
下划线_,也可以出现在变量名中,一般用于连接由多个单词组成的变量名,如:your_name或者airspeed_of_unladen_swallow。
当你给一个变量一个非法的变量名时,会提示你出现语法错误。
>>> 76trombones = 'big parade'
SyntaxError: invalid syntax
>>> more@ = 1000000
SyntaxError: invalid syntax
>>> class = 'Advanced Theoretical Zymurgy'
SyntaxError: invalid syntax
76trombones以数字开头,所以非法,more@包含非法字符串@。 那class为什么也是非法的?
因为class是一个Python的关键字。解释器通过关键字来辨识程序的结构,所以关键字不能用作变量名。
Python3的关键字包含:
False | class | finally | is | return |
None | continue | for | lambda | try |
True | def | from | nonlocal | while |
and | del | global | not | with |
as | elif | if | or | yield |
assert | else | import | pass | |
break | except | in | raise |
你没有必要记住表格中的全部内容,大多数开发环境中,都会以一种不同的颜色来显示关键字,所以当你想以一个关键字作为变量名时,你会知道的。
一个表达式是变量、值和操作符的结合。一个单独的值也是一个合法的表达式,一个单独的变量也是。以下都是合法的表达式:
>>> 42
42
>>> n
17
>>> n + 25
42
当你在提示符后面输入表达式之后,解释器会计算它的值。在上面的例子中,n的值是17,n+25的值是42 。
语句是具有效果的代码单元,如创建变量或显示值:
>>> n = 17
>>> print(n)
第一行是一个定义语句,赋给n一个值,第二条语句是一个打印语句,将n的值显示出来。
当你输入一条语句后,解释器会执行它。一般,一条语句是没有值的。
截至目前位置,我们一直使用交互模式运行python,直接和解释器进行交互。交互模式是一种很好的入门方式,但是如果使用的代码很多时就显得很笨拙。
另一种方法是将代码保存在一个脚本文件中,然后以脚本模式运行解释器来执行脚本。一般Python脚本的名称以.py
结尾。
当表达式包含多个运算符时,求值的顺序取决于操作符的优先级。对于数学运算符,Python遵循数学约定。首字母缩写PEMDAS 是一种记住规则的有用方法:
2 *(3-1)= 4,(1+1)**(5-2)= 8。
还可以使用括号使表达式更易于阅读,如(minute * 100) / 60,
。1 + 2**3
是9
,而不是27
,2* 3**2
是18
,而不是36
。2*3-1 = 5
,不是4
,6+4/2 = 8
,不是5
。度 / 2 * pi
的表达式中,先计算除法,结果再乘以pi
。要除以2pi
,可以用圆括号或者 写度/ 2 / pi
。不用刻意去记运算符的优先级。如果看不明白一个表达式,就用括号把它写清楚。
一般来说,不能对字符串执行数学运算,即使字符串看起来像数字,所以下面的操作是非法的:
'2' - '1' 'eggs' / 'easy' 'third' * 'a charm'
但是有两个例外,+
和*
。
+
运算符执行字符串连接,它通过将字符串首尾相接来合并字符串。例如:
>>> first = 'throat'
>>> second = 'warbler'
>>> first + second
throatwarbler
*
运算符也适用于字符串,它执行字符串重复。例如,“Spam” * 3
是 “SpamSpamSpam”
。如果一个操作数是字符串,那么另一个必须是整数。
随着程序变得越来越大、越来越复杂,也越来越难读懂。形式化的语言是密集的,通常很难查找一段代码并理解它在做什么,或者为什么这么做。
因此,在程序中添加注释以用自然语言解释程序在做什么非常重要。这些注释称为comments,以#
符号开始:
# compute the percentage of the hour that has elapsed
percentage = (minute * 100) / 60
在本例中,注释单独出现在一行上。你也可以在一行的末尾加上注释:
percentage = (minute * 100) / 60 # percentage of an hour
从#
到行尾的所有内容都被忽略——它对程序的执行没有影响。
注释在记录代码的不明显特性时最有用。像这个注释在代码中是冗余的,并且毫无用处:
v = 5 # assign 5 to v
而这个注释包含了代码中没有的有用信息:
v = 5 # velocity in meters/second
好的变量名可以减少对注释的需要,但是长名称会使复杂的表达式难于阅读,因此需要进行权衡。
返回目录
程序中,函数是执行计算的特定语句序列。定义函数时,要指定函数名称和语句序列。这之后,就可以按名称调用函数。
我们已经看到一个函数调用的例子:
>>> type(42)
<class 'int'>
函数的名称是type
。括号中的表达式称为函数的参数()。对于这个函数,函数的返回结果是参数的类型。
通常说函数“接受”一个参数并“返回”一个结果。结果也称为返回值90。
到目前为止,我们已经单独研究了程序的元素——变量、表达式和语句,但没有讨论如何组合它们。
编程语言最有用的特性之一是它们能够使用小的结构并将它们组合起来。例如,函数的自变量可以是任何一种表达式,可以包函算术运算符,甚至函数:
x = math.sin(degrees / 360.0 * 2 * math.pi)
x = math.exp(math.log(x+1))
几乎在任何可以放置值的地方,都可以放置任意表达式,但有一个例外: 赋值语句的左侧必须是变量名。左边的任何其他表达式都是语法错误,如:
>>> minutes = hours * 60 # right
>>> hours * 60 = minutes # wrong!
SyntaxError: can't assign to operator
返回目录
返回目录
返回目录
这本书的目的是教会你怎样像一个电脑科学家一样思考。
检查是否已经安装了turtle模块,可以在解释器中输入:
>>> import turtle
>>> bob = turtle.Turtle()
运行这句代码的时候,应该创建一个新的窗口,窗口中间有一个代表小乌龟的箭头。然后关掉这个窗口。
创建一个名为myploygon.py的文件,输入以下代码:
>>> import turtle
>>> bob = turtle.Turtle()
>>> print(bob)
>>> turtle.mainloop()
第二句代码利用turtle模块中Turtle函数创建了一个Turtle对象,并赋给了名为bob的变量。当打印bob时会显示:
表示bob指代一个在turtle模块中定义的Turtle对象。
mainloop告诉窗口等待用户的操作,这里我们什么都不做,直接关掉窗口。
##3.2 数学函数
##1.3 第一个程序
##1.4 数学操作符
返回目录
返回目录
返回目录
返回目录
返回目录
返回目录
返回目录
返回目录
这本书的目的是教会你怎样像一个电脑科学家一样思考。
检查是否已经安装了turtle模块,可以在解释器中输入:
>>> import turtle
>>> bob = turtle.Turtle()
运行这句代码的时候,应该创建一个新的窗口,窗口中间有一个代表小乌龟的箭头。然后关掉这个窗口。
创建一个名为myploygon.py的文件,输入以下代码:
>>> import turtle
>>> bob = turtle.Turtle()
>>> print(bob)
>>> turtle.mainloop()
第二句代码利用turtle模块中Turtle函数创建了一个Turtle对象,并赋给了名为bob的变量。当打印bob时会显示:
表示bob指代一个在turtle模块中定义的Turtle对象。
mainloop告诉窗口等待用户的操作,这里我们什么都不做,直接关掉窗口。
返回目录
返回目录
返回目录
返回目录
这章讲述迭代,表示能够重复运行的代码块。5.8章讲到的递归就是一种迭代,4.2章中讲到的for循环是另一种迭代形式。这一章中我们讲述用while语句表示的迭代。但首先我们讲一下变量的定义。
正如你所见,对同一个变量赋值多次是合法的。新的赋值操作使已经存在的变量指向新的值。
>>> x = 5
>>> x
5
>>> x = 7
>>> x
7
第一次我们打印x,他的值为5,而第二次其值为7 。
这里我解释一个普遍的疑惑。python中使用等号=来进行赋值操作,像a = b这样的语句很容易被理解为等价的数学命题,即声明a和b相等。但这种理解是不正确的。
首先,相等是一种对称的关系而赋值不是。
返回目录
返回目录
本章我们进行第二个案例学习,涉及到如何搜索具有特定特性的单词来解决填词问题。比如说我们找英文中最长的回文并指出其中字母按照字母表顺序排列的单词。我们会通过编程来简化这个问题
本章的练习中我们需要一系列英文单词。网有很多的单词列表,但最适合我们的还是Moby lexicon项目中的一个单词列表,由Grady Ward收集并贡献给公共领域。这个列表的单词来自113,809 份官方填字谜游戏。也就是说,这些单词在填字谜和其他游戏中被认为是有效的。Moby 收集的单词列表113809of.fic 你可以在http://thinkpython2.com/code/words.txt 下载一份拷贝,并命名为words.txt。
这个文件是一个纯文本文件,可以用文本编译器打开,当然你也可以使用Python来读取它。内置的open函数以文件名为参数并返回一个file对象用以读取文件。
>>> fin = open('words.txt')
fin是用以输入的文件对象的一个一般名称,文件对象提供一些用以读入的方法,比如 readline
用以读取一行字符并返回一个字符串:
>>>fin.readline()
'aa\n'
这个列表中第一个单词是’aa’,是一种 ,\n表示换行。
文件类指针会保持它在文件中所在的位置,所以当你再次调用 readline
你会得到新的一行:
>>>fin.readline()
'aah\n'
返回目录
返回目录
返回目录
返回目录
返回目录
本章介绍将数据永久保存的编程概念以及如何使用文件和数据库等不同的永久存储介质。
到目前为止我们见到的程序从某种意义上来讲都是暂时的,这些程序只运行很短时间,产生一些输出,但程序运行结束后所有数据都会消失掉。再次运行时又从零开始。
而有一些程序是持久的,它们运行很长那个时间,或者一直运行,它们至少会保存一些它们的数据到硬盘等这样永久性的存储介质。当它们被关闭或者重新开始时它们可以从上次停止的地方继续运行。
这样持久性的程序有操作系统,只要电脑开机操作系统就一直运行;还有网络服务器,它需要一直运行来等待接入网络的请求。
程序保存自己数据一个最简单的方法是读取和写入文本文件。我们已经见了很多从文本文件中读取数据的程序,本章就学习如何将数据写入文本文件。
也可以将程序状态储存到数据库中,本章也会介绍一个简单的数据库和模块,pickle,会让存储程序数据十分简单。
一个文本文件是一个存储在硬盘、闪存等永久存储介质上的字符序列。在9.1节中已经介绍了如何打开一个文件并读取数据。
写的变量必须是一个字符串string类型。
##14.
返回目录
到了这里你已经知道如何使用函数组织代码,如何用内置类型组织数据。接下来就要学习面向对象编程,用用户定义的类型来组织代码和数据。面向对象编程是一个很大的话题,需要讲几个章节。
.
除了python内置的类型外,用户还可以自定义类型。比如可以定义一个Point
类型,来代表二维空间的坐标。
在Python中有几种表示点的方法,可以将坐标分别存储在x和y两个变量中,可以将坐标存储为列表或元组中的元素,也可以创建一个新类型来将点表示为对象。创建一个新的对象比其他方式更复杂,但其优势也很明显。
用户自定义的类型也叫做类,类的定义方式如下:
class Point:
"""Represents a point in 2-D space."""
头部表示新类名为Point
。主体是一个说明类用途的文档字符串。也可以在类定义中定义变量和方法。
定义一个名为Point
的类将创建一个类对象。
>>> Point
<class '__main__.Point'>
因为Point
是在顶层定义的,所以它的“全名”是__main__ .Point
。
类对象类似于创建对象的工厂。要创建一个点,可以像调用函数一样调用Point
。
>>> blank = Point()
>>> blank
<__main__.Point object at 0xb7e9d3ac>
返回值是对Point
对象的引用,我们将其赋值为blank
。
创建新对象称为实例化,对象是类的实例。
当打印一个实例时,Python会告诉你它属于什么类以及它存储在内存中的什么位置(前缀0x
表示十六进制)。
每个对象都是某个类的实例,因此“对象”和“实例”是可互换的。但在本章中,使用“实例”表示正在讨论的是一个自定义的类型。
.
可以使用点符号为实例赋值:
>>> blank.x = 3.0
>>> blank.y = 4.0
语法类似于从模块中选择变量的语法。如math.pi,string.whitespace
。在本例中,为对象的元素赋值,这些元素称为属性。
变量blank
指向一个Point
对象,该对象包含两个属性。每个属性都引用一个浮点数。
可以使用相同的语法读取属性的值:
>>> blank.y
4.0
>>> x = blank.x
>>> x
3.0
表达式blank.x
的意思是,“到blank
所指的对象的地址,得到x的值。” 在这个例子中,我们将这个值赋给一个名为x
的变量。变量x
和属性x
之间没有冲突。
可以使用点符号作为任何表达式的一部分。例如:
>>> '(%g, %g)' % (blank.x, blank.y)
'(3.0, 4.0)'
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> distance
5.0
也可以将实例作为一般参数进行传递。例如:
def print_point(p):
print('(%g, %g)' % (p.x, p.y))
函数print_point
接受一个点作为参数,并用数学方式显示它。要调用它,可以将blank
作为参数传递:
>>> print_point(blank)
(3.0, 4.0)
在函数内部,p
是blank
的别名,因此如果函数修改了p
, blank
也会发生变化。
.
有时对象的属性应该是什么是显而易见的,但有时你必须做出决策。例如,假设要设计一个类来表示矩形。应该使用哪些属性来指定矩形的位置和大小?为了简单起见,假设矩形是垂直的或水平的。
可以指定矩形的一个顶点(或中心)、宽度和高度,也可以指定两个对角。很难说哪一个更好,所以选择第一种方式作为例子。
类可以这样定义:
class Rectangle:
"""Represents a rectangle.
attributes: width, height, corner.
"""
文档字符串列出了属性:width, height
是数字,corner
是指定左下角的Point
对象。
要表示矩形,必须实例化矩形对象并为属性赋值:
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0
表达式box.corne.x
表示 ”到对象box
所指的内存中,选择名为corner
的属性,然后转到那个对象并选择名为x
的属性。”
.
函数可以返回实例。例如,find_center
以Rectangle
为参数,返回一个包含矩形中心坐标的Point
对象:
def find_center(rect):
p = Point()
p.x = rect.corner.x + rect.width/2
p.y = rect.corner.y + rect.height/2
return p
下面是一个例子,它将box
作为参数传递,并将结果Point
赋给center
:
>>> center = find_center(box)
>>> print_point(center)
(50, 100)
.
可以通过为对象的一个属性赋值来更改对象的状态。例如,要在不改变矩形位置的情况下改变矩形的大小,可以修改width
和height
的值:
box.width = box.width + 50
box.height = box.height + 100
还可以编写修改对象的函数。例如,grow_rectangle
接受一个矩形对象Rectangle
和两个数字dwidth
和dheight
,并将这些数字添加到矩形的宽度和高度:
def grow_rectangle(rect, dwidth, dheight):
rect.width += dwidth
rect.height += dheight
下面是一个例子,展示了这种效果:
>>> box.width, box.height
(150.0, 300.0)
>>> grow_rectangle(box, 50, 100)
>>> box.width, box.height
(200.0, 400.0)
在函数内部,rect
是box
的别名,因此当函数修改rect
时,box
将发生更改。
.
引用会使程序难以阅读,因为一个地方的变化可能会在另一个地方产生意想不到的效果。很难跟踪所有可能引用给定对象的变量。
复制对象通常是引用的一种替代方法。copy
模块包含一个名为copy
的函数,可以复制任何对象:
>>> p1 = Point()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> import copy
>>> p2 = copy.copy(p1)
p1
和p2
包含相同的数据,但它们不是同一个Point
对象。
>>> print_point(p1)
(3, 4)
>>> print_point(p2)
(3, 4)
>>> p1 is p2
False
>>> p1 == p2
False
is
操作符表示p1
和p2
不是同一个对象。但是您可能认为使用 ==
会产生True
,因为这两个Point
对象包含相同的数据。但在这种情况下,==
操作符的默认行为与is
操作符相同,它检查对象标识,而不是对象的属性。这是因为对于自定义的类型,Python不知道什么应该被认为是等价的。
如果使用copy.copy
复制一个矩形对象Rectangle
,您会发现它复制的是矩形对象Rectangle
,而不是嵌入点对象Point
。
>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True
这个操作称为浅复制,因为它复制对象及其包含的任何引用,但不复制嵌入的对象。
多数情况下这不是想要的。在本例中,在其中一个矩形上调用grow_rectangle
不会影响另一个矩形,但是在其中一个矩形上调用move_rectangle
会影响两个矩形!这种行为容易混淆和出错。
幸运的是,copy
模块提供了一个名为deepcopy
的方法,该方法不仅复制对象,还复制它引用的对象,以及它们引用的对象,等等。
>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False
box3
和box
是完全独立的对象。
python3中类的引用和赋值操作是将原对象所指的地址赋值给新的对象,即两个对象所代表的内容储存在同一块内存空间中:
>>> box
<__main__.Rectangle object at 0x00000212CCF6E470>
>>> box4=box
>>> box4
<__main__.Rectangle object at 0x00000212CCF6E470>
>>> box4 == box
True
所以修改操作会对所有指向这块空间的对象产生影响。同理,一个对象中包函其他类的对象,在其空间中存储的是那个类的地址,浅复制只复制这类的存储空间中的内容,所以如果有其他类的话只复制了其地址。而深复制则会进一步到这个地址所指的位置复制这个类的内容。
>>> box5 = copy.copy(box)
>>> box
<__main__.Rectangle object at 0x00000212CCF6E470>
>>> box5
<__main__.Rectangle object at 0x00000212CCE54550>
>>> box.corner
<__main__.Point object at 0x00000212CCFC6C18>
>>> box5.corner
<__main__.Point object at 0x00000212CCFC6C18>
>>> box6 = copy.deepcopy(box)
>>> box6.corner
<__main__.Point object at 0x00000212CCFDAD30>
.
当你开始处理对象时,可能会遇到一些新的异常。如果你试图访问一个不存在的属性,你会得到一个AttributeError
:
>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'
如果不确定对象的类型,可以使用type
函数:
>>> type(p)
<class '__main__.Point'>
也可以使用isinstance
来检查一个对象是否是一个类的实例
>>> isinstance(p, Point)
True
如果不确定对象是否具有特定属性,可以使用内置函数hasattr
:
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
第一个参数可以是任何对象;第二个参数是一个包含属性名称的字符串。
还可以使用try语句查看对象是否具有需要的属性:
try:
x = p.x
except AttributeError:
x = 0
这种方法可以更容易地编写使用不同类型的函数。
返回目录
.
Time
定义一个名为Time
的类,来表示一天中的时间。类定义如下:
class Time:
"""Represents the time of day.
attributes: hour, minute, second
"""
创建一个新的时间对象,并分配小时、分钟和秒属性:
time = Time()
time.hour = 11
time.minute = 59
time.second = 30
编写一个名为print_time
的函数,该函数接受一个Time
对象并以 小时:分:秒 的形式打印它。
>>> def print_time(t1):
print(' %.2d:%.2d:%.2d ' % (t1.hour, t1.minute, t1.second) )
>>> print_time(time)
11:59:30
.
在接下来的几节中,将编写两个函数来做时间加法,使用了两种函数类型:纯函数和修改器。也演示了作者称之为 prototype 和 patch 的开发计划,这是一种通过从一个简单的原型开始,逐步处理复杂问题来解决复杂问题的方法。
下面是add_time
函数的一个简单原型:
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
return sum
该函数创建一个新的Time
对象,初始化它的属性,并返回对新对象的引用。这种函数称为纯函数,因为它不修改任何作为参数传递给它的对象,并且除了返回值之外,它没有显示值或获取用户输入之类的效果。
为了测试这个函数,创建两个时间对象:start
包含电影的开始时间,duration
包含电影的运行时间,即1小时35分钟。add_time
计算出电影什么时候结束。
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 0
>>> duration = Time()
>>> duration.hour = 1
>>> duration.minute = 35
>>> duration.second = 0
>>> done = add_time(start, duration)
>>> print_time(done)
10:80:00
因为这个函数不处理秒数或分钟数之和超过60的情况,所以结果为10:80:00
,下面是一个改进的版本:
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
if sum.second >= 60:
sum.second -= 60
sum.minute += 1
if sum.minute >= 60:
sum.minute -= 60
sum.hour += 1
return sum
虽然这个函数是正确的,但是它开始变大了。稍后我们将看到一个更短的替代方案。
.
有时候,需要通过函数修改通过参数传递的对象。在这种情况下,更改对调用者是可见的。以这种方式工作的函数称为修改器 modifiers。
increment
函数为Time
对象添加给定的秒数,可以自然地写成修改器。如:
def increment(time, seconds):
time.second += seconds
if time.second >= 60:
time.second -= 60
time.minute += 1
if time.minute >= 60:
time.minute -= 60
time.hour += 1
这个函数还存在问题,如果秒数远远大于60还是错的,在这种情况下,需要循环操作直到time.second
小于60。
任何可以用修改器完成的操作也可以用纯函数完成。事实上,一些编程语言只允许纯函数。有一些证据表明,与使用修改器的程序相比,使用纯函数的程序开发速度更快,更不容易出错。但是修改器有时很方便,功能程序的效率往往较低。
一般来说,建议在合理的情况下编写纯函数,只有在具有明显优势时才使用修饰符。这种方法可以称为函数式编程风格。
.
正在演示的开发计划作者称之为“prototype and patch”。对于每一个功能首先编写一个原型来执行基本的计算,然后对其进行测试,并在此过程中对错误进行了修补。
这种方法是有效的,尤其是当你还没有对问题有深刻的理解的时候。但是增量更正会生成不必要的复杂代码,因为它处理许多特殊情况,而且不可信赖,因为很难知道是否找到了所有的错误。
另一种方法是设计式开发designed development,在这种方法中,对问题的深度理解可以使编程变得容易得多。在本例中,可以发现Time
对象实际上是以60为基数的三位数。second
属性是“基数为1的列”,minute
属性是“基数为60的列”,hour
属性是“基数为3600的列”。
当写add_time
和increment
时,实际上是在做以60为底的加法,这就是为什么必须从一列进位到下一列。
这个观察结果提出了解决整个问题的另一种方法——可以把Time
对象转换成整数,便于计算机计算。
下面的函数将时间转换为整数:
def time_to_int(time):
minutes = time.hour * 60 + time.minute
seconds = minutes * 60 + time.second
return seconds
下面函数整数转换为时间(divmod
用第二个参数除第一个参数,并以元组的形式返回商和余数):
def int_to_time(seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
为了证明这些函数是正确的,可以针对不同的x
的值,测试time_to_int(int_to_time(x)) == x
:
一旦确信它们是正确的,可以用它们来重写add_time
:
def add_time(t1, t2):
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
这个版本比原来的版本更短,而且更容易验证。
在某种程度上,从60进制和10进制的转换比直接处理时间要困难得多。但是,如果把时间看作是以60为基数的数字,并在编写转换函数(time_to_int
和int_to_time
)上投入精力,就会得到一个更短、更容易阅读和调试、更可靠的程序。
以后添加其他特性也更容易。例如做两个时间的减法,最简单的方法是借位减法。但使用转换函数将更容易,正确率也更高。
.
如果分钟和秒的值在0到60之间(包括0但不包括60),且小时为正,则Time
对象是格式正确的。小时和分钟应该是整数值,但秒可以有小数部分。
像这样的需求称为不变量,因为它们应该总是正确的。换句话说,如果它们不是真的,那一定是出了问题。
编写检查不变量的代码可以帮助检测错误并找到错误的原因。例如,可能有一个像valid_time
这样的函数,它接受一个Time
对象,如果这个对象违反了一个不变式,则返回False
:
def valid_time(time):
if time.hour < 0 or time.minute < 0 or time.second < 0:
return False
if time.minute >= 60 or time.second >= 60:
return False
return True
在每个函数的开始,可以检查参数是否有效:
def add_time(t1, t2):
if not valid_time(t1) or not valid_time(t2):
raise ValueError('invalid Time object in add_time')
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
或者也可以使用assert
语句,它检查给定的不变式,如果失败,则引发异常:
def add_time(t1, t2):
assert valid_time(t1) and valid_time(t2)
seconds = time_to_int(t1) + time_to_int(t2)
return int_to_time(seconds)
assert
语句非常有用,因为它们将处理正常条件的代码与检查错误的代码区分开来。
返回目录
虽然我们使用了Python的一些面向对象的特性,但是前两章中的程序并不是真正的面向对象的,因为它们没有展示自定义类型和操作它们的函数之间的关系。下一步是将这些函数转换为显式关系的方法。
.
Python是一种面向对象的编程语言,这意味着它提供了支持面向对象编程的特性:
例如,第16章定义的Time
类对应于人们记录时间的方式,定义的函数对应于人们处理时间的方式。类似地,第15章中的Point
和Rectangle
类对应于数学概念中的点和矩形。
到目前为止,我们还没有利用Python提供的支持面向对象编程的特性。这些特性并不是严格必需的,它们中的大多数有替代语法。但在许多情况下,这种特性更简洁,更准确地表达了程序的结构。
例如,上一章的例子中,类定义和后面的函数定义之间没有明显的联系。但很明显,每个函数都至少使用一个Time
对象作为参数。这促使了方法的提出,方法是与特定类相关联的函数。在前面已经看到了字符串、列表、字典和元组的方法。在本章中将为自定义的类型定义方法。
方法在语义上与函数相同,但在句法上有两个不同之处:
在接下来的几节中,我们将把前两章中的函数转换成方法。这个变换完全是机械的,可以按照一系列的步骤来做。如果你能很自然的从一种形式转换到另一种形式,就可以为你正在做的事情选择最好的形式。
.
在第16章中,我们定义了一个名为Time
的类,在第16.1节中,编写了一个名为print_time
的函数:
class Time:
"""Represents the time of day."""
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
要调用这个函数,必须传递一个Time
对象作为参数:
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00
要使print_time
成为一个方法,所要做的就是将函数定义移动到类定义中。注意缩进的变化:
class Time:
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
现在有两种方法调用print_time
。第一种(也是不太常见的)方法是使用函数语法:
>>> Time.print_time(start)
09:45:00
Time
是类的名称,print_time
是方法的名称,start
作为参数传递。
第二种(也是更简洁的)方法是使用方法语法:
>>> start.print_time()
09:45:00
print_time
是方法的名称,start
是方法所调用的对象,该对象称为主体subject
。正如句子的主语是句子所讲的主题一样,方法调用的主语也是方法的主体。
在方法内部,subject被分配给第一个参数,因此在本例中start
被分配给time
。
按照惯例,方法的第一个参数被称为self
,因此更常见的写法是这样的print_time
:
class Time:
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
这种惯例的原因是一个隐含的隐喻:
print_time(start)
的语法表明函数是活动代理。它会这样说:“嘿,print_time
!这是你要打印的东西。”start.print_time()
这样的方法调用是说 “嘿,start
!请打印自己。”这种观点上的改变可能更有礼貌,但它是否有用并不明显。在我们目前看到的例子中,可能不是这样。但是,有时将职责从函数转移到对象上可以编写更多功能的函数(或方法),并使代码更容易维护和重用。
.
这是increment
重写为方法的一个版本:
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
这里假设time_to_int
被编写为一个方法。另外,请注意它是一个纯函数,而不是修饰符。
调用increment
方法:
>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17
主体start
被分配给第一个参数self
。参数1337
被分配给第二个参数seconds
。
这种机制可能会令人困惑,尤其是在出错的情况下。例如,如果使用两个参数调用increment
,将得到:
>>> end = start.increment(1337, 460)
TypeError: increment() takes 2 positional arguments but 3 were given
错误消息很令人困惑,因为括号中只有两个参数。但是这个主体也被认为是一个参数,所以总共是三个。
顺便说一下,位置参数是没有参数名的参数,也就是说,它不是一个关键字参数。在这个函数调用中:
sketch(parrot, cage, dead=True)
parrot
和cage
都是位置参数,而dead
是一个关键字参数。
.
init
方法是在对象实例化时调用的特殊方法。它的全名是__init__
(init
前后各两个下划线)。Time
类的init
方法如下:
# inside class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
通常情况下,_init__
的参数具有与属性相同的名称。语句
self.hour = hour
将参数hour
的值存储为self
的属性。
这些参数是可选的,因此如果调用Time
时没有参数的,就会得到默认值:
>>> time = Time()
>>> time.print_time()
00:00:00
如果提供一个参数,则覆盖超过hour
:
>>> time = Time (9)
>>> time.print_time()
09:00:00
如果提供两个参数,则覆盖hour
和minute
:
>>> time = Time(9, 45)
>>> time.print_time()
09:45:00
如果提供三个参数,它们将覆盖所有三个默认值。
.
__str__
像__init__
一样,是一个特殊的方法,返回一个对象的字符串表示形式。
下面是一个Time
对象的str
方法:
# inside class Time:
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
当打印对象时,Python调用str
方法:
>>> time = Time(9, 45)
>>> print(time)
09:45:00
当我编写一个新类时,我几乎总是先编写__init__
,这使得实例化对象更加容易,而__str__
对于调试也非常有用。
.
通过定义其他特殊方法,可以指定操作符对自定义的类型的操作。例如,如果为Time
类定义__add__
方法,就可以对Time
对象使用+
运算符。
这个定义是这样的:
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
可以这样使用它:
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
当将+
操作符应用于Time
对象时,Python将调用__add__
方法。当打印结果时,Python调用了__str__
,所以背后发生了很多事情!
更改操作符的行为,使其对自定义的类型有效,这称为操作符重载。Python中的每个操作符,都有一个对应的特殊方法,比如__add__
。有关详细信息,请参见操作符方法。
.
在上一节中,我们对两个Time
对象相加,但是有时也将Time
对象和一个整数相加。下面是版本的__add__
根据other
的类型调用add_time
或increment
:
# inside class Time:
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
def add_time(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
内置函数isinstance
接受一个值和一个类对象,如果该值是该类的一个实例,则返回True
。
如果other
是Time
对象,则__add__
调用add_time
。否则,它假定参数是一个数字并调用increment
。这个操作称为基于类型调用,因为它根据参数的类型调用不同的方法。
下面是不同类型使用+
运算符的例子:
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
>>> print(start + 1337)
10:07:17
但加法的实现不是可交换的。如果整数是第一个操作数,就得到
>>> print(1337 + start)
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
错误是:Python被要求为整数添加一个Time
对象,而不是为Time
对象添加一个整数,它不知道如何添加。对于这个问题有一个巧妙的解决方案:特殊方法__radd__
,它代表“右加”。当Time
对象出现在+
操作符的右侧时,将调用此方法。下面是定义:
# inside class Time:
def __radd__(self, other):
return self.__add__(other)
用法如下:
>>> print(1337 + start)
10:07:17
.
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'egg': 1, 'spam': 4}
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00
In general,
>>> p = Point(3, 4)
>>> vars(p)
{'y': 4, 'x': 3}
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
返回目录
.
返回目录
返回目录
程序中常见错误有三种: 语法错误(syntax errors)、运行时错误(runtime errors)和语义错误(semantic errors)。为了更快地找到它们,区分它们是有用的。
Syntax error:
“Syntax”是指程序的结构及其规则。例如,圆括号必须成对出现,所以
(1 + 2)
是合法的,而8)
是语法错误。
如果程序中任何地方出现语法错误,Python将显示错误消息并退出,您将无法运行程序。
Runtime error:
第二类错误是运行时错误,之所以称为运行时错误,是因为该错误运行程序时才出现。这些错误也称为异常(exceptions), 因为它们通常表示发生了异常事件。
Semantic error:
语义错误与程序意义相关。如果程序中存在语义错误,它将在不生成错误消息的情况下运行,但不会产生正确结果。
识别语义错误可能比较棘手,因为它要求您通过查看程序的输出并找出它在做什么来调试。
返回目录
返回目录
返回目录