Eric Way's Personal Site

I Write $\sin(x)$ Not Tragedies

浅谈Python面向对象编程

2020-03-12 Coding

  1. 1. 用字典表示对象
  2. 2. 类型
  3. 3. 继承
  4. 4. 结语

如果你觉得你已经有了Python的入门水平,那么面向对象编程一定是你Python进阶之路上的必修课;如果你想深入理解从而充分利用Python的各种功能强大的拓展库(比如数据分析中的numpypandas),那么你也应该首先理解Python的面向对象编程。

不过这并不困难。这篇文章会用尽可能简单的方式介绍Python的类型系统,让你对它有最基本的理解。

用字典表示对象

为了方便理解,假如我们有三只动物,每只动物有一个名字,属于某一个物种:

名字 物种
Jessie dog
Tom cat
Billy sheep

我们想在Python中把上面这段信息存起来,最简单的方法是每个动物用一个字典:

1
2
3
animal1 = {"name": "Jessie", "species": "dog"}
animal2 = {"name": "Tom", "species": "cat"}
animal3 = {"name": "Billy", "species": "sheep"}

这时的每只动物就可以理解为一个“对象”,即具有某些属性(名字、物种)的实例。 假如现在我们希望输出每只动物的信息,我们可以定义一个函数,接受一个表示动物的字典,读取相应的字段,然后进行输出:

1
2
3
def print_info(animal):
print("%s is a %s." %
(animal['name'], animal['species']))

比如想输出第一只动物的信息,我们可以:

1
2
print_info(animal1)
# Jessie is a dog.

请注意我们的print_info函数对所有的动物,不分物种,都进行同一种操作。 假如我们想让动物们给我们打个招呼。因为物种不同,它们打招呼的方式会有较大的差别,所以我们需要定义这么一个函数:

1
2
3
4
5
6
7
8
9
10
def greet(animal):
if animal['species'] == 'dog':
print('Woof!')
elif animal['species'] == 'cat':
print('Meow...')
elif animal['species'] == 'sheep':
print('Baa... Baa...')
else:
print('Unknown animal!')
# raise Exception('Unknown animal!')

这个函数接受一只用字典存储的动物,读取它的物种信息,然后根据不同的物种输出不同的叫声。当然,我们没有办法枚举所有的动物,所以只要如果不是我们已知的三个物种,我们就输出'Unknown animal!'。(如果你想,也可以让程序报错,像代码里的注释那样,但那不是我们的重点。) 目前为止,似乎一切都很正常。但这主要是因为我们现在只有三只动物,动物的信息并不复杂,动物的各种行为也比较简单。如果事情变得复杂起来,一些问题就会暴露出来:

  • 我们在用字典存储信息,这导致新增一只动物比较麻烦。我们必须把它的每个属性都作为字符串完整无误地敲一遍才行。如果像这样不小心打错了什么,尤其是如果每只动物拥有更多的属性,就会出现一些难以发现的错误:
1
2
3
animal4 = {"name": "Dannie", "speceis": "sheep"} #typo!
greet(animal4)
# KeyError: 'species'
  • 我们针对动物定义的函数print_infogreet是全局函数。我们其实只希望把一只动物传入这些函数,不希望其他传入其他的什么东西。但是我们现在不能阻止这样的事情:
1
2
3
person = 'David'
greet(person)
# TypeError: string indices must be integers
  • 我们很难把有关动物的各种操作作为一个模块封装起来,给其他程序使用。

类型

为了解决这些问题,我们就要引入“类型”这个概念了。什么是类型呢?一个类型可以理解为用来生成若干具有一定“属性”和“方法”的实例的模板。可以认为“属性”是表示类型实例的状态的值,而“方法”是修改或利用这些状态的函数。Python中的列表就可以看成是一种类型:每个列表实例储存有若干个值对作为它的内容(属性),每个列表实例又可以进行查找、修改、添加等等操作(方法)。 我们也可以把动物看成一种类型。对于一只动物来说,它有两个属性:名字和物种。因此在生成一只新的动物的时候,我们希望输入它的两个属性,然后让它自己记住这两个属性。我们可以这么定义一个Animal类型:

1
2
3
4
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

解释一下: self是什么意思呢?在类型定义之中,当每个实例想使用它自身的某个属性,或者调用自身的某个方法的时候,我们就用self来指代它自身。请记住self本质上是一个实例。 注意在这里我们是如何新建一个实例的属性的。我们就直接self.name = name的时候,就直接给self这一实例新建了一个name属性并赋值。这和Python中的变量声明和赋值机制非常相似,在其他地方我们也是通过直接赋值的方法新建变量,比如直接写一句x = 1就声明了x变量并将它赋值为1。我们也可以用同样的语法修改一个实例的属性。 注意区分self.namename。前者是self的一个属性,而后者是函数中的一个变量。两者并不冲突,因为其实它们是在不同的命名空间下的。 __init__又是什么意思?它是一个特殊的方法,赋予一个类型的属性以具体的值来生成一个实例。我们不应该在这个函数中定义返回值,但是可以理解为它默认的返回值是self。在上面的代码中,Animal类型定义了__init__方法,那么在类型定义的外面,我们就可以用调用函数的方法调用Animal,其实是调用了Animal类型中定义的__init__方法。像这样(在类型定义后面):

1
animal1 = Animal("Jessie", "dog")

下面我们可以给Animal类型添加一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

def print_info(self):
print("%s is a %s." %
(self.name, self.species))

def greet(self):
if self.species == 'dog':
print('Woof!')
elif self.species == 'cat':
print('Meow...')
elif self.species == 'sheep':
print('Baa... Baa...')
else:
print('Unknown animal!')
# raise Exception('Unknown animal!')

def change_name(self, new_name):
self.name = new_name

animal1 = Animal("Jessie", "dog")
animal2 = Animal("Tom", "cat")
animal3 = Animal("Billy", "sheep")

animal2.greet()
# Meow...
animal1.print_info()
# Jessie is a dog.
animal1.change_name("Alice")
animal1.print_info()
# Alice is a dog.

注意到所有的方法定义都必须以self作为第一个函数自变量。但在调用方法的时候,就要省略这一变量。你可以理解为,在调用animal1.print_info()的时候,函数定义中self的值被代入了animal1这个实例。事实上,你也可以显性地感受这一过程,因为animal1.print_info()完全等价于:

1
Animal.print_info(animal1)

这种写法也不常用,但同样可以帮助你理解。 我们还定义了一个change_name方法,用来给动物改名字。这就展示了如何使用方法修改实例的属性。从效果上,根据我们的方法的定义,下面两行代码等价:

1
2
animal1.change_name("Alice")
animal1.name = "Alice"

然而给类型的使用者一个方法作为接口,而不是让类型的使用者去直接修改实例的属性,通常是更加合理的做法;即,如果给动物改名是一个常用的需求,我们则更倾向于第一行的写法。 到目前为止,我们基本上解决了之前提出的三个问题。

  • 新建动物的操作规范化、方便了许多。
  • 因为greetprint_info都定义在Animal类型中,我们只能对动物实例使用这两个方法(函数)。
  • 我们可以把Animal类型的定义放在一个文件中,在另一个文件中import这一类型,就能非常方便地使用它。(当然,这并不是本文的重点。)

继承

一切都在正确的轨道上行进。 然而,现在还有一个问题:如果我们的动物物种增加呢?假如现在来了一只鸭子,我们想要让它正确地打招呼。这似乎非常容易,只需要给Animal类型的greet方法增加一个elif分支即可。然而,修改一个已经定义的类型中的代码往往是不好的操作。假如有人一直都在import这一类型,而你修改了它,就有可能导致其他人的代码出现错误。在这种情况下,我们会发现greet方法是有问题的,因为它难以进行拓展。 这时候,我们需要引入“继承”的概念。何为继承?我们意识到,既然Animal是一个类型,那么DogCatSheep也可以是一个类型。这三个类型沿用了Animal类型的许多内容,包括namespecies的属性,__init__change_nameprint_info的方法。然而,这三个类型在处理greet这一方法的时候,有不同的实现方式。 请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species

def print_info(self):
print("%s is a %s." %
(self.name, self.species))

def change_name(self, new_name):
self.name = new_name

class Dog(Animal): # 继承Animal类型
def greet(self):
print("Woof!")

class Cat(Animal):
def greet(self):
print('Meow...')

class Sheep(Animal):
def greet(self):
print('Baa... Baa...')

animal1 = Dog("Jessie", "dog")
animal1.greet()
# Woof!
animal1.print_info()
# Jessie is a dog.

class Duck(Animal):
def greet(self):
print("Quack!")

animal4 = Duck("Donald", "duck")
animal4.greet()
# Quack!
animal4.print_info()
# Donald is a duck.

可以认为继承就是对原有类型的修修补补。比如新建一个Dog类型的实例,将保留所有Animal类型的方法和属性,又在Animal类型的基础上新增了一个方法greet。 这时,如果再试图这么做,会得到错误:

1
2
3
animal0 = Animal("Jessie", "dog")
animal0.greet()
# AttributeError: 'Animal' object has no attribute 'greet'

原因是Animal类型并没有定义greet方法。(你可能注意到这里Python的报错信息显示“属性错误”,这是因为其实方法可以看成是属性,只是其值为函数。) 你还可以覆盖原有的类型的方法。比如,你可以让狗们有特殊的输出信息的方式:

1
2
3
class Dog(Animal):
def print_info(self):
print("Woof! %s is a dog!" % self.name)

这时,Dog类型中print_info方法的定义就覆盖了Animal类型中同名方法的定义。

1
2
3
animal1 = Dog("Jessie", "dog")
animal1.print_info()
# Woof! Jessie is a dog!

你还可以调用原有类型中的方法:

1
2
3
4
class Dog(Animal):
def print_info(self):
super().print_info()
print("Woof! A dog!")

其中super()表示的是superclass,即原有的(被继承的)类型。

1
2
3
4
animal1 = Dog("Jessie", "dog")
animal1.print_info()
# Jessie is a dog.
# Woof! A dog!

super().print_info()也可以写成:

1
Animal.print_info(self)

同样,这种写法并不常见。

结语

如果你看到这里还跟得上,那要恭喜你基本上理解了Python的类型系统,也就是所谓的面向对象编程——所谓“对象”,就是我们这里所说的“类型实例”。如果你感觉有点困难,可以自己用新的语法写写程序,加深理解。如果你寻求更加精准和专业的解释,请移步Python文档,或搜索相关资料。

This article was last updated on days ago, and the information described in the article may have changed.