在我的上一篇文章中,我解释了如何通过使用函数、创建模块或两者兼而有之来使 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 函数创建一个具有多个属性的敌人,例如血统、武器、生命值和防御等级。它返回每个属性的列表,代表敌人的总和。
从某种意义上说,这段代码创建了一个对象,即使它还没有使用类。程序员将这个“敌人”称为对象,因为函数的结果(在本例中是字符串和整数的列表)代表游戏中一个单一但复杂的事物。也就是说,列表中的字符串和整数不是任意的:它们共同描述了一个虚拟对象。
在编写描述符集合时,你使用变量,以便你可以在任何时候生成敌人时使用它们。这有点像模板。
在示例代码中,当需要对象的属性时,将检索相应的列表项。例如,要获取敌人的血统,代码查看 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 中,你的 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.
游戏可以运行了。它是模块化的。现在你知道应用程序面向对象意味着什么了。但最重要的是,你知道在挑战兽人决斗时要具体。
3 条评论