10 面向对象的编程
本章内容:
1、编写类
2、显示对象
3、灵活的初始化
4、设置函数和获取函数
5、继承
6、多态
7、更深入的学习
------------------------------------------
本章主要介绍,为什么Python是面向对象的编程语言(OOP)。所以,本章的内容主要围绕面向对象的编程语言的特点展开;
1、编写类
下面,先编写一个表示人的简单类:
# person.py class Person: """ Class to represent a person """ def __init__(self): self.name = '' self.age = 0
__init__ 使用来进行数据初始化的标准函数。
在这个函数中,我们只需要调用 Person() 对象,就可以使用 age 和 name 两个变量,但是这两个变量需要以句点表示的方式来指定数据;
另外一个难点就是 self 参数;在定义变量的时候,我们没有指定参数,而是通过 __init__函数调用了self参数,则是 person() 自己调用自己的意思;所以,self是一个指向对象本身的变量;
2、显示对象
方法是类中定义的函数;下面给上面Person类,添加一个方法,用于打印Person对象的内容:
# person.py class Person: """ Class to represent a person """ def __init__(self0): self.name = '' self.age = 0 def display(self): print("Person('%s',%d)" % (self.name, self.age))
方法display将person对象的内容适合程序眼阅读的方式打印到屏幕上
除了使用 display 的方法外,我们还可以使用其他的方式,来做的更好,让你能够定制对象以支持天衣无缝的打印。例如:特殊方法 __str__ 用于生成对象的字符串表示:
# person.py class Person(): #为了节省篇幅,省略了 __init__ def dsplay(self): print("Person('%s',%d)" % (self.name, self.age)) def __str__(self): return ("Person('%s',%d)" % (self.name, self.age))
运行结果如下:
同样,还可以使用str来简化display:
# person.py class Person: # 为了节省篇幅,省略了 __init__ def display(self): print(str(self)) def __str__(self): return "Person('%s', %d)" % (self.name, self.age)
也可以定义特殊方法 __repr__ ,它返回对象的“官方”表示,例如Person对象的默认官方表示不太实用:
通过添加方法 __repr__,我们可控制这里打印的字符串。在大多数类中,方法 __repr__ 都与方法 __str__ 相同:
# person.py class Person: # 为节省篇幅,省略了 __init__ def display(self): print(str(self)) def __str__(self): return "Person('%s', %d) % (self.name, self.age) def __repr__(self): return str(self)
3、灵活的初始化
当前,要创建具有特定姓名和年龄的Person对象,必须这样做:
一种更加方便的方式,在构造对象时,将姓名和年龄传递给__init__。为此,需要重写__init__。
# person.py class Person: def __init__(self, name= '', age = 0): self.name = name self.age = age
这样的话,初始化Person对象将简单的多;
由于__init__的参数有默认值,你甚至可以创建“空的”Person对象:
注意方法 __init__,我们在其中使用了self.name和name(以及self.age和age)。变量name指向传入__init__的值,而self.name指向存储在对象中的值。使用self能够更加清晰的指出谁是谁;
4、设置函数和获取函数
当前,我们可以使用句点表示法来读写Person()对象的name以及age值,如下:
这种做法存在一个问题是,可能不小心将年纪设置成 -45 或者 509 等不符合逻辑的数字。对于常规Python变量,无法对付给他的值进行限制。但在对象中,可编写特殊设置函数(setter)和获取函数(getter),对存取值的方式进行控制。
首先,添加一个设置函数,它仅仅提供的值合理时才修改age:
def set_age(self, age): if 0 < age <= 150: self.age = age
此时,定义一个不再该范围内的数值,则不会修改;
对于这种设置函数,一种常见的抱怨是,输入p.set_age(30)比输入p.age = 30更加繁琐。为了解决这个问题,我们可以使用特性装饰器(property decorator)。
特性装饰器:
它融变量的简洁和函数的灵活于一身。装饰器指出函数或者方法有点特殊,这里使用他们来知识设置函数和获取函数;
获取函数返回变量的值,我们将使用@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类类似于下面这样:
# person.py class Person: def __init__(self, name = '', age = 0): self._name = name self._age = age @property def age(self): return self._age def set_age(self, age): if 0 < age <= 150: self._age = age def display(self): print(self) def __str__(self): return "Person('%s', %d)" % (self._name, self._age) def __repr__(self): return str(self)
为了给age创建设置函数,我们将方法set_age重命名为age,并使用@age.setter进行装饰:
@age.setter def age(self, age): if 0 < age <= 150: self._age = age
修改完以后这样输出:
由于给age提供了设置函数和获取函数,编写的代码就像直接使用变量age,但差别在于:遇到代码p.age = -4时,Python实际上将调用方法age(self, age);同样,遇到代码p.age时,将调用方法age(self)。这提供了如下优点:1、赋值语句很简单了 2、同时可控制变量的设置和获取方式;
私有变量:
依然可以直接访问self._age:
问题在于,直接修改 _age 可能导致对象不一致,因此通常不希望直接修改的情况发生;
为了降低变量self._age被直接修改的可能性,一种方式是将其重新命名为self._age,即在变量名开头包含两个下划线。两个下划线表明age是私有变量,不应在Person类外直接访问它。要直接访问self._age,需要在前面加上_Person,如下所示:
这虽然不能禁止你直接修改内部的变量,但是将无意间这样做的可能性几乎降到了零;
5、继承
继承是一种重用类的机制,让你能够这样创建全新的类:给基友类的副本添加变量方法;
假设我们要开发一款游戏,其中涉及人类玩家和计算机玩家,为此,可以创建一个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 = %d" % (self._name, self._score) def __repr__(self): return 'Player(%s)' % str(self)
我们可以像下面这样使用Player对象:
咋们假设有两类玩家:人和计算机。主要差别在于,人通过键盘输入走法,而计算机使用函数生成走法。除此之外,这两类玩家相同,它们都由名称和得分;
下面来编写一个Human类,用于表示人类玩家。为此,一种办法是通过赋值并粘贴新建Player类的一个副本,再添加让玩家走棋的方法get_move(self)。这种办法虽然可行,但更好的做法是使用继承。我们可以让Human类继承Player类的所有变量和方法,这样就不需要再次编辑它们了:
class Human(Player): pass
在Python中,pass语句表示“什么都不做”。对human类来说,这是一个完整而实用的定义,它继承player类的代码,然我们能够像下面一样定义变量:
重写方法:
一个小瑕疵是,h的字符串表示为player,但更准确的说法应该是human。为修复这种问题,可给Human定义方法__repr__;
class Human(Player): def __repr__(self): return 'Human(%s)' % str(self)
这样结果显示如下:
这就是方法重写:Human中的方法__repr__重写了从Player那里继承的方法__repr__。这是定制派生类的常用方式;
class Computer(Player): def __repr__(self): return 'Computer(%s)' % str(self)
这三个类组成了一个小型的类层次结构。player为基类,而且他两个类都为派生类。
【基类常被称为父类,而派生类则被称为子类】
6、多态
为了演示OOP的威力,咋们来创建一个名为Undercut的简单游戏。在这个游戏中,两个玩家同时选择一个1-10的整数,如果一个玩家选择的整数比对方选择整数的小1,则该玩家获胜,否则算打平。例如,如果Thomas和Bonnie一起玩游戏Undercur,且他们选择的数字分别为9和10,则Thomas获胜;如果他们分别选择4和7,则打成平手;
下面是一个玩Undercut游戏的函数:
def play_undercut(p1, p2): p1.reset_score() p2.reset_score() m1 = p1.get_move() m2 = p2.get_move() print("%s move: %d" % (p1.get_name(), m1)) print("%s move: %d" % (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'
其中,p1.get_move() 和 p2.get_move() 我们还没有实现这些函数,因为他们随有些而异。下面就来实现这些函数;
实现get_move函数:
虽然在游戏Undercut中,走法不过是选择1-10的数字,但任何计算机选择数字的方式截然不同。人类玩家通过键盘输入一个1-10的数字,而计算机玩家使用函数来选择数字。因此,Human和Commputer类需要专用的get_move(self)方法。
下面时候Human类的方法get_move():
class Human(Plays): def __repr__(self): 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('Oops!')
上述代码不断要求用户输入一个1-10的整数,知道用户按要求做。try/except 机构用于铺货用户输入的不是整数(如two)时函数int引发的异常。
对于计算机玩家,我们让它返回一个1-10的随机数(如果需要,以后可改进计算机采取的策略):
import random class Computer(Player): def __repr__(self): return 'Computer(%s)' % str(self) def get_move(self): return random.randint(1,10)
再关联上述代码,则可以执行该游戏;
7、更深入的学习
这里,我们主要了解面向对象设计模式。推荐《设计模式:可复用面向对象软件的基础》