Python之面向对象2
隐藏数据
你可能已经意识到,查看或修改对象中的数据(属性)有两种方法。可以直接访问,像这样:
myDog.cooked_level = 5
或者也可以使用修改属性的方法,例如:
myDog.cook(5)
如果热狗开始时是生的(cooked_level = 0),这两种做法的作用相同。它们都会把cooked_level设置为5。那么为什么还要那么麻烦,专门建立一个方法来做这个工作呢?为什么不直接修改呢?
我可以想到至少两个原因。
• 如果直接访问属性,烤热狗至少需要两部分:改变cooked_level和改变cooked_string。而利用一个方法,可以只做一个方法调用,它就会完成我们需要的一切工作。
• 如果直接访问属性,就会有这样的结果:
cooked_level = cooked_level - 2
这会使热狗比以前还生。不过热狗肯定不会越烤越生!所以这是毫无意义的。通过使用方法,可以确保cooked_level只会增加而不会减少。
术语箱
按编程术语来讲,如果限制对对象数据的访问,使得只能通过使用方法来获取和修改这些数据,就称为数据隐藏(data hiding)。Python没有提供任何途径来保证数据隐藏,不过如果你愿意,可以适当地编写代码来遵循这个规则。
目前为止,我们已经看到对象包含属性和方法。而且了解了如何创建对象以及如何利用一个名为__init__()的特殊方法初始化对象。我们还看到了另一个特殊方法__str__(),利用这个方法可以更好地打印我们的对象。
多态和继承
接下来,我们来看对象最为重要的两个方面:多态(polymorphism)和继承(inheritance)。这两个词很长很深奥,不过正是因为有这两个方面,才使得对象如此有用。我会在下面几节清楚地解释它们的含义。
多态——同一个方法,不同的行为
非常简单,多态是指对于不同的类,可以有同名的两个(或多个)方法。取决于这些方法分别应用到哪个类,它们可以有不同的行为。
例如,假设你要建立一个程序做几何题,需要计算不同形状的面积,比如三角形和正方形。你可以创建两个类,如下:
Triangle类和Square类都有一个名为getArea()的方法。所以,如果分别有这两个类的实例,如下:
>>> myTriangle = Triangle(4, 5)
>>> mySquare = Square(7)
就可以使用getArea()分别计算它们的面积:
>>> myTriangle.getArea()
10.0
>>> mySquare.getArea()
49
这两个形状都使用了方法名getArea(),不过每个形状中这个方法做的工作不同。这就是一个多态的例子。
继承——向父母学习
在真实的(非编程)世界中,人们可以从他们的父母或者其他亲戚那里继承一些东西。你可以继承一些特征,比如说红头发,或者可以继承像钱和财产之类的东西。
在面向对象编程中,类可以从其他类继承属性和方法。这样就有了类的整个“家族”,这个“家族”中的每个类共享相同的属性和方法。这样一来,每次向“家族”增加新成员时就不必从头开始。
从其他类继承属性或方法的类称为派生类(derived class)或子类(subclass)。可以举一个例子来解释这个概念。
假想我们要建立一个游戏,玩家一路上可以捡起不同的东西,比如食物、钱或衣服。可以建一个类,名为GameObject。GameObject类有name等属性(例如coin、apple或hat)和pickUp()等方法(它会把硬币增加到玩家的物品集合中)。所有游戏对象都有这些共同的方法和属性。
然后,可以为硬币建立一个子类。Coin类从GameObject派生。它要继承GameObject的属性和方法,所以Coin类会自动有一个name属性和pickUp()方法。Coin类还需要一个value属性(这个硬币价值多少)和一个spend()方法(可以用这个硬币去买东西)。
下面来看这些类的代码:
未雨绸缪
在上面的例子中,我们并没有在方法中加入任何实际代码,只有一些注释来解释这些方法要做什么。这是一种未雨绸缪的方法,是对以后要增加的内容提前做出计划或提前考虑。具体的代码要取决于游戏如何工作。程序员编写比较复杂的代码时通常就会采用这种做法来组织他们的想法。“空”函数或方法称为代码桩(code stub)。
如果想运行前面的例子,会得到一条错误消息,因为函数定义不能为空。
没错,Carter,不过注释不起作用,因为它们只是给你读的,而不是让计算机来执行。
如果希望建立一个代码桩,可以使用Python的pass关键字作为一个占位符。代码实际上应该像下面这样:
我不打算再在这一章中给出使用对象、多态和继承的更详细的例子。学习这本书后面的内容时还会看到很多关于对象以及如何使用对象的例子。通过在实际的程序(比如游戏)中使用对象,你会有更深入的理解。
动手试一试
1.为BankAccount建立一个类定义。它应该有一些属性,包括账户名(一个字符串)、账号(一个字符串或整数)和余额(一个浮点数),另外还要有一些方法显示余额、存钱和取钱。
2. 建立一个可以挣利息的类,名为 InterestAccount。这应当是 BankAccount 的一个子类(所以会继承 BankAccount 的属性和方法)。InterestAccount 还应当有一个对应利息率的属性,另外有一个方法来增加利息。为了力求简单,假设每年会调用一次 addInterest() 方法计算利息并更新余额。
模块
这是讨论收集方式的最后一章(This is the last chapter that talks about ways of collecting things together.)。前面已经了解了列表、函数和对象,这一章中我们将学习模块。下一章中,我们将使用一个名为Pygame的模块开始画一些图形。
1 什么是模块
模块就是某个东西的一部分。如果一个东西可以分为几部分,或者你可以很容易地把它分解成多个不同部分,我们就说这个东西是模块化的。乐高(LEGO)积木可能就是模块化最好的例子。可以拿一堆不同的积木,用它们搭建不同的东西。
在Python中,模块(module)是包含在一个更大程序中的类似的部分。每个模块或部分都是硬盘上的一个单独的文件。可以把一个大程序分解为多个模块或文件。或者也可以反过来,从一个小的模块开始,逐渐增加其他部分来建立一个大程序。
2 为什么使用模块
为什么要那么麻烦地把程序分解为较小的部分呢?要知道我们需要所有这些部分才能让程序正常工作。为什么不直接把所有内容都放在一个大文件中呢?
原因有几个。
- 这样做文件会更小,因而就能更容易地查找代码。
- 一旦创建模块,这个模块就能在很多程序中使用。这样下一次需要相同的功能时就不必再从头开始了。
- 并不是所有模块都要使用。模块化意味着你可以使用各部分的不同组合来完成不同的任务,就像利用同样的一组乐高积木可以搭建不同的东西一样。
3 积木桶
在关于函数的第13章中,我们说过函数就像积木,那么模块可以认为是一桶积木。根据需要,你可以从一个桶中取很多或者很少的积木,也可以有很多桶不同的积木。也许有一桶正方形积木,一桶长方形积木, 还有一桶奇形怪状的积木。程序员通常也采用这种方法来使用模块,也就是说,他们会把类似的函数收集在一个模块中。或者他们也有可能把一个项目需要的所有函数收集在一个模块中,就像你会把搭城堡需要的所有积木都放在一个桶中一样。
4 如何创建模块
下面来创建模块。模块就是一个Python文件,类似代码清单15-1中给出的文件。在一个IDLE编辑器窗口中键入代码清单1中的代码,把它保存为my_module.py。
就这么简单!这样就创建了一个模块!模块中只有一个函数,也就是c_to_f()函数,它会把温度从摄氏度转换为华氏度。
接下来我们在另一个程序中使用my_module.py。
5 如何使用模块
要使用模块中的某个函数,首先必须告诉Python我们想要使用哪些模块。在程序中包含其他模块的Python关键字是import。可以这样使用:
import my_module
下面写一个程序来使用我们刚才编写的模块,这里我们想用c_to_f()函数完成温度转换。
前面已经了解了如何使用函数并向它传递参数。这里惟一不同的是,函数与主程序不在同一个文件中,而在另外的一个单独的文件中,所以必须使用import。代码清单15-2中的程序使用了我们刚才编写的模块my_module.py。
创建一个新的IDLE编辑器窗口,键入这个程序。保存为modular.py,然后运行这个程序,看看会发生什么。需要把它保存到my_module.py所在的同一个文件夹(或目录)下。
能正常工作吗?应该会看到类似下面的结果:
Enter a temperature in Celsius: 34
Traceback (most recent call last):
File "C:/local_documents/Warren/PythonBook/Sample programs/modular.py",
line 3, in -toplevel-
fahrenheit= c_to_f(celsius)
NameError: name 'c_to_f' is not defined
程序不能正常工作!怎么回事?错误消息指出函数c_to_f()没有定义。不过我们很清楚前面已经在my_module中定义了这个函数,而且我们确实已经导入了这个模块。
出现这个问题的原因是,在Python中指定在其他模块中定义的函数时必须更加具体。解决这个问题的一种方法是把这一行代码
fahrenheit = c_to_f(celsius)
改为
fahrenheit = my_module.c_to_f(celsius)
现在我们向Python特别指出:c_to_f()函数在my_module模块中。做了这个修改后再试着运行程序,看看能不能正常工作。
6 命名空间
Carter提到的内容与命名空间(namespace)概念有关。这个话题有点复杂,不过确实需要知道,所以现在就来讨论这个概念。
什么是命名空间
假设在你们学校,你在Morton老师的班里,班里有个学生名叫Shawn。现在Wheeler老师教的那个班也有一个名叫Shawn的学生。如果你在自己的班里说“Shawn有一个新书包”时,你们班的所有人都会知道(或者至少他们会认为),你指的是你们班的Shawn。如果你想说另外那个班的Shawn就会说“Wheeler老师班里的Shawn”或者“另外那个Shawn”,或者其他类似的说法。
你们班里只有一个Shawn,所以你说Shawn时,同班的同学就会知道你说的是哪个人。换种说法来讲,在你们班的这个空间里,只有一个名字Shawn。你们班就是你的命名空间,在这个命名空间里只有一个Shawn,所以不会有混淆。
现在,如果校长必须通过学校的广播系统把Shawn叫到办公室,她不会说“请Shawn到办公室来一趟”。如果她这样做,两个Shawn都会出现在他的办公室。对于使用广播系统的校长来说,命名空间是整个学校。这说明,学校的每一个人都会听到这个名字,而不只是一个班的同学。所以她必须更明确地指出她指的是哪一个Shawn。她必须这样说:“请Morton老师班里的Shawn到办公室来一趟。”
校长还可以用另一种方法找Shawn,就是走到你们班门口说:“Shawn,请跟我来。”这里只有一个Shawn听到,所以校长能找到真正要找的那个Shawn。在这种情况下,命名空间就只是一个教室,而不是整个学校。
一般来讲,程序员把较小的命名空间(比如你的教室)称作局部命名空间,而较大的命名空间(如整个学校)称为全局命名空间。
导入命名空间
下面假设你们学校(John Young学校)根本没有一个名叫Fred的人。如果校长通过广播系统想找Fred,她肯定找不到这个人。现在假设与你们学校同在一条街上的另一个学校(Stephen Leacock学校)正在进行部分校舍维修,这个学校把一个班级临时搬到你们学校的活动房里上课。在这个班里,恰好有一个学生名叫Fred。不过这个活动房还没有连上广播系统。如果校长找Fred,肯定还是找不到。但是,如果她把这个新的活动房连入广播系统,然后再找Fred,就会找到Stephen Leacock学校的Fred。
连接另一个学校的活动房屋,这在Python中就像导入一个模块。
导入了模块,就可以访问这个模块中的所有名字,包括所有变量、函数以及对象。
导入模块的含义与导入一个命名空间是一样的。导入模块时,就导入了命名空间。
导入命名空间(模块)有两种方法。可以这样做:
import StephenLeacock
如果这样做,StephenLeacock仍然是一个单独的命名空间。你可以访问这个命名空间,但是在使用之前必须明确地指定想要哪一个命名空间。所以校长必须这样做:
call_to_office(StephenLeacock.Fred)
如果校长想找到Fred,除了名字(Fred)外,她还必须给出命名空间(Stephen Leacock)。在前面的温度转换程序中就是这样做的。
为了让这个程序正常工作,我们写了这样一行代码:
fahrenheit = my_module.c_to_f(celsius)
这里指定了命名空间(my_module)以及函数名(c_to_f)。
导入命名空间的另一种方法是:
from StephenLeacock import Fred
如果校长这样做,会把StephenLeacock的名字Fred包含到她的命名空间中,现在就可以这样找到Fred:
call_to_office(Fred)
因为Fred现在就在校长的命名空间中,所以她不必再去StephenLeacock命名空间找Fred。
在这个例子中,校长只是从StephenLeacock把名字Fred导入她的局部命名空间中。如果她想导入所有人,可以这样做:
from StephenLeacock import *
在这里,星号(*)表示全部。不过她必须当心,如果Stephen Leacock学校与John Young学校有同名的学生,就会出现混乱了。
到目前为止,你可能对命名空间的概念还是不太清楚。不用担心!通过完成后面几章的例子,你会越来越明白。后面需要导入模块时,我都会清楚地解释要做什么。
7 标准模块
我们已经知道了如何创建和使用模块,是不是总是必须编写我们自己的模块?并不是这样!这正是Python的妙处之一。
Python提供了大量标准模块,可以用来完成很多工作,比如查找文件、报时(或计时)、生成随机数,以及很多其他功能。有时,人们说Python“配有电池”,指的就是Python的所有标准模块。这称为Python标准库。
为什么这些内容必须放在单独的模块中呢?嗯,不是非得这样,不过设计Python的人认为这样会更高效。否则,每个Python程序都必须包含所有可能用到的函数。通过建立单独的模块,就只需包含你真正需要的那些函数。
当然,有些内容(如print、for和if-else)是Python的基本命令,所以这些基本命令不需要一个单独的模块,它们都在Python的主要部分中。
如果Python没有提供合适的模块来完成你想做的工作(如建立一个图形游戏),可以下载另外一些插件模块,它们通常都是免费的!我们在这本书里就包含了一些这样的插件模块,如果使用这本书网站上的安装程序,就会同时安装这些模块。或者,你也完全可以单独安装。
下面来看几个标准模块。
time
利用time模块,能够获取你的计算机时钟的信息,如日期和时间。还可以利用它为程序增加延迟。(有时计算机动作太快,你必须让它慢下来。)
time模块中的sleep()函数可以用来增加一个延迟,也就是说,可以让程序等待一段时间,什么也不做。这就像让你的程序睡眠,正是这个原因,这个函数名叫sleep()。可以告诉它你要它睡多长时间(多少秒)。
代码清单15-3中的程序展示了sleep()函数如何工作。键入这个程序,保存并运行,看看会发生什么。
# Listing 15.3 Putting your program to sleep # 代码清单 15-3让程序睡眠 import time print "How", time.sleep(2) print "are", time.sleep(2) print "you", time.sleep(2) print "today?" |
要注意,调用sleep()函数时,必须在前面加上time.。这是因为,尽管我们已经用import导入了time,但是并没有让它成为主程序命名空间的一部分。所以每次想要使用sleep()函数时,都必须调用time.sleep()。
如果试图这样做:
import time
sleep(5)
这是不行的,因为sleep并不在我们的命名空间中。我们会得到这样一条错误消息:
NameError: name 'sleep' is not defined
不过如果这样导入:
from time import sleep
就会告诉Python,“在time模块中寻找名为sleep的变量(或者函数或对象),把它包含到我的命名空间中。”现在就可以直接使用sleep函数,而不需要再在前面加上time.了:
from time import sleep
print 'Hello, talk to you again in 5 seconds...'
sleep(5)
print 'Hi again'
如果想要得到这种将名字导入局部命名空间带来的方便(这样就无需每次都指定模块名),但是又不知道需要模块中的哪些名字,就可以使用星号(*)把所有名字都导入到我们的命名空间里:
from time import *
*表示“全部”,这样就会从模块导入所有可用的名字。使用这个命令必须特别当心。如果在我们的程序中创建了一个名字,而它与time模块中的一个名字相同,就会出现冲突。用*导入所有名字不是最佳做法,最好只导入你真正需要的部分。
还记得第8章代码清单8-6中的倒计时程序吗?现在你应该知道那个程序中time.sleep(1)的作用了吧。
随机数
random模块用于生成随机数。这在游戏和仿真中非常有用。
下面试着在交互模式中使用random模块:
>>> import random
>>> print random.randint(0, 100)
4
>>> print random.randint(0, 100)
72
每次使用random.randint()时,会得到一个新的随机整数。由于我们为它传递的参数是0和100,所以得到的整数会介于0到100之间。我们在第1章的猜数程序中就是使用random.randint()来创建秘密数。
如果你想得到一个随机的小数,可以使用random.random()。不用在括号里放任何参数,因为random.random()总是会提供一个介于0到1之间的数:
>>> print random.random()
0.270985467261
>>> print random.random()
0.569236541309
如果你想得到其他范围内的一个随机数,比如说0到10之间,只需要将结果乘以10:
>>> print random.random() * 10
3.61204895736
>>> print random.random() * 10
8.10985427783
动手试一试
1.编写一个模块,包含第13章“动手试一试”中的“用大写字母打印名字”函数。然后编写一个程序导入这个模块,并调用这个函数。
2.修改代码清单15-2中的代码,把c_to_f()包含到主程序的命名空间里。也就是说,修改这个代码,
从而可以写:fahrenheit = c_to_f(celsius)
而不是:fahrenheit = my_module.c_to_f(celsius)
3.编写一个小程序,生成1到20之间的5个随机整数的列表,并打印出来。
4.编写一个小程序,要求它工作30秒,每3秒打印一个随机小数