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

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


![](/data/attachment/album/201909/08/091142y2bdbboctw7xdbjq.jpg)


在我上一篇文章中，我解释了如何通过使用函数、创建模块或者两者一起来[使 Python 代码更加模块化](/article-11295-1.html)。函数对于避免重复多次使用的代码非常有用，而模块可以确保你在不同的项目中复用代码。但是模块化还有另一种方法：类。


如果你已经听过<ruby> 面向对象编程 <rt>  object-oriented programming </rt></ruby>（OOP）这个术语，那么你可能会对类的用途有一些概念。程序员倾向于将类视为一个虚拟对象，有时与物理世界中的某些东西直接相关，有时则作为某种编程概念的表现形式。无论哪种表示，当你想要在程序中为你或程序的其他部分创建“对象”时，你都可以创建一个类来交互。


### 没有类的模板


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


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



```
#!/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](https://opensource.com/article/17/4/how-program-games-raspberry-pi) 非常有效地利用这个原理来近似了一个面向对象模型。


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


### 使用对象


在 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` 或初始化的函数。这类似于其他语言中的[构造器](https://opensource.com/article/19/6/what-java-constructor)，它创建了类的一个实例，你可以通过它的属性和调用类时使用的任何变量来识别它（示例代码中的 `foe`）。


### Self 和类实例


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


![Image of an Orc, CC-BY-SA by Buch on opengameart.org](/data/attachment/album/201909/08/091202o7lzrliwfprtwilt.jpg "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.enemy` 或 `gorblar.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: <https://opensource.com/article/19/7/get-modular-python-classes>


作者：[Seth Kenlon](https://opensource.com/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[MjSeven](https://github.com/MjSeven) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
In my previous article, I explained how to [make Python modular](https://opensource.com/article/19/6/get-modular-python-functions) by using functions, creating modules, or both. Functions are invaluable to avoid repeating code you intend to use several times, and modules ensure that you can use your code across different projects. But there's another component to modularity: the class.

If you've heard the term *object-oriented programming*, then you may have some notion of the purpose classes serve. Programmers tend to consider a class as a virtual object, sometimes with a direct correlation to something in the physical world, and other times as a manifestation of some programming concept. Either way, the idea is that you can create a class when you want to create "objects" within a program for you or other parts of the program to interact with.

## Templates without classes


Assume you're writing a game set in a fantasy world, and you need this application to be able to drum up a variety of baddies to bring some excitement into your players' lives. Knowing quite a lot about functions, you might think this sounds like a textbook case for functions: code that needs to be repeated often but is written once with allowance for variations when called.

Here's an example of a purely function-based implementation of an enemy generator:

```
#!/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")
```


The **enemy** function creates an enemy with several attributes, such as ancestry, a weapon, health points, and a defense rating. It returns a list of each attribute, representing the sum total of the enemy.

In a sense, this code has created an object, even though it's not using a class yet. Programmers call this "enemy" an *object* because the result (a list of strings and integers, in this case) of the function represents a singular but complex *thing* in the game. That is, the strings and integers in the list aren't arbitrary: together, they describe a virtual object.

When writing a collection of descriptors, you use variables so you can use them any time you want to generate an enemy. It's a little like a template.

In the example code, when an attribute of the object is needed, the corresponding list item is retrieved. For instance, to get the ancestry of an enemy, the code looks at **foe[0]**, for health points, it looks at **foe[2]** for health points, and so on.

There's nothing necessarily wrong with this approach. The code runs as expected. You could add more enemies of different types, you could create a list of enemy types and randomly select from the list during enemy creation, and so on. It works well enough, and in fact [Lua](https://opensource.com/article/17/4/how-program-games-raspberry-pi) uses this principle very effectively to approximate an object-oriented model.

However, there's sometimes more to an object than just a list of attributes.

## The way of the object

In Python, everything is an object. Anything you create in Python is an *instance* of some predefined template. Even basic strings and integers are derivatives of the Python **type** class. You can witness this for yourself an interactive Python shell:

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


When an object is defined by a class, it is more than just a collection of attributes. Python classes have functions all their own. This is convenient, logically, because actions that pertain only to a certain class of objects are contained within that object's class.

In the example code, the fight code is a function of the main application. That works fine for a simple game, but in a complex one, there would be more than just players and enemies in the game world. There might be townsfolk, livestock, buildings, forests, and so on, and none of them ever need access to a fight function. Placing code for combat in an enemy class means your code is better organized; and in a complex application, that's a significant advantage.

Furthermore, each class has privileged access to its own local variables. An enemy's health points, for instance, isn't data that should ever change except by some function of the enemy class. A random butterfly in the game should not accidentally reduce an enemy's health to 0. Ideally, even without classes, that would never happen, but in a complex application with lots of moving parts, it's a powerful trick of the trade to ensure that parts that don't need to interact with one another never do.

Python classes are also subject to garbage collection. When an instance of a class is no longer used, it is moved out of memory. You may never know when this happens, but you tend to notice when it doesn't happen because your application takes up more memory and runs slower than it should. Isolating data sets into classes helps Python track what is in use and what is no longer needed.

## Classy Python

Here's the same simple combat game using a class for the 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
# 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()
```


This version of the game handles the enemy as an object containing the same attributes (ancestry, weapon, health, and defense), plus a new attribute measuring whether the enemy has been vanquished yet, as well as a function for combat.

The first function of a class is a special function called (in Python) an *init*, or initialization, function. This is similar to a [constructor](https://opensource.com/article/19/6/what-java-constructor) in other languages; it creates an instance of the class, which is identifiable to you by its attributes and to whatever variable you use when invoking the class (**foe** in the example code).

## Self and class instances

The class' functions accept a new form of input you don't see outside of classes: **self**. If you don't include **self**, then Python has no way of knowing *which* instance of the class to use when you call a class function. It's like challenging a single orc to a duel by saying "I'll fight the orc" in a room full of orcs; nobody knows which one you're referring to, and so bad things happen.

![CC-BY-SA by Buch on opengameart.org Image of an Orc, CC-BY-SA by Buch on opengameart.org](https://opensource.com/sites/default/files/images/orc-buch-opengameart_cc-by-sa.jpg)

opensource.com

Each attribute created within a class is prepended with the **self** notation, which identifies that variable as an attribute of the class. Once an instance of a class is spawned, you swap out the **self** prefix with the variable representing that instance. Using this technique, you could challenge just one orc to a duel in a room full of orcs by saying "I'll fight the gorblar.orc"; when Gorblar the Orc hears **gorblar.orc**, he knows which orc you're referring to (him*self*), and so you get a fair fight instead of a brawl. In Python:

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


Instead of looking to **foe[0]** (as in the functional example) or **gorblar[0] **for the enemy type, you retrieve the class attribute (**gorblar.enemy** or **gorblar.hp** or whatever value for whatever object you need).

## Local variables

If a variable in a class is not prepended with the **self** keyword, then it is a local variable, just as in any function. For instance, no matter what you do, you cannot access the **hit** variable outside the **Enemy.fight** class:

```
>>> 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'
```


The **hit** variable is contained within the Enemy class, and only "lives" long enough to serve its purpose in combat.

## More modularity

This example uses a class in the same text document as your main application. In a complex game, it's easier to treat each class almost as if it were its own self-standing application. You see this when multiple developers work on the same application: one developer works on a class, and the other works on the main program, and as long as they communicate with one another about what attributes the class must have, the two code bases can be developed in parallel.

To make this example game modular, split it into two files: one for the main application and one for the class. Were it a more complex application, you might have one file per class, or one file per logical groups of classes (for instance, a file for buildings, a file for natural surroundings, a file for enemies and NPCs, and so on).

Save one file containing just the Enemy class as **enemy.py** and another file containing everything else as **main.py**.

Here's **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
```


Here's **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()
```


Importing the module **enemy.py** is done very specifically with a statement that refers to the file of classes as its name without the **.py** extension, followed by a namespace designator of your choosing (for example,** import enemy as en**). This designator is what you use in the code when invoking a class. Instead of just using **Enemy()**, you preface the class with the designator of what you imported, such as **en.Enemy**.

All of these file names are entirely arbitrary, although not uncommon in principle. It's a common convention to name the part of the application that serves as the central hub **main.py**, and a file full of classes is often named in lowercase with the classes inside it, each beginning with a capital letter. Whether you follow these conventions doesn't affect how the application runs, but it does make it easier for experienced Python programmers to quickly decipher how your application works.

There's some flexibility in how you structure your code. For instance, using the code sample, both files must be in the same directory. If you want to package just your classes as a module, then you must create a directory called, for instance, **mybad** and move your classes into it. In **main.py**, your import statement changes a little:

`from mybad import enemy as en`


Both systems produce the same results, but the latter is best if the classes you have created are generic enough that you think other developers could use them in their projects.

Regardless of which you choose, launch the modular version of the game:

```
$ 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.
```


The game works. It's modular. And now you know what it means for an application to be object-oriented. But most importantly, you know to be specific when challenging an orc to a duel.

## 3 Comments