在我之前的文章中,我解释了如何通过使用函数、创建模块或两者兼而有之来使 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 就无法知道要使用哪个类的实例。这就像在一个满是兽人的房间里通过说“我要和兽人战斗”来挑战一个兽人进行决斗一样;没有人知道您指的是哪一个,因此会发生不好的事情。

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.enemy 或 gorblar.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.
游戏可以运行了。它是模块化的。现在您知道应用程序面向对象意味着什么了。但最重要的是,您知道在挑战兽人进行决斗时要具体。
3 条评论