本页目录

Python面向对象编程

本文对面向对象的基本理论(为什么要有类、什么是属性、什么是方法等)不多做解释,重点在Python中的编程实现。

基本操作

定义一个类

class Unit:
    # 初始化方法/构造函数
    def __init__(self, name, hp, damage):
        # 实例属性
        self.name = name
        self.hp = hp
        self.damage = damage

    # 实例方法
    def sayhi(self):
        print(f"hi, I'm {self.name}")

u1 = Unit("u1", 100, 20)
u2 = Unit("u2", 200, 10)

u1.sayhi()  # Hi, I'm u1
print(u1.hp)  # 100
print(u1.damage)  # 20

上面的代码定义了一个Unit类,这个例子中暂且理解为一个游戏中的作战单位,每个单位具有名字name、生命值hp、攻击力damage这些属性。

__init__方法称作类的初始化方法/构造函数,实例本身会作为函数的第一个参数self被传入。从这个角度理解,实际上__init__方法是将传入的参数“绑定”到新创建的实例上。

类属性和实例属性

我们在类下直接定义了一个属性utype,它是一个类属性;与之对应的是__init__方法为实例创建的属性,我们称之为实例属性

class Unit:
    # 类属性,占用同一块地址
    utype = "unit"

    def __init__(self, name, hp, damage):
        # 实例属性,独属于每个实例
        self.name = name
        self.hp = hp
        self.damage = damage

    def sayhi(self):
        print("hi, I'm %s" % self.name)

u1 = Unit("u1", 100, 20)
u2 = Unit("u2", 200, 10)

访问实例属性时,把它们当作普通变量就好了;实例属性也可以在__init__方法之外动态的添加。

print(u1.hp)  # 100
print(u1.damage)  # 20
u1.level = 1
print(u1.level)  # 1

类属性可以在类上取得,也可以在实例上取得。类属性共享内存地址。

print(Unit.utype, u1.utype, u2.utype)  # unit unit unit
print(id(Unit.utype))  # 2197356757808
print(id(u1.utype))    # 2197356757808
print(id(u2.utype))    # 2197356757808

由于共享内存地址,因此修改类属性具有全局性:

Unit.utype = "spell"
print(Unit.utype, u1.utype, u2.utype)  # spell spell spell

注意

u1.utype = "hero"
print(Unit.utype, u1.utype, u2.utype)  # spell hero spell

如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例属性。上面的例子相当于给u1添加了一个与类属性同名实例属性,u1.utype访问到的不是类属性。

要想访问u1的类属性,可以通过__class__访问:

print(u1.__class__.utype)  # spell

继承

实现一个子类

我们还是定义一个Unit类,并实现两个方法:info用于输出自身基本信息,attack模拟攻击另一个单位。

class Unit:
    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    def info(self):
        print(f"name: {self.name}, hp: {self.hp}, damage: {self.damage}")

    def attack(self, unit):
        unit.hp -= self.damage
        print(f"{self.name} attacks {unit.name}, {unit.name}.hp = {unit.hp}")

现在我们希望实现一个GroundUnit类,表示地面单位,并且地面单位具有特有的伤害加成,用属性buff定义。显然,会有大量的逻辑与Unit类是重复的。这时可以通过继承来实现:

class GroundUnit(Unit):
    def __init__(self, name, hp, damage, buff):
        super().__init__(name, hp, damage)
        # 子类新增的实例属性
        self.buff = buff

    # 重写父类方法
    def attack(self, unit):
        unit.hp -= self.damage * (1 + self.buff)
        print(f"{self.name} attacks {unit.name}, {unit.name}.hp = {unit.hp}")

gu1 = GroundUnit("gu1", 100, 20, 0.2)
gu2 = GroundUnit("gu2", 200, 10, 0.2)

gu2.info()  # name: gu2, hp: 200, damage: 10
gu1.attack(gu2)  # gu1 attacks gu2, gu2.hp = 176.0
gu2.info()  # name: gu2, hp: 176.0, damage: 10

定义类时,用class 子类(父类):表示继承。如果子类有自己的构造函数,会覆盖父类的构造函数;否则会继承父类的构造函数。

代码中super()函数可以找到父类,高亮的代码等价于Unit.__init__(self, name, hp, damage)

如果子类需要对父类的方法进行重写,只需要在子类下定义同名方法,然后重写逻辑。其他父类的方法会被继承到子类中,例如这个例子中的info()

isinstance函数

isinstance(实例,类)可以判断一个实例是否属于给定的类。子类的实例同时也是父类的实例

u1 = Unit("u1", 100, 20)
gu1 = GroundUnit("gu1", 100, 20, 0.2)

print(isinstance(u1, Unit))         # True
print(isinstance(u1, GroundUnit))   # False
print(isinstance(gu1, Unit))        # True
print(isinstance(gu1, GroundUnit))  # True

多继承

假设我们的游戏复杂起来,引入了稀有度系统,每个单位有一个所属的稀有度,例如普通、稀有、史诗、传奇等等。以史诗级为例,假设对于这些不同稀有度的单位有着其他独特的机制,以至于我们不得不新创建一个EpicRarity类:

class EpicRarity:
    def __init__(self, level):
        self.level = level

    def info(self):
        print(f"This is a lv.{self.level} epic unit")

当然,为了便于演示,我们只定义了一个level属性和info方法。

现在,我们想实现EpicGroundUnit子类,表示史诗级地面单位。显然它需要同时继承父类EpicRarityGroundUnit,这就是多继承

class EpicGroundUnit(EpicRarity, GroundUnit):
    def __init__(self, name, hp, damage, buff, level):
        GroundUnit.__init__(self, name, hp, damage, buff)
        EpicRarity.__init__(self, level)

egu1 = EpicGroundUnit("egu1", 100, 20, 0.2, 1)

egu1.info()  # This is a lv.1 epic unit

在构造函数中我们需要分别对父类进行初始化。

注意到两个父类都实现了info方法并且没有被子类重写。调用之后我们发现,子类继承的是EpicRarityinfo方法。如果代码改写为:

class EpicGroundUnit(GroundUnit, EpicRarity):
    def __init__(self, name, hp, damage, buff, level):
        GroundUnit.__init__(self, name, hp, damage, buff)
        EpicRarity.__init__(self, level)

egu1 = EpicGroundUnit("egu1", 100, 20, 0.2, 1)

egu1.info()  # name: egu1, hp: 100, damage: 20

就会发现,调换父类的顺序后,现在子类继承的是GroundUnitinfo方法。这就引出了下一节的内容:方法解析顺序

方法解析顺序MRO

对于多继承情况下的同名方法,如何从父类中找应该优先使用哪个父类的方法就叫方法解析顺序(Method Resolution Order, MRO)。 Python采用C3线性化算法来计算线性化列表,保证继承顺序列表中每个类只出现一次。

class A:
    def test(self):
        print('A')

class B:
    def test(self):
        print('B')

class C(A, B):
    pass

class D(C, B):
    pass

obj = D()
obj.test()  # A

上述代码描述了一个如下图所示的复杂继承关系:

img

可以通过mro()函数得到类的方法解析顺序:

# 注意是类名,不是实例名
print(D.mro())  # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

输出的列表中,从左到右的顺序为查找方法的顺序。上述例子中,DC类都没有定义test方法,因此顺次使用了A类的test方法。

注意

MRO顺序不是简单的深度优先或广度优先!

封装

在前面Unit类的例子中,我们可以通过直接访问u1.hp修改其值,这样并不安全(这岂不是像外挂一样)!封装的目的是为了保护数据,不让外部直接访问和修改。在Python中,约定通过在属性名称前加两个下划线__来将属性私有化。这种命名约定会使Python解释器修改变量名为_类名__属性名的形式,使其在类外部变得难以访问。

将实例属性私有化

class Unit:
    def __init__(self, name, hp, damage):
        self.name = name
        # 私有属性
        self.__hp = hp
        self.__damage = damage

    def attack(self, unit):
        unit.__hp -= self.__damage
        print(f"{self.name} attacks {unit.name}, {unit.name}.__hp = {unit.__hp}")

    # 通过公有方法访问私有属性
    def get_hp(self):
        return self.__hp

在定义私有属性后,对外开放一个公有方法get_hp,通过这个公有方法可以间接的访问到__hp属性。这样相当于让此属性对外部“只读”。

u1 = Unit("u1", 100, 20)
print(u1.get_hp())  # 100
print(u1.__hp)  # AttributeError: 'Unit' object has no attribute '__hp'

通过_类名__属性名的形式可以强制访问私有属性:

print(u1._Unit__hp)  # 100

注意

在类外绑定的双下划线变量是公有的。

u1.__var = 1
print(u1.__var)  # 1

类比于C++

C++中的封装有三种:publicprotectedprivate;Python中没有这些关键字,但是可以通过属性名命名约定来实现。

public:公有变量,可以在类的内部和外部访问,正常命名即可。

protected:保护变量,只能在类内和子类访问,属性名前加单下划线_。这只是一种命名约定,实际上是可以访问的。

private:私有变量,只能在类的内部访问,属性名前加双下划线__,这样会使Python解释器修改变量名为_类名__属性名的形式。

class A:
    def __init__(self, x, y, z):
        # 公有变量
        self.x = x
        # 保护变量
        self._y = y
        # 私有变量
        self.__z = z

class B(A):
    def __init__(self, x, y, z):
        super().__init__(x, y, z)

    def info_x(self):
        print(f"x: {self.x}")

    def info_y(self):
        print(f"y: {self._y}")

    def info_z(self):
        print(f"z: {self.__z}")

b = B(1, 2, 3)
b.info_x()  # x: 1
b.info_y()  # y: 2
b.info_z()  # AttributeError: 'B' object has no attribute '_B__z'. Did you mean: '_A__z'?

注意

C++中的protected关键字是一种严格的访问控制机制,而Python中的单下划线变量只是一种命名约定,不具有强制性,实际上是可以访问的。只不过有时违反了这样的约定时,有些代码编辑器会给出警告。

print(b._y)  # 2

多态

举个例子

多态是当一个类继承自另一个类并重写了其方法时,可以在不改变原有接口的情况下,根据对象的实际类型来调用不同的方法实现。这听起来有些复杂,我们来举一个具体的例子:

class Unit:
    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    def info(self):
        print(f"name: {self.name}, hp: {self.hp}, damage: {self.damage}")

class GroundUnit(Unit):
    def info(self):
        print("This is a ground unit.")

class AirUnit(Unit):
    def info(self):
        print("This is an air unit.")

def show_info(unit):
    unit.info()

gu1 = GroundUnit("gu1", 100, 20)
au1 = AirUnit("au1", 100, 20)
show_info(gu1)  # This is a ground unit.
show_info(au1)  # This is an air unit.

这个例子中,show_info函数接受一个Unit类型的参数,但是我们传入的是其子类GroundUnitAirUnit类型的实例。子类重写了父类的info方法,相当于共用了父类的接口,但是子类又通过继承重写了接口,从而实现了不同的功能。

通过抽象类实现多态

抽象类是指包含抽象方法的类;抽象类只能被继承,不能被实例化。
抽象方法是指只有声明,没有实现的方法,它存在的意义是让子类重写这个方法。

上面的例子中,如果我们将Unit类的info方法定义为:

class Unit:
    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    # 这是一个抽象方法,子类必须重写这个方法,否则在调用时会报错
    def info(self):
        raise NotImplementedError("Subclasses must implement abstract method.")

这时子类如果没有重写info方法,就会继承父类中的方法,在调用的时候就会报错。

class UnitWithoutInfo(Unit):
    pass

u = UnitWithoutInfo("u", 100, 20)
u.info()  # NotImplementedError: Subclasses must implement abstract method.

Python中的abc模块

Python的abc模块中定义了抽象基类ABC (Abstract Base Classes),可以强制其子类必须实现某些方法。

上面的例子使用abc模块可以改写为:

from abc import ABC, abstractmethod

class Unit(ABC):
    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    @abstractmethod
    def info(self):
        pass

这时子类如果没有重写info方法,在实例化时就会报错!

class UnitWithoutInfo(Unit):
    pass

# 在实例化时就会报错
u = UnitWithoutInfo("u", 100, 20)  # TypeError: Can't instantiate abstract class UnitWithoutInfo with abstract method info

三大方法

类方法

类方法用修饰器@classmethod定义,传入的第一个参数是类本身而不是实例,通常命名为cls。通过它可以访问到类属性。

class Unit:
    utype = "unit"

    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    @classmethod
    def show_type(cls):
        # 可以访问到类属性
        print(f"This is a {cls.utype}.")

u1 = Unit("u1", 100, 20)
u1.show_type()  # This is a unit.

类方法的应用:自动计算实例数

假如我们希望每创建一个类时,都可以自动计数当前类的实例数量。这个功能可以由类方法实现。

class Unit:
    type = "unit"
    __unit_num = 0  # 声明为私有类属性

    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage
        self.add_unit_num()

    @classmethod
    def add_unit_num(cls):
        cls.__unit_num += 1

    @classmethod
    def get_unit_num(cls):
        return cls.__unit_num

u1 = Unit("u1", 100, 20)
u2 = Unit("u2", 200, 10)
print(u1.get_unit_num())  # 2
print(u2.get_unit_num())  # 2

u3 = Unit("u3", 300, 30)
print(u3.get_unit_num())  # 3
print(Unit.get_unit_num())  # 3

我们在__init__方法中调用一次类方法add_unit_num(),就可以把总实例数统计到类属性__unit_num中。

静态方法

静态方法用修饰器@staticmethod定义。静态方法不能访问类属性,也不能访问实例属性。静态方法可以在类的命名空间内定义一些功能性代码,通常用于实现一些与类相关的工具函数。

class Unit:
    type = "unit"

    def __init__(self, name, hp, damage):
        self.name = name
        self.hp = hp
        self.damage = damage

    @staticmethod
    def calc_hp_after_attack(hp, damage):  # 没有隐式的第一参数
        return hp - damage

u1 = Unit("u1", 100, 20)
u2 = Unit("u2", 200, 10)
print(u1.calc_hp_after_attack(u1.hp, u2.damage))  # 90

属性方法

属性方法以方法的形式定义,但是可以像属性一样进行访问,其作用是支持对属性的灵活操作。

属性方法相当于允许更细致的设置一个属性的访问更改删除操作,具体的做法是:实现属性的gettersetterdeleter方法。

假设我们有这样的需求:定义一个Circle类,它具有直径diameter和半径radius两个属性。我们希望修改其中一个属性时,另外一个属性也随之变化。也就是:

circle = Circle(5)
print(circle.radius)  # 5
print(circle.diameter)  # 10

circle.diameter = 14
print(circle.radius)  # 7.0

circle.radius = 12
print(circle.diameter)  # 24

用此前的知识似乎无法实现这样的功能,但属性方法可以解决:

class Circle:
    def __init__(self, radius):
        self.__radius = radius
        self.__diameter = 2 * radius

    # 属性的getter方法
    @property
    def radius(self):
        return self.__radius

    # 属性的setter方法
    @radius.setter
    def radius(self, value):
        self.__radius = value
        self.__diameter = value * 2

    @property
    def diameter(self):
        return self.__diameter

    @diameter.setter
    def diameter(self, value):
        self.__diameter = value
        self.__radius = value * 0.5

将访问、修改属性的操作定义为函数,就允许了我们除了获取、修改变量本身之外,还可以做一些其他的手脚。

上面的例子没有体现属性的deleter方法。它的一般实现可以是:

class Circle:
    ...

    # 属性的deleter方法
    @radius.deleter
    def radius(self):
        print("delete radius")
        del self.__radius

    @diameter.deleter
    def diameter(self):
        print("delete diameter")
        del self.__diameter

del circle.diameter  # delete diameter
# print(circle.diameter)  # AttributeError: 'Circle' object has no attribute '_Circle__diameter'

反射

Python中的反射

在计算机科学中反射(reflection)是指计算机程序在运行时可以检查、访问、和修改它本身状态或行为的一种能力。表现在Python面向对象编程中有四个内置函数:getattr()setattr()hasattr()delattr(),可以通过字符串的形式操作对象的属性和方法。

class A:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def info_x(self):
        print(f"x: {self.x}")

    def info_y(self):
        print(f"y: {self.y}")

    def info_z(self):
        print(f"z: {self.z}")

a = A(10, 20, 30)

hasattr()函数用于判断对象是否包含对应的属性或方法:

print(hasattr(a, "x"))  # True
print(hasattr(a, "info_x"))  # True
print(hasattr(a, "info_w"))  # False

getattr()函数用于获取对象的属性或方法:

print(getattr(a, "x"))  # 10
info_x = getattr(a, "info_x")
info_x()  # x: 10

setattr()函数用于设置对象的属性或方法:

setattr(a, "x", 100)
print(getattr(a, "x"))  # 100

delattr()函数用于删除对象的属性或方法:

delattr(a, "y")
print(getattr(a, "y"))  # AttributeError: 'A' object has no attribute 'y'

反射的应用

假如我们现在想创建一个类A2Z,它具有a-z26个属性与info_a()info_z()26个方法,手动创建这些属性和方法是非常繁琐的。这时我们可以利用反射来动态的创建它们:

a_to_z = "abcdefghijklmnopqrstuvwxyz"

class A2Z:
    pass

a2z = A2Z()

for ch in a_to_z:
    # 动态的创建属性
    setattr(a2z, ch, ord(ch) - ord('a') + 1)

    # 动态的创建方法
    def info_ch():
        print(f"{ch}: {getattr(a2z, ch)}")

    setattr(a2z, f"info_{ch}", info_ch)

print(a2z.d)  # 4
a2z.info_z()  # z: 26

魔术方法

魔术方法通常以双下划线包围,用于实现类的特殊行为。下面以一个Vector3d类为例,介绍一些常用的魔术方法。

class Vector3d:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

v1 = Vector3d(3, 4, 5)
v2 = Vector3d(1, 2, 3)

__len__

len(obj)时调用。__len__方法的返回值只能是整数。

class Vector3d:
    ...

    def __len__(self):
        return 3

print(len(v1))  # 3

__repr__和__str__

__repr__方法的返回值应该是一个可以用来重新创建对象的字符串。
__str__方法在str(obj)时调用,应当返回实例格式良好、可读性强的字符串表示。

print(obj)时会优先使用__str__方法的返回值,如果没有定义__str__方法,则会使用__repr__方法。

class Vector3d:
    ...

    def __repr__(self):
    return f"Vector3d({self.x}, {self.y}, {self.z})"

    def __str__(self):
        return f"({self.x}, {self.y}, {self.z})"

print(str(v1))  # (3, 4, 5)
print(repr(v1))  # Vector3d(3, 4, 5)
# 会优先调用__str__方法
print(v1)  # (3, 4, 5)

__call__

__call__方法使得实例可以像函数一样被调用。

这个例子中我们约定,调用obj(x, y, z)时设置向量的xyz分量。

class Vector3d:
    ...

    def __call__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

v2(2, -2, 1)
print(v2)  # (2, -2, 1)

运算符重载

我们为Vector3d类定义加法、减法、乘法操作。这个例子中我们约定,加减法就是普通的按元素加减,而乘法满足:

obj*常数时返回缩放后的向量

obj1*obj2时返回点乘数值

class Vector3d:
    ...

    # 重载加号运算符,obj1+obj2时调用
    def __add__(self, other):
        return Vector3d(self.x + other.x, self.y + other.y, self.z + other.z)

    # 重载减号运算符,obj1-obj2时调用
    def __sub__(self, other):
        return Vector3d(self.x - other.x, self.y - other.y, self.z - other.z)

    # 重载乘号运算符
    def __mul__(self, other):
        if isinstance(other, (int, float)):
            return Vector3d(self.x * other, self.y * other, self.z * other)
        else:
            return self.x * other.x + self.y * other.y + self.z * other.z

print(v1)  # (3, 4, 5)
print(v2)  # (2, -2, 1)
print(v1 + v2)  # (5, 2, 6)
print(v1 - v2)  # (1, 6, 4)
print(v1 * v2)  # 3
print(v1 * 2)  # (6, 8, 10)

反运算(右侧运算)

如果只有上述运算符重载,下面的代码会报错:

print(2 * v1)  # TypeError: unsupported operand type(s) for *: 'int' and 'Vector3d'

这是因为整数类型的乘法不适用。解决这个问题需要定义Vector3d类的右侧乘法__rmul__

class Vector3d:
    ...

    def __rmul__(self, other):
        return self.__mul__(other)

print(2 * v1)  # (6, 8, 10)

__getitem__和__setitem__

__getitem__在取obj[key]时调用,这个例子中我们约定obj[key]返回向量第i个分量;

__setitem__在设置obj[key]=value时调用,这个例子中我们约定obj[key]=value设置向量第i个分量。

class Vector3d:
    ...

    def __getitem__(self, key):
    if key == 0:
        return self.x
    elif key == 1:
        return self.y
    elif key == 2:
        return self.z
    else:
        raise IndexError(f"index out of range: {key}")

    def __setitem__(self, key, value):
        if key == 0:
            self.x = value
        elif key == 1:
            self.y = value
        elif key == 2:
            self.z = value
        else:
            raise IndexError(f"index out of range: {key}")

print(v1[0], v1[1], v1[2])  # 18 -24 15
v1[0] = 2
print(v1)  # (2, -24, 15)