本章简要地介绍面向对象编程(简称OOP)。OOP是一种组织程序的方法,提倡仔细设计和代码重用。大多数现代编程语言都支持OOP,事实证明这是一种组织和创建大型程序的实用方式。
从本质上说,对象是一组数据以及操作这些数据的函数。本书一直在使用 Python对象,因为数字、字符串、列表、字典和函数都是对象。
要创建新型对象,必须先创建类。从本质上说,类就是设计蓝图,用于创建特定类型的对象。类指定了对象将包含哪些数据和函数,还指定了对象与其他类的关系。对象封装了数据以及操作这些数据的函数。
一个重要的OOP功能是继承:创建新类时,可让它继承既有类的数据和函数。妥善地使用继承可避免重机关报编写是代码,还可让程序更容易理解。
#person.py
class 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()。这导致Python运行Person类的函数__init__,并返回一个新的Person对象。
变量age和name包含在对象中,因此每个新创建的Person对象都有自己的age和name。要访问age和name,必须使用句点表示法指定存储它们的对象。
你可能注意到了,我们调用Person()时没有提供任何参数,但函数__init__(self)期望获得名为self的输入。这是因为在OOP中,self是一个指向对象本身的变量,(相当于C++中的this.)
前面说过,方法是在类中定义的函数。下面给Person类添加一个方法,用于打印Person对象的内容 :
#person2.py
class 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.py
class 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)
#person4.py
class 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.py
class 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对象,必须这样做:
一种更方便的方法是,在构造对象时将姓名和年龄传递给__init__。为此,需要重写__init__:
#person6.py
class 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)
由于__init__的参数有默认值 ,你甚至可以创建“空的”Person对象:
注意方法__init__,我们在其中使用了self.name和name(以及self.name和age)。变量name指向传入__init__的值,而self.name指向存储在对象中的值。使用self更清楚地指出了谁是谁。
当前,我们可以使用句点表示法来读写Person对象的name和age值:
这种做法存在的一个问题是,可能不小心将年龄设置为荒谬的值,如-45, 509。对于常规Python变量,无法对可赋给它的值进行限制。但在对象中,可编写特殊的设置函数(setter)和获取函数(getter),对存取值的方式进行控制。
#person7.py
class 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)
特性装饰器融变量的简单与函数的灵活于一身。装饰器指出函数或方法有点特殊,这里使用它们来指出函数或方法有点特殊,这里使用它们来指示设置函数和获取函数。
获取函数返回变量的值,我们将使用@property装饿饰器来指出这一点:
这个age方法除必不可少的self外不接受任何参数。我们在它前面加上了@property,指出这里一个获取函数。这个方法的名称将被用于设置变量。
我们还将底层变量self.age重命名为self._age。在对象变量前加上下划线是一种常见的做法,这里使用这种方式 将这个变量与方法age区分开来。你需要将Person类中的每个self.age换成self._age。为保持 一致性,最后也将self.name换成self._name。修改后的Person类类似于下面这样:
#person8.py
class 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.py
class 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)。这提供了如下优点:赋值语法很简单,同时可控制变量的设置和获取方法。
问题在于,直接修改_age可能导致对象不一致,因此通常不希望直接修改的情况发生。
为降低变量self._age被直接修改的可能性,一种方式是将其重命名为self.__age,即在变量名开头包含两个下划线。两个下划线表明age是私有变量,不应在Person类外直接访问它。
#person10.py
class 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,如下所示:
这虽然不能禁止你直接修改内部变量,但将无意间这样做的可能性几乎降到了零。
编写大型程序时,一条实用 的经验规则是,首先将所有对象变量都设置为私有的(即以两个下划线打头),再在有充分理由的情况下将其改为公有的。这可避免无意间修改对象内部变量导致的错误
继承是一种重用类的机制,让你能够这样创建全新的类:给既有类的考贝添加变量和方法。
假设我们要开发一款游戏,其中涉及人类玩家和计算机玩家。为此,可创建一个Player类,它包含玩家都有的东西,如得分和名称:
#players.py
class 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.py
class 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的代码,让我们能够像下面这样做:
一个小瑕疵是,h的字符串表示为Player,但更准确的说法应该是Human。为修复这种问题,可给Human定义方法__repr__:
#human2.py
class 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类,用于表示计算机玩家:
return 'Computer(%s)' % str(self)
#computer.py
class 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为基类,而其他两个类为派生(扩展)类。
为演示OOP的威力,咱们来创建一个名为Undercut的简单游戏。在这个游戏中,两个玩家同时选择一个1——10的整数,如果一个玩家选择的整数比对方选择整数的小1,则该玩家获胜,否则算打平。例如,如果Thomas和Bonnie一起玩游戏Undercut,且他们选择的数字分别为9和10,则Thomas获胜;如果他们分别选择4和7,则打成平手。
#polymorphous.py
import random
class 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'
万事俱备,可以开始玩游戏Undercut了。先让人和计算机来玩款游戏:
必须在函数play_undercut外面创建玩家对象,明白这一点很重要。这是一种良好的设计:函数play_undercut只关心玩游戏,而不关心如何初始化玩家对象。
函数play_undercut返回一个形式为(p1, p2, message)的三元组。其中p1和p2是传入的玩家对象;如果有玩家获胜,则将其得分加1。message是一个字符串,指出了获胜的玩家或打成平手。
也可以将两个计算机玩家传递给函数play_undercut:
本章介绍了OOP的一些基本知识,Python还有很多其他OOP功能,你可通过阅读在线文档进行学习。
一个重要主题是打造良好的面向对象设计。相比于仅仅使用对象,妥善地使用对象要难得多。为组织面向对象的程序,一种流行的方式是使用面向对象设计模式。面向对象设计模式是使用对象解决常见编程问题的处方,经过了实践的检验。
在探计这个主题的著作中,Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides所著的《设计模式:可复用面向对象软件的基础》影响最为深元。掌握OOP的所有技术细节后,如果要了解重要的设计问题,阅读这部著作将是很不错的选择。