Python面向对象编程01-类的定义与使用

类和对象的基本概念

什么是类

在编程时,往往需要通过程序修改变量。但有时候多个变量间可能有很强的关联,是一个整体。此时,可以使用结构体描述这一个整体。然而,结构体只能包含数据成员,却不包含对这些数据操作所需要的函数。

如果把需要描述并操作的某些变量和函数看成一个整体,这个整体就称为(type)。类定义了该集合中每个对象所共有的属性和支持的操作。例如,对于每一种数据结构如链表、栈、二叉树等,每个集合可能都具有一些相同的属性例如后向指针、栈顶位置、树结点值,但还有一些操作如增(add)、删(remove)、改(update)、查(find),不同的数据结构执行的操作也不一样。这种将一系列属性与方法作为一个整体封装成一个类的编程思路,称为 面向对象编程 (object-oriented programming)。面向对象编程是一种编程思路而不是具体的编程语言,使用 C 语言照样可以编写具有面向对象编程思想的代码,只是实现起来比较麻烦。

Python 是一种原生支持 面向对象编程 的编程语言,可以轻松实现面向对象编程的大多数要求。Python 提供了许多内置的类,例如列表就是其中之一。list 类的属性有各个元素(暂且可以这么认为),方法有修改,排序数据等。除此之外还可以实现自己的类,例如向量类,这个类的属性有方向和大小,它的方法有获取模长、标量放缩等。

在 Python 中,实现一个最简单的类是这样的:

class SomeType:
    ...

类的定义以关键字 class 开头,之后跟着一个类名,并且以冒号和缩进确定类的主体。

类名是一个标识符,必须符合 Python 标识符的命名规则。一般建议类的命名应该遵循大驼峰式标记,即类的任意一个英文单词都应该以大写字母开头。

紧跟类的定义后是类的内容,并且需要缩进。由于第一个类目前并没有实际完成一些事情,因此使用了 pass 关键字表示类的主体暂时忽略。

可以使用内置函数 type() 来查看刚刚定义的类的类型:

class SomeType:
    pass
print(SomeType, type(SomeType))

结果为:

$ python -u demo.py <class '__main__.SomeType'> <class 'type'>

结果表明,SomeType 的类型是“类”

什么是实例

有了类以后,还需要一个具体的实例(instance)来完成具体的行为。对于内置的 list 类,需要有一个存储了具体数据的列表来完成对列表的操作。同样,对于上述的自定义类向量,也需要一个具体的向量实例来完成取模、计算等操作。

对于以上定义的类,获得一个类的实例的方法如下:

SomeType()

通过输入类名并紧跟一对小括号,便可以获得一个类的对象。这种由类创建出的具体对象称为实例(instance)。类是抽象的概念,而实例是具体的概念。

例如,假设有一个类 Fruit(水果),显然它有一些属性例如 taste(味道)、color(颜色) 等,但只有获得了一个实例 apple(苹果) 后,这些属性才能具体描述。

回到上述语句,获得一个实例有点像调用一个函数,当然这实际上是根据类名生成了一个类的实例。并且由于这个类缺乏具体描述,因此小括号中暂时不需要传入参数。

可以将实例赋值给一个变量,并且每次使用括号,都会为该类生成一个新的实例。例如,以下代码生成了两个类,并使用 type() 函数查看它们的类型:

one_instance = SomeType()
another_instance = SomeType()
print(one_instance, type(another_instance))

结果为:

$ python -u demo.py <__main__.SomeType object at 0x0000020644132FD0> <class '__main__.SomeType'>

观察上述结果,打印实例变量时,它输出了一个地址,这表明每一个实例之间都是独一无二的。另外,结果显示实例 another_instance 的类型是“当前文件的 SomeType 类”。

什么是属性

以上创建的类和实例看起来没有任何作用,它不包含任何数据,也不做任何事情。对于一个已经创建的类,最首要的就是明确类的属性(attribute)。

前文说过,属性用来描述一个类有哪些特性。对于一个类,它的每个实例属性具体的值可能不太一样,但是它们都存在这个属性。

假设将平面上的一个点看出一个类,那么可以这样定义它:

class Point:
    pass
one_point = Point()

这个类 Point 需要描述它的坐标,可以通过点记法给一个实例赋予任意的属性。例如,可以这样描述它的属性:

one_point.x = 5
one_point.y = 6
print(one_point.x, one_point.y)

以上描述为实例添加属性的语法为:

instance.attribute = value

这种语法称为点记法,它的赋值过程和变量的赋值是一样的,值可以是任意的,甚至是一个函数或者另一个类。

通过在实例后面加上一个点,就可以表示它的属性,这种方式就和结构体访问字段是一样的。运行以上代码,即可打印属性值:

$ python -u demo.py 5 6

这几行代码表明,one_point 实例被添加了两个属性,一个是代表横坐标的 x ,一个是代表纵坐标的 y 。并且它们的属性都被赋予了一个具体的整数值。

什么是方法

之前说过,类是属性和操作的集合。获得一个带属性的类以后,这个类还需要完成一些事情,来改变这些属性。改变属性的行为称为方法(method)。

例如,对于内置类 list ,它有一些方法,比如增加数据(appendinsertextend )、移除数据(remove)等,每个具体的方法实现的效果都不一样。对于自定义的向量类,可以使用方法对它做一些放缩、运算、取模等操作。对于抽象一些的类 Fruit(水果) 来说,它也可以定义一些方法,例如 cut(把一个具体的水果切成小块) 、flavor(给水果加上调料改变味道) 等。

从之前实现的 Point 类开始,可以给它添加一个叫做 move_to_origin 的方法,该方法用来将这个点移动到原点。包含该方法后,完整的类的定义为:

class Point:
    def move_to_origin(self):
        self.x = 0
        self.y = 0

Python 中类的方法和定义一个函数类似,都以关键字 def 开头,然后是一个函数/方法名和一对小括号,括号内包含一些参数,以冒号引出函数/方法的主体。一般推荐方法名遵循蛇形命名规则,即名称中的单词字母全部小写并以下划线连接。

注意方法和函数有一点不一样,所有针对实例的方法都必须包含一个参数,这个参数通常被称为 self ,它必须是第一个位置参数,不过它的名称不一定要是 self ,也可以改成 this 或者 Hello 。一般情况下,定义方法的语法为:

def method(self, ...):
    ...

一个方法中的 self 参数,是对调用这个方法对象的一个引用。

不过无需手动传入这个 self 参数,调用一个实例方法的方式是实例名后面跟上一个点,再跟上一个方法名,后面像调用函数一样加上一个括号,即:

instance.method(...)

Python 会自动将一个具体的实例作为第一个参数传给 self 。此时,self.x 实际上就是 instance.x ,替换为实际参数后可以看出形式参数 self 用来代表某个具体的实例,而执行方法时将某个具体的实例对象传给这个形式参数。

例如,以下尝试调用该方法:

another_point = Point()
another_point.x = 10
another_point.y = 20
print(another_point.x, another_point.y)
another_point.move_to_origin()
print(another_point.x, another_point.y)

结果为:

$ python -u demo.py 10 20 0 0

可以看出执行了 .move_to_origin() 方法后,该点确实被移到了原点。如果在定义方法时忘记了 self 参数,那么通过实例调用方法时,会产生 TypeError ,错误信息是某方法需要 0 个参数,却传入了 1 个,这就是因为调用方法时需要参数 self 来代表一个具体的实例,才能通过参数去获取、更改它的属性。

实际上也可以在类中调用这个函数,并且将类的一个实例作为实际参数传给方法:

p = Point()
Point.move_to_origin(p)
print(p.x, p.y)

两者是完全一样的。许多面向对象编程的语言都使用关键字 this 表示对实例的一个引用,而 Python 使用这种参数的形式来获取实例,加强了类与实例间的联系。

以下定义了一个更复杂的方法 scale ,用来在坐标轴上按相对位置缩放一个点:

class Point:
    def scale(self, scalar: float, relative: Point=None):
        if relative is None:
            relative = Point()
            relative.x = 0
            relative.y = 0
        self.x = (self.x - relative.x) * scalar
        self.y = (self.y - relative.y) * scalar

通过这个方法,可以看出方法和函数一样,都可以有多个参数,都可以有返回值,甚至可以拥有类型标注。通过实例调用一个方法就类似于直接执行一个函数,只不过第一个位置参数是自动传入的。

类的初始化

在上一节中,定义了一个类 Point ,并使用 .x.y 属性来描述它的坐标,.move_to_origin().scale() 方法来多点执行一些操作。然而,在每次实例化一个类后,都需要给它的 .x.y 属性赋一个具体值,否则会访问不到它们的属性,产生 AttributeError

最好能在实例化类时,就能给实例的属性赋予一个具体值,这样即使后续忘记赋值,也不影响程序的执行。然而,为了给实例添加上属性,只能通过 self. 的格式从形式参数中获取类的实例,然而只有在有实例时,该方法才能调用,也就是说还是只能在生成实例后手动为每一个实例添加属性。

事实上,类有一个特殊的方法 .__init__() ,它是一个初始化方法,用来初始化类的属性。类还有很多类似的特殊方法(special method, or magic method),它们都以双下划线开头,双下划线结尾,有时也被称为双下方法(dunder method)。所有的特殊方法在以后会逐一介绍。

在创建自己的方法时,尽量不要以双下划线开头结尾,否则一旦创建了一些特殊的方法,可能会在不必要的时刻被调用,从而招致莫名其妙的错误。

.__init__() 方法类似别的面向对象编程语言的构造方法,不过 Python 的构造方法另有它用,.__init__() 的实际效果等效于构造方法。

一旦类拥有了这样一个初始化方法,在实例化该类时,使用类名加上一对圆括号,实际上就是在调用这个方法。假设下面有一个类 Vector

class Vector:
    def __init__(self, direction, magnitude):
        self.direction = direction
        self.magnitude = magnitude

在初始化时,调用 Vector() 实际上就是在调用它的初始化方法。由于 .__init__() 方法有两个参数,因此实例化时,也需要传入两个参数:

v = Vector(direction=90, magnitude=6)

这样,在初始化时,就向实例传入了两个参数,这样确保了只要成功生成了一个实例,它就自动执行一些代码,生成了一些属性,无需后续再手动添加属性,减少了代码量和出错率。

试着调用一下实例的属性:

print(v.direction, v.magnitude)
$ python -u demo.py 90 6

可以看出在初始化时,它成功地创建了一些属性。

当然,初始化方法也不一定要添加参数,它也可以仅包含 self 参数,但这样就不能在创建时通过传参为每个实例添加不同的属性了。仅包含 self 参数的 .__init__() 构造方法,又称为类的默认构造方法。在有些时候只需要确定的实例属性,当然可以这么做。

本节介绍了面向对象编程的基础和使用 Python 编写面向对象程序的基本语法。面向对象编程是一种非常实用,也应用非常广泛的编程思想,许多编程语言都提供了面向对象编程的语法支持。本节对面向对象编程的思想介绍并不深刻,有兴趣的读者可以自行去 stackoverflow 、知乎等网站搜索,许多人都对其有自己独到的理解。

参考资料/延伸阅读

https://docs.python.org/3/tutorial/classes.html

Python3 官方文档对 Python 面向对象编程的简介,目前仍是 Python 面向对象编程入门的最好教程。

京ICP备2021034974号
contact me by hello@frozencandles.fun