Burnov

导航

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()
    elsereturn 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、更深入的学习

   这里,我们主要了解面向对象设计模式。推荐《设计模式:可复用面向对象软件的基础》

posted on 2019-07-19 15:48  Burnov  阅读(523)  评论(3编辑  收藏  举报