使用 Python 学习面对对象的编程

使用 Python 类使你的代码变得更加模块化。html

在我上一篇文章中,我解释了如何经过使用函数、建立模块或者二者一块儿来使 Python 代码更加模块化。函数对于避免重复屡次使用的代码很是有用,而模块能够确保你在不一样的项目中复用代码。可是模块化还有另外一种方法:类。java

若是你已经听过面对对象编程object-oriented programming(OOP)这个术语,那么你可能会对类的用途有一些概念。程序员倾向于将类视为一个虚拟对象,有时与物理世界中的某些东西直接相关,有时则做为某种编程概念的表现形式。不管哪一种表示,当你想要在程序中为你或程序的其余部分建立“对象”时,你均可以建立一个类来交互。python

没有类的模板

假设你正在编写一个以幻想世界为背景的游戏,而且你须要这个应用程序可以涌现出各类坏蛋来给玩家的生活带来一些刺激。了解了不少关于函数的知识后,你可能会认为这听起来像是函数的一个教科书案例:须要常常重复的代码,可是在调用时能够考虑变量而只编写一次。linux

下面一个纯粹基于函数的敌人生成器实现的例子:git

#!/usr/bin/env python3

import random

def enemy(ancestry,gear):
    enemy=ancestry
    weapon=gear
    hp=random.randrange(0,20)
    ac=random.randrange(0,20)
    return [enemy,weapon,hp,ac]

def fight(tgt):
    print("You take a swing at the " + tgt[0] + ".")
    hit=random.randrange(0,20)
    if hit > tgt[3]:
        print("You hit the " + tgt[0] + " for " + str(hit) + " damage!")
        tgt[2] = tgt[2] - hit
    else:
        print("You missed.")


foe=enemy("troll","great axe")
print("You meet a " + foe[0] + " wielding a " + foe[1])
print("Type the a key and then RETURN to attack.")

while True:
    action=input()

    if action.lower() == "a":
        fight(foe)

    if foe[2] < 1:
        print("You killed your foe!")
    else:
        print("The " + foe[0] + " has " + str(foe[2]) + " HP remaining")
复制代码

enemy 函数创造了一个具备多个属性的敌人,例如谱系、武器、生命值和防护等级。它返回每一个属性的列表,表示敌人所有特征。程序员

从某种意义上说,这段代码建立了一个对象,即便它尚未使用类。程序员将这个 enemy 称为对象,由于该函数的结果(本例中是一个包含字符串和整数的列表)表示游戏中一个单独但复杂的东西。也就是说,列表中字符串和整数不是任意的:它们一块儿描述了一个虚拟对象。github

在编写描述符集合时,你可使用变量,以便随时使用它们来生成敌人。这有点像模板。shell

在示例代码中,当须要对象的属性时,会检索相应的列表项。例如,要获取敌人的谱系,代码会查询 foe[0],对于生命值,会查询 foe[2],以此类推。编程

这种方法没有什么不妥,代码按预期运行。你能够添加更多不一样类型的敌人,建立一个敌人类型列表,并在敌人建立期间从列表中随机选择,等等,它工做得很好。实际上,Lua 很是有效地利用这个原理来近似了一个面对对象模型。ruby

然而,有时候对象不只仅是属性列表。

使用对象

在 Python 中,一切都是对象。你在 Python 中建立的任何东西都是某个预约义模板的实例。甚至基本的字符串和整数都是 Python type 类的衍生物。你能够在这个交互式 Python shell 中见证:

>>> foo=3
>>> type(foo)
<class 'int'>
>>> foo="bar"
>>> type(foo)
<class 'str'>
复制代码

当一个对象由一个类定义时,它不只仅是一个属性的集合,Python 类具备各自的函数。从逻辑上讲,这很方便,由于只涉及某个对象类的操做包含在该对象的类中。

在示例代码中,fight 的代码是主应用程序的功能。这对于一个简单的游戏来讲是可行的,但对于一个复杂的游戏来讲,世界中不只仅有玩家和敌人,还可能有城镇居民、牲畜、建筑物、森林等等,它们都不须要使用战斗功能。将战斗代码放在敌人的类中意味着你的代码更有条理,在一个复杂的应用程序中,这是一个重要的优点。

此外,每一个类都有特权访问本身的本地变量。例如,敌人的生命值,除了某些功能以外,是不会改变的数据。游戏中的随机蝴蝶不该该意外地将敌人的生命值下降到 0。理想状况下,即便没有类,也不会发生这种状况。可是在具备大量活动部件的复杂应用程序中,确保不须要相互交互的部件永远不会发生这种状况,这是一个很是有用的技巧。

Python 类也受垃圾收集的影响。当再也不使用类的实例时,它将被移出内存。你可能永远不知道这种状况会何时发生,可是你每每知道何时它不会发生,由于你的应用程序占用了更多的内存,并且运行速度比较慢。将数据集隔离到类中能够帮助 Python 跟踪哪些数据正在使用,哪些不在须要了。

优雅的 Python

下面是一个一样简单的战斗游戏,使用了 Enemy 类:

#!/usr/bin/env python3

import random

class Enemy():
    def __init__(self,ancestry,gear):
        self.enemy=ancestry
        self.weapon=gear
        self.hp=random.randrange(10,20)
        self.ac=random.randrange(12,20)
        self.alive=True

    def fight(self,tgt):
        print("You take a swing at the " + self.enemy + ".")
        hit=random.randrange(0,20)

        if self.alive and hit > self.ac:
            print("You hit the " + self.enemy + " for " + str(hit) + " damage!")
            self.hp = self.hp - hit
            print("The " + self.enemy + " has " + str(self.hp) + " HP remaining")
        else:
            print("You missed.")

        if self.hp < 1:
            self.alive=False

# 游戏开始
foe=Enemy("troll","great axe")
print("You meet a " + foe.enemy + " wielding a " + foe.weapon)

# 主函数循环
while True:
   
    print("Type the a key and then RETURN to attack.")
        
    action=input()

    if action.lower() == "a":
        foe.fight(foe)
                
    if foe.alive == False:
        print("You have won...this time.")
        exit()
复制代码

这个版本的游戏将敌人做为一个包含相同属性(谱系、武器、生命值和防护)的对象来处理,并添加一个新的属性来衡量敌人时候已被击败,以及一个战斗功能。

类的第一个函数是一个特殊的函数,在 Python 中称为 init 或初始化的函数。这相似于其余语言中的构造器,它建立了类的一个实例,你能够经过它的属性和调用类时使用的任何变量来识别它(示例代码中的 foe)。

Self 和类实例

类的函数接受一种你在类以外看不到的新形式的输入:self。若是不包含 self,那么当你调用类函数时,Python 没法知道要使用的类的哪一个实例。这就像在一间充满兽人的房间里说:“我要和兽人战斗”,向一个兽人发起。没有人知道你指的是谁,全部兽人就都上来了。

Image of an Orc, CC-BY-SA by Buch on opengameart.org
CC-BY-SA by Buch on opengameart.org

CC-BY-SA by Buch on opengameart.org

类中建立的每一个属性都以 self 符号做为前缀,该符号将变量标识为类的属性。一旦派生出类的实例,就用表示该实例的变量替换掉 self 前缀。使用这个技巧,你能够在一间尽是兽人的房间里说:“我要和谱系是 orc 的兽人战斗”,这样来挑战一个兽人。当 orc 听到 “gorblar.orc” 时,它就知道你指的是谁(他本身),因此你获得是一场公平的战斗而不是斗殴。在 Python 中:

gorblar=Enemy("orc","sword")
print("The " + gorblar.enemy + " has " + str(gorblar.hp) + " remaining.")
复制代码

经过检索类属性(gorblar.enemygorblar.hp 或你须要的任何对象的任何值)而不是查询 foe[0](在函数示例中)或 gorblar[0] 来寻找敌人。

本地变量

若是类中的变量没有以 self 关键字做为前缀,那么它就是一个局部变量,就像在函数中同样。例如,不管你作什么,你都没法访问 Enemy.fight 类以外的 hit 变量:

>>> print(foe.hit)
Traceback (most recent call last):
  File "./enclass.py", line 38, in <module>
    print(foe.hit)
AttributeError: 'Enemy' object has no attribute 'hit'

>>> print(foe.fight.hit)
Traceback (most recent call last):
  File "./enclass.py", line 38, in <module>
    print(foe.fight.hit)
AttributeError: 'function' object has no attribute 'hit'
复制代码

hit 变量包含在 Enemy 类中,而且只能“存活”到在战斗中发挥做用。

更模块化

本例使用与主应用程序相同的文本文档中的类。在一个复杂的游戏中,咱们更容易将每一个类看做是本身独立的应用程序。当多个开发人员处理同一个应用程序时,你会看到这一点:一个开发人员负责一个类,另外一个开发人员负责主程序,只要他们彼此沟通这个类必须具备什么属性,就能够并行地开发这两个代码块。

要使这个示例游戏模块化,能够把它拆分为两个文件:一个用于主应用程序,另外一个用于类。若是它是一个更复杂的应用程序,你可能每一个类都有一个文件,或每一个逻辑类组有一个文件(例如,用于建筑物的文件,用于天然环境的文件,用于敌人或 NPC 的文件等)。

将只包含 Enemy 类的一个文件保存为 enemy.py,将另外一个包含其余内容的文件保存为 main.py

如下是 enemy.py

import random

class Enemy():
    def __init__(self,ancestry,gear):
        self.enemy=ancestry
        self.weapon=gear
        self.hp=random.randrange(10,20)
        self.stg=random.randrange(0,20)
        self.ac=random.randrange(0,20)
        self.alive=True

    def fight(self,tgt):
        print("You take a swing at the " + self.enemy + ".")
        hit=random.randrange(0,20)

        if self.alive and hit > self.ac:
            print("You hit the " + self.enemy + " for " + str(hit) + " damage!")
            self.hp = self.hp - hit
            print("The " + self.enemy + " has " + str(self.hp) + " HP remaining")
        else:
            print("You missed.")

        if self.hp < 1:
            self.alive=False
复制代码

如下是 main.py

#!/usr/bin/env python3

import enemy as en

# game start
foe=en.Enemy("troll","great axe")
print("You meet a " + foe.enemy + " wielding a " + foe.weapon)

# main loop
while True:
   
    print("Type the a key and then RETURN to attack.")

    action=input()

    if action.lower() == "a":
        foe.fight(foe)

    if foe.alive == False:
        print("You have won...this time.")
        exit()
复制代码

导入模块 enemy.py 使用了一条特别的语句,引用类文件名称而不用带有 .py 扩展名,后跟你选择的命名空间指示符(例如,import enemy as en)。这个指示符是在你调用类时在代码中使用的。你须要在导入时添加指示符,例如 en.Enemy,而不是只使用 Enemy()

全部这些文件名都是任意的,尽管在原则上不要使用罕见的名称。将应用程序的中心命名为 main.py 是一个常见约定,和一个充满类的文件一般以小写形式命名,其中的类都以大写字母开头。是否遵循这些约定不会影响应用程序的运行方式,但它确实使经验丰富的 Python 程序员更容易快速理解应用程序的工做方式。

在如何构建代码方面有一些灵活性。例如,使用该示例代码,两个文件必须位于同一目录中。若是你只想将类打包为模块,那么必须建立一个名为 mybad 的目录,并将你的类移入其中。在 main.py 中,你的 import 语句稍有变化:

from mybad import enemy as en
复制代码

两种方法都会产生相同的结果,但若是你建立的类足够通用,你认为其余开发人员能够在他们的项目中使用它们,那么后者更好。

不管你选择哪一种方式,均可以启动游戏的模块化版本:

$ python3 ./main.py 
You meet a troll wielding a great axe
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You missed.
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You hit the troll for 8 damage!
The troll has 4 HP remaining
Type the a key and then RETURN to attack.
a
You take a swing at the troll.
You hit the troll for 11 damage!
The troll has -7 HP remaining
You have won...this time.
复制代码

游戏启动了,它如今更加模块化了。如今你知道了面对对象的应用程序意味着什么,但最重要的是,当你向兽人发起决斗的时候,你知道是哪个。


via: opensource.com/article/19/…

做者:Seth Kenlon 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

相关文章
相关标签/搜索