通过 Python 学习面向对象编程

使用 Python 类使您的代码更模块化。
206 位读者喜欢这个。
3 cool machine learning projects using TensorFlow and the Raspberry Pi

Opensource.com

在我之前的文章中,我解释了如何通过使用函数、创建模块或两者兼而有之来使 Python 模块化。函数对于避免重复多次使用的代码非常宝贵,而模块确保您可以在不同的项目中使用您的代码。但模块化还有另一个组成部分:类。

如果您听说过面向对象编程这个术语,那么您可能对类所服务的目标有一些概念。程序员倾向于将类视为虚拟对象,有时与物理世界中的事物直接相关,有时则作为某些编程概念的体现。无论哪种方式,其想法是,当您想在程序中创建“对象”以供您或程序的其他部分与之交互时,您可以创建一个类。

没有类的模板

假设您正在编写一个以幻想世界为背景的游戏,并且您需要此应用程序能够产生各种各样的坏人,为您的玩家的生活带来一些刺激。如果您对函数了解很多,您可能会认为这听起来像是函数的教科书式案例:需要经常重复但只编写一次的代码,并在调用时允许变化。

这是一个纯粹基于函数的敌人生成器实现的示例

#!/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”称为对象,因为函数的结果(在本例中是字符串和整数的列表)代表游戏中一个单一但复杂的事物。也就是说,列表中的字符串和整数不是任意的:它们共同描述了一个虚拟对象。

在编写描述符集合时,您可以使用变量,以便在任何时候想要生成敌人时都可以使用它们。这有点像模板。

在示例代码中,当需要对象的属性时,将检索相应的列表项。例如,要获取敌人的血统,代码会查看 foe[0],对于生命值,它会查看 foe[2],依此类推。

这种方法不一定有什么问题。代码按预期运行。您可以添加更多不同类型的敌人,您可以创建敌人类型列表并在敌人创建期间从列表中随机选择,等等。它运行良好,实际上 Lua 非常有效地使用此原理来近似面向对象的模型。

但是,有时对象的含义不仅仅是属性列表。

对象之道

在 Python 中,一切皆对象。您在 Python 中创建的任何内容都是某个预定义模板的实例。即使是基本的字符串和整数也是 Python type 类的派生。您可以在交互式 Python shell 中亲自见证这一点

>>> foo=3
>>> type(foo)
<class 'int'>
>>> foo="bar"
>>> type(foo)
<class 'str'>

当对象由类定义时,它不仅仅是属性的集合。Python 类有自己的函数。这在逻辑上很方便,因为仅与特定对象类相关的操作包含在该对象的类中。

在示例代码中,战斗代码是主应用程序的函数。这对于简单的游戏来说效果很好,但在复杂的游戏中,游戏世界中不仅有玩家和敌人。可能还有城镇居民、牲畜、建筑物、森林等等,它们都不需要访问战斗函数。将战斗代码放在敌人类中意味着您的代码组织得更好;在复杂的应用程序中,这是一个显着的优势。

此外,每个类都有权访问其自己的局部变量。例如,敌人的生命值是不应更改的数据,除非通过敌人类的某些函数。游戏中随机的蝴蝶不应意外地将敌人的生命值降为 0。理想情况下,即使没有类,这种情况也永远不会发生,但在具有大量移动部件的复杂应用程序中,确保不需要相互交互的部件永远不会相互交互是一个强大的技巧。

Python 类也受垃圾回收的约束。当不再使用类的实例时,它会从内存中移出。您可能永远不知道这种情况何时发生,但当它没有发生时,您往往会注意到,因为您的应用程序占用更多内存并且运行速度比应有的速度慢。将数据集隔离到类中有助于 Python 跟踪哪些正在使用,哪些不再需要。

优雅的 Python

这是使用类作为敌人的相同简单战斗游戏

#!/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

# game start
foe=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()

这个版本的游戏将敌人作为一个对象来处理,该对象包含相同的属性(血统、武器、生命值和防御力),以及一个衡量敌人是否已被击败的新属性,以及一个用于战斗的函数。

类的第一个函数是一个特殊的函数,在 Python 中称为 init 或初始化函数。这类似于其他语言中的构造函数;它创建类的实例,您可以通过其属性以及在调用类时使用的任何变量(示例代码中的 foe)来识别该实例。

Self 和类实例

类的函数接受您在类外部看不到的新形式的输入:self。如果您不包含 self,那么当您调用类函数时,Python 就无法知道要使用哪个类的实例。这就像在一个满是兽人的房间里通过说“我要和兽人战斗”来挑战一个兽人进行决斗一样;没有人知道您指的是哪一个,因此会发生不好的事情。

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

opensource.com

在类中创建的每个属性都以 self 表示法开头,该表示法将该变量标识为类的属性。一旦生成类的实例,您就可以将 self 前缀替换为表示该实例的变量。使用此技术,您可以通过说“我要和 gorblar.orc 战斗”来挑战一个满是兽人的房间中的一个兽人进行决斗;当兽人 Gorblar 听到 gorblar.orc 时,他知道您指的是哪个兽人(他自己),因此您会得到一场公平的战斗而不是一场斗殴。在 Python 中

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

您不是查看 foe[0](如函数式示例中)或 gorblar[0] 以获取敌人类型,而是检索类属性(gorblar.enemygorblar.hp 或您需要的任何对象的任何值)。

局部变量

如果类中的变量没有以 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)。此指示符是您在调用类时在代码中使用的内容。您不仅可以使用 Enemy(),还可以在类前面加上您导入内容的指示符,例如 en.Enemy

所有这些文件名都是完全任意的,尽管在原则上并非不常见。将应用程序中充当中央枢纽的部分命名为 main.py 是一种常见的约定,而包含类的文件通常以小写字母命名,其中的类以大写字母开头。您是否遵循这些约定不会影响应用程序的运行方式,但它确实使经验丰富的 Python 程序员更容易快速理解您的应用程序的工作方式。

代码的结构方式具有一定的灵活性。例如,使用代码示例,两个文件必须位于同一目录中。如果您只想将类打包为模块,那么您必须创建一个名为 mybad 的目录并将您的类移入其中。在 main.py 中,您的导入语句略有变化

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.

游戏可以运行了。它是模块化的。现在您知道应用程序面向对象意味着什么了。但最重要的是,您知道在挑战兽人进行决斗时要具体。

标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时进行。

3 条评论

我想知道 Python 是纯粹的面向对象编程,还是像 Java 和 C++ 一样基于面向对象编程。

我从未自己设计过编程语言,也没有尝试实现 Python,因此可能存在一些我没有经验就无法理解的细微之处。话虽如此,Python 中的一切都是对象:整数、字符串等等。甚至 ``None`` 也是一种类型。您可以使用 ``type`` 验证这一点

>>> type(None)

>>> type(3)

所以是的,我会将 Python3 称为“纯粹”的面向对象语言。

但是,如果您有不同的标准,那么它可能不符合条件。

它不是基于 Java 或 C++(实际上它是用 C 编写的)。

我希望我正确理解了这个问题,并充分回答了它。

干杯!

回复 作者 Ghanendra Yadav

嗨 Seith,

很棒的文章!我刚开始上大学,我们正在使用 python 进行编程。非常感谢这篇文章,它真的帮助我理清了一些我在讲课中完全错过的概念 :D

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.