本文共 11324 字,大约阅读时间需要 37 分钟。
本章简要地介绍面向对象编程(简称OOP)。OOP是一种组织程序的方法,提倡仔细设计和代码重用。大多数现代编程语言都支持OOP,事实证明这是一种组织和创建大型程序的实用方式。
从本质上说,对象是一组数据以及操作这些数据的函数。本书一直在使用 Python对象,因为数字、字符串、列表、字典和函数都是对象。
要创建新型对象,必须先创建类。从本质上说,类就是设计蓝图,用于创建特定类型的对象。类指定了对象将包含哪些数据和函数,还指定了对象与其他类的关系。对象封装了数据以及操作这些数据的函数。
一个重要的OOP功能是继承:创建新类时,可让它继承既有类的数据和函数。妥善地使用继承可避免重机关报编写是代码,还可让程序更容易理解。
10.1 编写类
下面就来介绍OOP——编写一个表示人的简单类:
#person.pyclass Person: """Class to represent ap person """ def __init__(self): self.name = '' self.age = 0
上述代码定义了一个名为Person的类。它定义了Person对象包含的数据和函数。Person类很简单,它包含数据name和age当前唯一一个函数是__init__,这是用于初始化对象值 的标准函数。正如你将看到的,你创建Person对象时,Python将自动调用__init__.
术语说明
在有些OOP语言中,__init__被称为构造函数,因为它构造对象。每次创建新对象时,都将调用构造函数。在Java和C++等语言中,创建对象时需要使用关键字new。
在类中定义的函数称为方法。与__init__一样,方法的第一个参数必须是self.
可能像下面这样使用Person对象:
要创建Person对象,只需调用Person()。这导致Python运行Person类的函数__init__,并返回一个新的Person对象。
变量age和name包含在对象中,因此每个新创建的Person对象都有自己的age和name。要访问age和name,必须使用句点表示法指定存储它们的对象。
参数self
你可能注意到了,我们调用Person()时没有提供任何参数,但函数__init__(self)期望获得名为self的输入。这是因为在OOP中,self是一个指向对象本身的变量,(相当于C++中的this.)
如图10-1所示。
10.2 显示对象
前面说过,方法是在类中定义的函数。下面给Person类添加一个方法,用于打印Person对象的内容 :
#person2.pyclass Person: """Class to represent a person """ def __init__(self): self.name = '' self.age = 0 def display(self): print("Person('%s', %d)" % (self.name, self.age))
方法display将Person对象的内容以适合程序员阅读的格式打印到屏幕上:
方法dispaly的效果很好,但我们还可以做得更好:Python提供了一些特殊方法,让你能够定制对象以支持天衣无缝的打印。例如,特殊方法__str__用于生成对象的字符串表示:
#person3.pyclass Person: """Class to represent a person """ def __init__(self): self.name = '' self.age = 0 def display(self): print("Person('%s', %d)" % (self.name, self.age)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age)
现在我们可以这样编写代码:
还可使用str来简化方法display:
#person4.pyclass Person: """Class to represent a person """ def __init__(self): self.name = '' self.age = 0 def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age)
运行:
你还可以定义特殊方法__repr__,它返回对象的“官方”(official)表示。例如,Person对象的默认官方表示不太实用:
通过添加方法__repr__,我们可控制这里打印的字符串。在大多数类中,方法__repr__都与方法__str__相同:
#person5.pyclass Person: """Class to represent a person """ def __init__(self): self.name = '' self.age = 0 def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age) def __repr__(self): return str(self)
现在Person对象使用起来更容易:
10.3 灵活的初始化
当前,要创建具有特定姓名和年龄的Person对象,必须这样做:
一种更方便的方法是,在构造对象时将姓名和年龄传递给__init__。为此,需要重写__init__:
#person6.pyclass Person: """Class to represent a person """ def __init__(self, name = '', age = 0): self.name = name self.age = age def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age) def __repr__(self): return str(self)
这样,初始化Person对象将简单得多:
由于__init__的参数有默认值 ,你甚至可以创建“空的”Person对象:
注意方法__init__,我们在其中使用了self.name和name(以及self.name和age)。变量name指向传入__init__的值,而self.name指向存储在对象中的值。使用self更清楚地指出了谁是谁。
10.4 设置函数和获取函数
当前,我们可以使用句点表示法来读写Person对象的name和age值:
这种做法存在的一个问题是,可能不小心将年龄设置为荒谬的值,如-45, 509。对于常规Python变量,无法对可赋给它的值进行限制。但在对象中,可编写特殊的设置函数(setter)和获取函数(getter),对存取值的方式进行控制。
首先,添加一个设置函数,它公在提供的值合理时才修改age:
#person7.pyclass Person: """Class to represent a person """ def __init__(self, name = '', age = 0): self.name = name self.age = age def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age) def __repr__(self): return str(self) def set_age(self, age): if 0 < age <= 150: self.age = age
运行:
对于这种设置函数,一种常见的抱怨是,输入p.set_age(30)比输入p.age=30更烦琐。为解决这种问题,可使用特性装饰器(property decorator)
10.4.1 特性装饰器
特性装饰器融变量的简单与函数的灵活于一身。装饰器指出函数或方法有点特殊,这里使用它们来指出函数或方法有点特殊,这里使用它们来指示设置函数和获取函数。
获取函数返回变量的值,我们将使用@property装饿饰器来指出这一点:
@property
def age(self):
"""Returns this person's age.
"""
return self._age
这个age方法除必不可少的self外不接受任何参数。我们在它前面加上了@property,指出这里一个获取函数。这个方法的名称将被用于设置变量。
我们还将底层变量self.age重命名为self._age。在对象变量前加上下划线是一种常见的做法,这里使用这种方式 将这个变量与方法age区分开来。你需要将Person类中的每个self.age换成self._age。为保持 一致性,最后也将self.name换成self._name。修改后的Person类类似于下面这样:
#person8.pyclass Person: """Class to represent a person """ def __init__(self, name = '', age = 0): self._name = name self._age = age def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self._name, self._age) def __repr__(self): return str(self) @property def age(self): return self._age def set_age(self, age): if 0 < age <= 150: self._age = age
运行结果:
为给age创建设置函数,我们将方法set_age重命名为age,并使用@age.setter进行装饰:
#person9.pyclass Person: """Class to represent a person """ def __init__(self, name = '', age = 0): self._name = name self._age = age def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self._name, self._age) def __repr__(self): return str(self) @property def age(self): return self._age @age.setter def age(self, age): #设置函数必须在获取函数之后定义 if 0 < age <= 150: self._age = age
完成这些修改后,运行:
由于给age提供了设置函数和获取函数,编写的代码就像直接使用变量age,但差别在于:遇到代码p.age = 230时,Python实际上将调用方法age(self, age);同样,遇到代码p.age时,将调用方法age(self)。这提供了如下优点:赋值语法很简单,同时可控制变量的设置和获取方法。
10.4.2 私有变量
依然可以直接访问self._age:
>>>p._age = -44
>>>p
Person('Lia', 44)
问题在于,直接修改_age可能导致对象不一致,因此通常不希望直接修改的情况发生。
为降低变量self._age被直接修改的可能性,一种方式是将其重命名为self.__age,即在变量名开头包含两个下划线。两个下划线表明age是私有变量,不应在Person类外直接访问它。
#person10.pyclass Person: """Class to represent a person """ def __init__(self, name = '', age = 0): self.__name = name self.__age = age #不以下划线打头的变量是公有变量,任何代码都可访问它们 def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.__name, self.__age) def __repr__(self): return str(self) @property def age(self): return self.__age @age.setter def age(self, age): if 0 < age <= 150: self.__age = age
修改之前:
修改之后运行结果:
要直接访问self.__age,需要在前面中上_Person,如下所示:
这虽然不能禁止你直接修改内部变量,但将无意间这样做的可能性几乎降到了零。
提示
编写大型程序时,一条实用 的经验规则是,首先将所有对象变量都设置为私有的(即以两个下划线打头),再在有充分理由的情况下将其改为公有的。这可避免无意间修改对象内部变量导致的错误
10.5 继承
继承是一种重用类的机制,让你能够这样创建全新的类:给既有类的考贝添加变量和方法。
假设我们要开发一款游戏,其中涉及人类玩家和计算机玩家。为此,可创建一个Player类,它包含玩家都有的东西,如得分和名称:
#players.pyclass Player: def __init__(self, name): self._name = name self._score = 0 def reset_score(self): self._score = 0 def incr_score(self): self._score = self._score + 1 def get_name(self): return self._name def __str__(self): return "name = '%s', score = %s" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)
运行结果:
咱们假设有两类玩家:人和计算机。主要差别在于,人通过键盘输入走法,而计算机使用函数生成走法,除此之外,这两类玩家相同,它们都有名称和得分。
下面来编写一个Human类,用于表示人类玩家。为此,一种办法是通过复制并粘贴新建Player类的一个拷贝,再添加让玩家走棋的方法make_move(self)。这种办法虽然可么,但更佳的做法是使用继承。我们可以让Human类继承Player类的所有变量和方法, 样就不需要再次编写它们了。
#human.pyclass Player: def __init__(self, name): self._name = name self._score = 0 def reset_score(self): self._score = 0 def incr_score(self): self._score = self._score + 1 def get_name(self): return self._name def __str__(self): return "name = '%s', score = %s" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)class Human(Player): #继承Player类 pass
在Python中,pass语句表示“什么都不做”。对Human类来说,这是一个完整而实用的定义,它继承Player的代码,让我们能够像下面这样做:
考虑到我们只为Human类编写两行代码,这相当令人难忘
重写方法
一个小瑕疵是,h的字符串表示为Player,但更准确的说法应该是Human。为修复这种问题,可给Human定义方法__repr__:
#human2.pyclass Player: def __init__(self, name): self._name = name self._score = 0 def reset_score(self): self._score = 0 def incr_score(self): self._score = self._score + 1 def get_name(self): return self._name def __str__(self): return "name = '%s', score = %s" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)class Human(Player): #继承Player类 def __repr__(self): #定制派生类的__repr__ return 'Human(%s)' % str(self)
这就是方法重写:Human中的方法__repr__重写了从Player那里继承的方法__repr__.这是定制派生类的常用方式。
现在,可轻松编写类似的Computer类,用于表示计算机玩家:
class Computer(Player):
def __repr__(self):
return 'Computer(%s)' % str(self)
#computer.pyclass Player: def __init__(self, name): self._name = name self._score = 0 def reset_score(self): self._score = 0 def incr_score(self): self._score = self._score + 1 def get_name(self): return self._name def __str__(self): return "name = '%s', score = %s" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)class Human(Player): #继承Player类 def __repr__(self): #定制派生类的__repr__ return 'Human(%s)' % str(self) class Computer(Player): #继承Player类 def __repr__(self): #定制派生类的__repr__ return 'Computer(%s)' % str(self)
运行结果:
这3个类组成了一个小型的类层次结构,如图10-2的类图所示。Player为基类,而其他两个类为派生(扩展)类。
10.6 多态
为演示OOP的威力,咱们来创建一个名为Undercut的简单游戏。在这个游戏中,两个玩家同时选择一个1——10的整数,如果一个玩家选择的整数比对方选择整数的小1,则该玩家获胜,否则算打平。例如,如果Thomas和Bonnie一起玩游戏Undercut,且他们选择的数字分别为9和10,则Thomas获胜;如果他们分别选择4和7,则打成平手。
#polymorphous.pyimport randomclass Player: def __init__(self, name): self._name = name self._score = 0 def reset_score(self): self._score = 0 def incr_score(self): self._score = self._score + 1 def get_name(self): return self._name def __str__(self): return "name = '%s', score = %s" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)class Human(Player): #继承Player类 def __repr__(self): #定制派生类的__repr__ return 'Human(%s)' % str(self) def get_move(self): while True: try: n = int(input('%s move (1-10: )' % self.get_name())) if 1 <= n <= 10: return n else: print('Oops!') except: print('except-----Oops!')class Computer(Player): #继承Player类 def __repr__(self): #定制派生类的__repr__ return 'Computer(%s)' % str(self) def get_move(self): return random.randint(1, 10)def play_undercut(p1, p2): p1.reset_score() p2.reset_score() m1 = p1.get_move() m2 = p2.get_move() print("%s move: %s" % (p1.get_name(), m1)) print("%s move: %s" % (p2.get_name(), m2)) if m1 == m2 - 1: p1.incr_score() return p1, p2, '%s wins!' % p1.get_name() elif m2 == m1 - 1: p2.incr_score() return p1, p2, '%s wins!' % p2.get_name() else: return p1, p2, 'draw: no winner'
10.6.2 玩游戏Undercut
万事俱备,可以开始玩游戏Undercut了。先让人和计算机来玩款游戏:
必须在函数play_undercut外面创建玩家对象,明白这一点很重要。这是一种良好的设计:函数play_undercut只关心玩游戏,而不关心如何初始化玩家对象。
函数play_undercut返回一个形式为(p1, p2, message)的三元组。其中p1和p2是传入的玩家对象;如果有玩家获胜,则将其得分加1。message是一个字符串,指出了获胜的玩家或打成平手。
也可以将两个计算机玩家传递给函数play_undercut:
这次没有人类玩家,因此不会要求用户输入数字。
还可以传入两个人类玩家:
10.7 更深入地学习
本章介绍了OOP的一些基本知识,Python还有很多其他OOP功能,你可通过阅读在线文档进行学习。
一个重要主题是打造良好的面向对象设计。相比于仅仅使用对象,妥善地使用对象要难得多。为组织面向对象的程序,一种流行的方式是使用面向对象设计模式。面向对象设计模式是使用对象解决常见编程问题的处方,经过了实践的检验。
在探计这个主题的著作中,Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides所著的《设计模式:可复用面向对象软件的基础》影响最为深元。掌握OOP的所有技术细节后,如果要了解重要的设计问题,阅读这部著作将是很不错的选择。
转载地址:http://yyiub.baihongyu.com/