Python 06_Python面向对象编程

一、Python面向对象编程之概念简介

1.1 什么是面向对象编程OOP

面向对象编程(Object-Oriented Programming,简称OOP) 是一种编程范式,它使用 类(class)对象(instance - 类的实例) 来设计应用程序和计算机程序。这些对象由数据(属性)和能够操作这些数据的方法组成。面向对象编程的主要目标是提高软件的可重用性、可维护性和灵活性。在Python中,面向对象编程具有以下几个核心概念:

  • 类(Class):类是创建对象的模板或蓝图。它定义了一组属性(称为字段或变量),以及操作这些数据的方法。一个类可以包括基本的数据属性(静态的信息片段),以及能够对数据执行特定功能的方法。

  • 对象(Object):对象是类的实例(instance)。如果类是蓝图,对象就是根据这个蓝图构建的房子。每个对象都拥有类定义的字段和方法的具体实例。即便两个对象来自同一个类,它们也可以拥有不同的数据属性。

  • 封装(Encapsulation):封装是面向对象编程的一个主要特点,指的是将对象的数据(属性)和代码(方法)捆绑在一起,形成一个独立的单元。在封装的概念中,类通常会防止外部代码直接访问内部数据结构,而是通过方法(称为getter和setter)来操作数据,这提供了更好的数据控制和更容易的维护。

  • 继承(Inheritance):继承允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类重用父类的代码可以减少冗余,而且子类可以扩展或修改从父类继承的行为,这提高了代码的可用性和可扩展性。

  • 多态(Polymorphism):多态是指同一个接口支持不同的底层形态(数据类型)。在Python中,多态表现为可以通过相同的接口调用不同类的方法,具体调用哪个方法取决于调用方法的对象。比如,不同的类可能都定义了一个相同的方法名,但是每个类的该方法的具体实现可能不同。

  • 抽象(Abstraction):抽象是简化复杂的现实问题的方法,它通常用于隐藏复杂度,只显示最相关的细节。在Python中,可以使用抽象类来定义不能直接实例化的类,这些抽象类意在专门为其他类提供基本的、通用的功能模板,具体实现则留给继承了抽象类的子类。 通过使用这些OOP原则,Python程序员能够创建可读性强、易于扩展和维护的应用程序。

二、Python面向对象编程之类详解

2.1 Python类的定义与实例化

  • Python类的定义

在Python中,通过 class 关键字定义一个类,比如定义一个人的类(Person)。按照 Python 的编程习惯,类名以大写字母开头。因此可以这样定义:

1
2
3
# 类的定义 方式一
class Person:
    pass

在这个Person类的定义里面,并没有继承任何类,除了这样定义以外,还可以有以下两种定义方式:

1
2
3
4
5
6
7
# 类的定义 方式二
class Person():
    pass

# 类的定义 方式三
class Person(object):
    pass

这三种方式,在Python3中,是没有区别的,但是在Python2中,则有一定的区别。

在Python2中,对于第一种定义的方法,Person类只有有限的几个内建函数 ‘doc’, ‘module’, ’name’,而对于第二种、第三种定义的方法,则会继承Python object对象的更多的内建函数,可以更便捷的操作对象。这是Python2版本的差异。在Python3中,这三种方式都可以定义一个类,功能相同。

  • Python类的实例化

定义了类之后,就可以对类进行实例化了,实例化是指 把抽象的类赋予实物的过程。比如,定义好 Person 这个类后,就可以实例化多个 Person 类的对象出来了。 创建实例使用 类名 + (),类似函数调用的形式创建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# -*- coding: utf-8 -*-

# 类的定义 
class Person(object):
    pass


if __name__ == '__main__':
    print("Main Process running...")

    # 实例化 Person 类的对象
    xR = Person()
    xW = Person()

    print(xR)
    print(xW)

2.2 Python类实例属性的定义与使用

虽然 1.1 小节中已经通过 Person 类创建出 xR、xW 等实例对象,但是这些实例看上去并没有任何区别。在现实世界中,一个人拥有名字、性别、年龄等等的信息,在Python中,可以通过以下的方式赋予实例这些属性,并且把这些属性打印出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if __name__ == '__main__':
    xR = Person()
    
    xR.name = '小R'
    xR.sex = 'boy'
    xR.age = 16
   
    print(xR.name)
    print(xR.sex)
    print(xR.age)
    xR.age += 1
    print(xR.age)

除此以外,这些属性也可以和普通变量一样进行运算。比如xiaohong长大了一岁:xR.age += 1

2.3 Python类实例属性的初始化

通过前面 1.2 小节的方式定义一个实例的属性非常方便,但也有一些问题。

  • 首先,如果定义属性的过程中使用了不同的属性名字,比如性别,xR 使用了sex,xW 使用了gender,那对于一个类的不同实例,存储一个信息就用了两个不同的属性,这样的代码在后面将会难以维护。
  • 其次,名字、性别、年龄等等,都是人的基本信息,在抽象一个类的时候,理应包含这些信息。

所以,在定义 Person 类时,可以为 Person 类添加一个特殊的 __init__() 方法,当创建实例时,__init__() 方法将会被被自动调用,这样就能在此 __init__() 方法中为每个实例都统一加上必要属性, 如下所示:

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-

# 类的定义 
class Person(object):
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

需要注意的是,__init__() 方法的第一个参数必须是 self(也可以用别的名字,但建议使用习惯用法),后续参数则可以自由指定,和定义函数没有任何区别。 在定义类后,就可以相应的实例化对象了,需要注意的是,在实例化的时候,需要提供 __init__() 方法中除了 self 以外的所有参数,如下所示:

1
2
3
4
# -*- coding: utf-8 -*-

xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)

而访问这些属性的方式和之前的一样:

1
2
3
4
5
6
print(xiaohong.name)
print(xiaohong.sex)
print(xiaohong.age)

# 但当访问不存在的属性时,依然会报错
print(xiaohong.birth)

Tips: 要特别注意的是,在定义 __init__() 方法时如果忘记了 self 参数,那么在实例化对象的时候 Python 解释器会将 __init__() 方法中的第一个参数都当作是 self 参数,这样就会导致实例化时报 TypeError: __init__() ... 的错误。

2.4 Python类属性

类和实例对象是有区别的,类是抽象,是模板,而实例则是根据类创建的对象,比如类:动物,只是一个抽象,并没有动物的详细信息,而猫、狗等,则是具体的动物,是类的对象。

在前面,实例对象绑定的属性只属于这个实例,绑定在一个实例上的属性不会影响其它实例;同样的,类也可以绑定属性,但是类的属性不属于任何一个对象,而是属于这个类。如果在类上绑定一个属性,则所有实例都可以访问类的属性,并且,所有实例访问的类属性都是同一个!也就是说,实例属性每个实例各自拥有,互相独立,而类属性有且只有一份。

Tips: Python中类属性 与 C++ 中 类的 static 成员变量 类似。

定义类属性可以直接在 class 中定义,比如在前面的 Person 类中,加入 国籍 的类属性:

1
2
3
4
5
6
class Person(object):
    nationality = 'China'
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

在上面的代码中,nationality 就是属于 Person 这个类的类属性,此后,通过 Person() 实例化的所有对象,都可以访问到 localtion,并且得到唯一的结果。

1
2
3
4
5
6
7
xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)

print(xiaoming.nationality) # ==> China
print(xiaohong.nationality) # ==> China
# 类属性,也可以通过类名直接访问
print(Person.nationality) # ==> China

类属性也是可以动态添加和修改的,需要注意的是,因为类属性只有一份,所以改变了,所有实例可以访问到的类属性都会变更:

1
2
3
Person.nationality = 'America'
print(xiaoming.nationality) # ==> America
print(xiaohong.nationality) # ==> America

那通过实例,可不可以修改类属性呢?通过实例来尝试一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
import sys

class Person(object):
    nationality = 'China'
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

if __name__ == '__main__':
    print("Main Process running...")

    xiaoming = Person('Xiao Ming', 'boy', 13)
    xiaohong = Person('Xiao Hong', 'girl', 14)
    print(xiaoming.nationality) # ==> China
    print(xiaohong.nationality) # ==> China
    # 类属性,也可以通过类名直接访问
    print(Person.nationality) # ==> China

    xiaoming.nationality ='America'
    print(xiaoming.nationality) # ==> America
    print(xiaohong.nationality) # ==> China
    print(Person.nationality) # ==> China

从上面示例中可以看出,通过实例修改类属性,并不会影响到类属性,因为类属性是属于类的,而实例属性是属于实例的。事实上,通过实例方法修改类属性,只是给实例绑定了一个对应的实例属性(与类属性同名),并没有实际修改类属性,因此,需要特别注意,尽量不要通过实例来修改类属性,否则很容易引发意想不到的错误

2.5 Python类属性和实例属性的优先级及可见性

从前文介绍内容可知,属性可以分为类属性和实例属性,那么问题就来了,如果类属性和实例属性名字相同时,会怎么样,这就涉及Python中类属性和实例属性的优先级的问题了。

以上文定义的 Person 类来做一个实验,在前面类定义的基础上,在实例属性中,也初始化一个 nationality 的属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Person(object):
    nationality = 'China'
    def __init__(self, name, sex, age, nationality):
        self.name = name
        self.sex = sex
        self.age = age
        self.nationality = nationality

# 初始化两个实例,并把 nationality 打印出来。
xiaoming = Person('Xiao Ming', 'boy', 13, 'America')
xiaohong = Person('Xiao Hong', 'girl', 14, 'America')
print(xiaoming.nationality) # ==> America
print(xiaohong.nationality) # ==> America
# 类属性,也可以通过类名直接访问
print(Person.nationality) # ==> China

可见,在类属性和实例属性同时存在(同名)的情况下,实例属性的优先级是要高于类属性的,通过实例对象只能访问(操作)实例的属性,不能操作与实例属性同名的类属性。此时类属性在实例对象中被覆盖(隐藏,不能被实例对象访问),只能通过类来访问类属性。

2.6 Python类和实例属性的访问控制(访问限制)

Python 中并不是所有的(类(class)和实例(instance)的)属性都可以在外部访问的,这种不能被外部访问的属性称为 私有属性。私有属性是以双下划线 __ 开头的属性。

 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
# 示例一
class Person(object):
    __nationality = 'China'
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

if __name__ == '__main__':
    print(Person.__nationality) 

# Traceback (most recent call last):
#   File "/Users/mac/classes.py", line 12, in <module>
#     print(Person.__nationality)
# AttributeError: type object 'Person' has no attribute '__nationality'

# 示例二

class Person(object):
    def __init__(self, name, sex, age, nationality):
        self.name = name
        self.sex = sex
        self.age = age
        self.__nationality = nationality

if __name__ == '__main__':
    xiaoming = Person('Xiao Ming', 'boy', 13, 'China')
    print(xiaoming.name)
    print(xiaoming.sex)
    print(xiaoming.age)
    
    print(xiaoming.__nationality) 

# Traceback (most recent call last):
#   File "/Users/mac/classes.py", line 17, in <module>
#     print(xiaohong.__nationality)
# AttributeError: 'Person' object has no attribute '__nationality'

由此可见 在类外边使用类名 或 类实例对象 访问私有属性将会抛出异常,提示没有这个属性。 虽然私有属性无法从外部访问,但是,从类的内部是可以访问的。私有属性是为了保护类或实例属性不被外部污染而设计的。

Tips: Python中,类 或 对象 的 私有属性 与 C++ 中 类的 私有成员变量 类似。

私有属性没有办法从外部访问,只能在类的内部操作;那如果外部需要操作私有属性怎么办?这个时候可以通过定义类或者实例的方法来操作(访问)私有属性。

2.7 Python类实例方法

实例的方法 指的就是在类中定义的函数,实例方法的第一个参数永远都是self,self是一个引用,指向调用该方法的实例对象本身,除此以外,其它参数和普通函数是完全一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person(object):
    def __init__(self, name, sex, age, nationality):
        self.name = name
        self.sex = sex
        self.age = age
        self.__nationality = nationality

    def get_nationality(self):
        return self.__nationality

    def get_name(self):
        return self.name

在上面的定义,__nationality 是实例的私有属性,从外部是无法访问的,而 get_nationality(self) 就是一个实例方法,在实例方法里面是可以操作私有属性的,注意,它的第一个参数是self。

同样的,在实例方法里面也是可以操作普通属性的,比如上面的 get_name(self) 实例方法 可以操作普通 name 属性。

另外,上文中 介绍的 __init__() 其实也可看做是一个特殊的实例方法。

通过定义 get_nationality(self) 方法,在外部就可以通过这个方法访问私有属性了:

1
2
xiaoming = Person('Xiao Ming', 'boy', 13, 'China')
print(xiaoming.get_nationality()) # ==> China

通过定义实例方法来操作私有属性的这种方法是推荐的,这种数据封装的形式除了能保护内部数据一致性外,还可以简化外部调用的难度。当然,实例方法并不仅仅是为私有属性服务的,我们可以把和类的实例有关的操作都抽象成实例方法,比如:打印 Person 类实例的详细信息等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person(object):
    def __init__(self, name, sex, age, nationality):
        self.name = name
        self.sex = sex
        self.age = age
        self.__nationality = nationality

    def get_nationality(self):
        return self.__nationality

    def get_info(self):
        fmt_tmp = 'name = {}, age = {}, sex = {}, nationality = {}'
        return fmt_tmp.format(self.name, self.age, self.localtion, self.__nationality)


xiaoming = Person('Xiao Ming', 'boy', 13, 'China')
print(xiaoming.get_nationality()) # ==> China
print(xiaoming.get_info()) # ==> name = Xiao Ming, age = 13, sex = boy, nationality = China

2.8 Python类方法

在 1.7 小节中,为了操作实例对象的私有属性,我们定义了实例方法;同样的,如果需要需要操作类的私有属性,则应该定义类的方法。

默认的,在 class 中定义的全部是实例方法,实例方法第一个参数 self 是实例本身。

要定义类方法,需要在函数名前面一行加上 @classmethod 装饰器,类方法的第一个参数永远是cls,cls 是一个引用,指向调用该方法的类本身, 如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person(object):
    __nationality = 'China'
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

    @classmethod
    def set_nationality(cls, nationality):
        cls.__nationality = nationality

    @classmethod
    def get_localtion(cls):
        return cls.__nationality

if __name__ == '__main__':
    print(Person.get_localtion()) # ==> China
    Person.set_nationality('America')
    print(Person.get_localtion()) # ==> America

    xiaoming = Person('Xiao Ming', 'boy', 13)
    print(xiaoming.get_localtion()) # ==> America
    xiaoming.set_nationality('China')
    print(xiaoming.get_localtion()) # ==> China

和实例方法有所不同,这里有两点需要特别注意:

  • 类方法需要使用@classmethod来标记为类方法,否则定义的还是实例方法
  • 类方法的第一个参数将传入类本身,通常将参数名命名为 cls,上面的 cls.__localtion 实际上相当于Animal.__localtion。

因为是在类上调用,而非实例上调用,因此类方法无法访问任何实例属性,只能获得类的引用。

类方法在Python中非常有用,它们通常用于以下场景:

  • 共享状态:当多个实例需要共享某些状态时,类方法可以用来更新这些共享状态,而不需要每个实例都执行相同的操作。
  • 配置管理:类方法可以用来管理类的配置或设置,这些配置对于所有实例都是通用的。
  • 数据初始化:在初始化类级别的数据时,类方法可以确保所有实例使用的是最新的数据。
  • 工厂方法:类方法可以用作工厂方法,根据不同的参数创建不同的实例。
  • 工具方法:对于执行与类相关但与实例无关的任务,如数据校验、格式化、计算等,类方法是一个很好的选择。
  • 属性访问控制:类方法可以用来控制对类属性的访问,例如,通过类方法来获取或设置属性值。
  • 装饰器:类方法可以作为装饰器使用,用于增强或修改类的方法或属性。
  • 注册或注销实例:在某些设计模式中,如注册表模式,类方法可以用来注册或注销类的实例。

2.9 Python 类的静态方法

在Python中,静态方法(Static Method) 是一种类方法,定义了一个类的功能,它不需要传递实例(self)即可被调用,也不依赖于类的任何属性,可以用来创建工具函数,这些函数与类的任何实例无关。例如数据的验证,处理不需要修改类状态的数据等。

在Python中,静态方法 通过装饰器 @staticmethod 来定义。这个装饰器告诉Python解释器,该方法不需要传递实例(self),也不需要访问实例的属性或方法。这使得静态方法在调用时更加灵活,因为它们不受实例状态的影响。

Tips: 在Python中,@staticmethod 装饰器用于将一个方法转换为静态方法。

静态方法的定义格式如下:

1
2
3
@staticmethod
def static_func_name([arg1, arg2 ...]):
    ...

调用静态方法的格式如下:

1
2
3
4
# 方式一:
类名.静态方法名(args)  # 推荐使用
# 方式二:
对象名.静态方法名(args)

静态方法通常用于以下场景:

  • 工具函数:当你在类中定义了一些通用的函数,这些函数不依赖于类的实例属性,也不需要访问实例方法时,可以将它们定义为静态方法。例如,一些数学计算(sin、cos、tan)或者数据处理工具。
  • 类级别的操作:有时你需要执行一些操作,这些操作是在类级别上进行的,而不是在实例级别。例如,你可能想要重置类的某个状态或者更新类的静态属性。
  • 多个构造函数:在Python中,每个类只能有一个构造函数(init)。如果你需要根据不同的参数集创建多个构造函数,可以使用静态方法来模拟不同的构造函数。
  • 工厂方法:在某些设计模式中,特别是当你想要使用工厂模式来创建类的实例时,静态方法可以用来返回不同类型的实例。
  • 回调函数:在某些框架中,如Twisted,你可能会定义一些回调函数,这些函数作为参数传递给框架的其他部分,并在适当的时候被调用。这些回调通常是静态方法。
  • 协程和异步:在使用asyncio或者类似的库时,静态方法可以作为协程的入口点,因为它们不依赖于任何特定的实例。

示例:

1
2
3
4
5
6
7
8
9
# 静态方法的定义
class MyClass:
    @staticmethod
    def static_method(x, y):
        return x + y

# 使用静态方法
result = MyClass.static_method(3, 4)
result1 = MyClass().static_method(3, 4)

三、Python面向对象之类的继承和多态

3.1 Python类的继承

继承(Inheritance) 允许一个类(称为子类)继承另一个类(称为父类或基类)的属性和方法,也就是说,继承可以让子类把父类的所有的属性和方法获取到。这样做的主要好处是代码重用,可以将通用的代码放在一个父类中,通过继承机制在多个子类中复用这些代码。

只有一个父类的继承称为 单继承

对人类的抽象可以定义为 Person 类,而学生、老师等,也都是人类,所以,在Python当中,如果定义学生 Student 的类,可以继承 Person 类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Person(object):
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

class Student(Person):
    def __init__(self, name, sex, age, score):
        super(Student, self).__init__(name, sex, age)
        # Person.__init__(self, name, sex, age) # 也可写成这样
        self.score = score

student = Student('Alice', 'girl', 13, 100)
print(student.name) # ==> Alice
print(student.sex) # ==> girl
print(student.age) # ==> 13
print(student.score) # ==> 100

如上示例所示,在定义Student类的时候,由于继承了Person类,所以Student类自动拥有name、sex 和 age 属性,因此,在定义Student类的时候,只需要把其它额外的属性加上即可,不需要重复去定义 Person 类中已有属性, 这样就定义了一个学生类 Student。

在定义继承类的时候,有几点是需要注意的:

  • class Student()定义的时候,需要在括号内写明继承的类Person,即:class Student(Person)
  • 在继承类的 __init__() 方法,需要调用 super(Student, self).__init__(name, sex, age) 来初始化从父类继承过来的属性。

Tips: super的两种使用方式

  • A对象的 super().方法() : 直接从mro顺序表里找相应的方法进行调用,并且A作为起始位置
  • A对象的 super(B,self).方法(): 直接直接从mro顺序表里找相应的方法进行调用,并且B作为起始位置

3.2 Python类型判断(isinstance)

在 Python 编程的代码中,通常有程序员自定义的类(class)类型,也有 Python 自有的str、list、dict等内置数据类型,它们的本质都是都是Python中的一种数据类型。在 Python 编程的过程中,有时需要判断一个变量实例的数据类型,通过函数内置的 isinstance() 函数就可以判断一个变量是否是指定的某一数据类型。

类型判断是非常重要的,因为在 Python 中,所有的数据类型都继承自 object 类,因此,可以用 isinstance() 来判断一个对象是否是某个类或某个类的实例。

 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
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

# 使用 isinstance 判断类型
print(isinstance(p, Person))    # True, p 是 Person 类型
print(isinstance(p, Student))   # False, p 不是 Student 类型
print(isinstance(p, Teacher))   # False, p 不是 Teacher 类型
# 这说明在继承链上,一个父类的实例不能是子类类型,因为子类比父类多了一些属性和方法。

print(isinstance(s, Person))    # True, s 是 Person 类型
print(isinstance(s, Student))   # True, s 是 Student 类型
print(isinstance(s, Teacher))   # False, s 不是 Teacher 类型
# 这说明在一条继承链上,一个实例可以看成它本身的类型,也可以看成它父类的类型。


# isinstance也可以用于Python自有数据类型的判断。
s = 'this is a string.'
n = 10
isinstance(s, int) # ==> False
isinstance(n, str) # ==> False

3.3 抽象基类

抽象基类(Abstract Base Classes,简称ABC) 是一个包含一组抽象方法(即只有方法签名而没有具体实现)的类,它的主要作用是定义一组接口或规范,而不是提供具体的实现(所以,抽象基类不能被直接实例化)。抽象基类通常用于定义一组类的通用行为,以确保继承了抽象基类的派生类具有相似的接口和行为(规范)。

在Python编程中,抽象基类(Abstract Base Classes,简称ABC) 是一种非常有用的工具,用于定义接口和规范类的行为。抽象基类提供了一种机制,可以确保子类具备特定的方法和属性。

抽象基类是对一类事物的特征行为的抽象,由抽象方法组成。在Python3中可以使用abc模块,该模块中包括了创建抽象基类需要的修饰符和元类型:

  • abc.ABCMeta 是一个抽象基类的元类,用来生成抽象基础类的元类。由它生成的类可以被直接继承。
  • abc.ABC 是一个抽象基类(辅助类),让程序员可以不用关心元类概念,直接继承它,就有了ABCMeta元类。使用时注意元类冲突
  • abc.abstractmethod 是一个修饰器,用来定义抽象方法。除了这个装饰器,其余装饰器都被deprecated了。
  • 参考博文:https://cloud.tencent.com/developer/article/2048936

下面示例展示了一个抽象基类的定义和使用:

 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
40
41
from abc import ABC, abstractmethod

# 定义一个名为 Shape 的 抽象基类,其包含两个抽象方法area()和perimeter(),分别用于计算形状的面积和周长
# 基类不实现具体的方法,由子类去实现
class Shape(ABC):
    @abstractmethod
    def area(self):         # 抽象方法
        pass

    @abstractmethod
    def perimeter(self):    # 抽象方法
        pass

# 定义了Rectangle和Circle两个 Shape 的子类
# 子类必须实现抽象基类中定义的所有抽象方法,否则实例化子类时会抛出TypeError异常。
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius * self.radius

    def perimeter(self):
        return 2 * 3.14 * self.radius

rectangle = Rectangle(3, 4)
circle = Circle(5)

print(isinstance(rectangle, Shape))  # True
print(isinstance(circle, Shape))  # True

抽象基类的应用场景

  • 强制子类实现特定接口

抽象基类可以用于强制子类实现特定的接口或方法,这在项目开发中尤为重要,特别是在团队合作或开发大型项目时。通过使用抽象基类,可以确保每个子类都实现了特定的方法,从而保证了整个系统的稳定性和可靠性。例如,假设正在开发一个数据库访问框架,可以定义一个抽象基类来规范所有数据库连接对象的行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from abc import ABC, abstractmethod

class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def disconnect(self):
        pass

    @abstractmethod
    def execute_query(self, query):
        pass

然后,可以创建具体的数据库连接类,如MySQLConnection、PostgreSQLConnection等,确保它们都实现了connect()、disconnect()和execute_query()等方法。

  • 提供统一的API接口

抽象基类还可以用于提供统一的API接口,使得不同的类具有相似的行为和接口。这样可以简化代码逻辑,提高代码的可读性和可维护性。例如,假设正在开发一个机器学习框架,可以定义一个抽象基类来规范所有机器学习模型的行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from abc import ABC, abstractmethod

class MachineLearningModel(ABC):
    @abstractmethod
    def train(self, X, y):
        pass

    @abstractmethod
    def predict(self, X):
        pass

    @abstractmethod
    def evaluate(self, X, y):
        pass

然后,可以创建具体的机器学习模型类,如LinearRegression、RandomForest等,确保它们都实现了train()、predict()和evaluate()等方法。

  • 定义插件架构

抽象基类可以用于定义插件架构,使得插件之间具有相似的接口和行为。这样可以灵活地扩展和替换插件,提高系统的可扩展性和灵活性。例如,假设正在开发一个图像处理应用,可以定义一个抽象基类来规范所有图像处理插件的行为:

1
2
3
4
5
6
from abc import ABC, abstractmethod

class ImageProcessorPlugin(ABC):
    @abstractmethod
    def process(self, image):
        pass

然后,可以创建具体的图像处理插件类,如ResizePlugin、FilterPlugin等,确保它们都实现了process()方法。这样,用户可以根据需要选择并使用不同的插件,而无需修改核心代码。

3.4 Python中的多继承

多重继承(Multiple Inheritance) 是一种面向对象编程的特性,允许一个类同时继承多个父类的方法和属性。在 Python 中,多重继承可以通过在类的定义中使用多个父类来实现。

多重继承的好处是可以复用多个父类的代码,但同时它也带来了一些复杂性。其中一个问题是,当某个类有多个父类时,如果这些父类具有相同的方法或属性,那么子类将继承多个相同的成员,这可能会导致名称冲突或其他问题。

为了解决这个问题,Python 使用了一种叫做方法解析顺序(Method Resolution Order,MRO)的算法来确定子类的方法调用顺序。MRO 是一个列表,其中包含了子类的所有父类,以及这些父类的父类,依次类推。当子类调用一个方法时,Python 会按照 MRO 的顺序依次在每个父类中查找该方法。如果在某个父类中找到了该方法,则直接调用该方法。如果在一个父类中没有找到该方法,则继续在下一个父类中查找,直到找到为止。

Tips: 可以通过类名 <类名>.__mro__ 或类名 <类名>.mro() 获得“类的层次结构”,方法解析顺序也是按照这个“类的层次结构”寻找到。

MRO 的算法如下:

  • 从子类开始,将子类添加到 MRO 的末尾。
  • 对于子类的每个父类,将其添加到 MRO 的末尾。
  • 对于每个父类的父类,将其添加到 MRO 的末尾。
  • 重复步骤 2 和步骤 3,直到所有父类都被添加到 MRO。

以下是一个演示多重继承的 Python 示例:

 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 A:
    def __init__(self):
        print("Class A constructor")

    def method1(self):
        print("Method 1 of class A")

class B:
    def __init__(self):
        print("Class B constructor")

    def method2(self):
        print("Method 2 of class B")

class C(A, B):
    def __init__(self):
        super().__init__()
        super().__init__()
        print("Class C constructor")

    def method3(self):
        print("Method 3 of class C")

c = C()
c.method1()
c.method2()
c.method3()
# 输出:
# Class A constructor
# Class B constructor
# Class C constructor
# Method 1 of class A
# Method 2 of class B
# Method 3 of class C

在这个示例中,类 C 同时继承了类 A 和类 B。当创建一个 C 类的实例时,会先调用 A 类的构造方法,再调用 B 类的构造方法,最后调用 C 类的构造方法。当你调用 C 类的方法时,Python 会按照 MRO 的顺序依次在 C 类、A 类和 B 类中查找该方法。

Python中的MRO是通过C3线性化算法来确定的。C3线性化算法是一种广度优先搜索算法,它遵循以下几个规则:

  • 子类的MRO永远在父类的MRO之前。
  • 如果一个类有多个父类,那么它们的MRO的顺序将按照它们在类定义时的顺序进行合并。
  • 如果多个父类的MRO中存在相同的类,那么只保留第一个出现的类,其余的将被忽略。

下面我们通过一个例子来演示MRO的工作原理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class A:
    def method(self):
        print("This is method A")

class B(A):
    def method(self):
        print("This is method B")

class C(A):
    def method(self):
        print("This is method C")

class D(B, C):
    pass

d = D()
d.method()  # This is method B

在上面的例子中,定义了四个类A、B、C和D。类A定义了方法method,类B继承了类A并重写了方法method,类C也继承了类A并重写了方法method,类D继承了类B和类C。

创建了一个类D的实例d,并调用了其继承自类B和类C的方法method。根据MRO的规则,类D的MRO为D, B, C, A。因为类D继承自类B和类C,所以类B和类C的MRO会在类D的MRO之前。最后,因为类B和类C都继承自类A,所以类A的MRO会在类B和类C的MRO之前。

从输出结果可以看出,当调用d.method()时,Python按照类D的MRO的顺序查找方法method,并调用了类B中重写的方法。

如果一个类继承了多个父类,这种继承就称为 多继承(multiple inheritance)*。

  • 如果多个父类的方法和属性都不相同,子类会把所有的不相同的方法和属性都继承下来
  • 如果多个父类的方法和属性有相同的情况,子类会按照继承顺序, 先继承(father1, father2) father1的相关的方法和属性

Tips: Python类的继承机制

  • 在Python中,类的继承机制允许多重继承,这意味着一个类可以同时继承多个父类。在Python中,多继承引入了寻找方法和属性的顺序问题,即当一个类继承自多个父类时,Python2.X需要一个明确的规则来决定从哪个父类中寻找方法或属性,这就涉及到深度优先和广度优先的搜索策略。
  • 在Python 2中,有经典类(不显式继承自 object)和新式类(显式继承自 object)之分。Python 3中,所有类默认都是新式类(即使不显式继承自 object)。‘即使不显式’通常用于描述某些操作或行为在没有明确指定的情况下依然会发生的情况,这种说法帮助简化代码和提高编程效率,但同时它也要求开发者理解默认行为,以避免意外的错误。
  • 在Python 3中,类的继承和方法解析遵循C3线性化算法。C3线性化是一种特定的算法,用于解决在具有多重继承的类体系中确定方法解析顺序的问题。这个算法确保任何类都会在其父类之前被检查,同时也保持了父类的顺序。这意味着,Python 3中不再单纯使用传统的深度优先或广度优先策略来解析方法调用。

3.5 Python中的多态

多态(Polymorphism) 是面向对象编程中的一个重要概念,它是指一个对象可以表现出多种形态。在Python中,多态通常是通过继承和子类重写父类的方法来实现的。

实现多态的步骤:

  • 定义一个父类(Base),实现某个方法(比如:run)
  • 定义多个子类,在子类中重写父类的方法(run),每个子类run方法实现不同的功能

假设我们定义了一个函数,需要一个Base类型的对象的参数,那么调用函数的时候,传入Base类不同的子类对象,那么这个函数就会执行不同的功能,这就是多态的体现。

 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
class Animal(object):
    """动物类"""
 
    def func(self):
        print('动物发出了声音')
 
 
class Cat(Animal):
    """猫类"""
 
    def func(self):
        print('喵 喵 喵')
 
 
class Dog(Animal):
    """狗类"""
 
    def func(self):
        print('汪 汪 汪 ')
 
 
class Hero:
    def func(self):
        print('这个是英雄类的方法,不是动物类的对象')
 
 
def work01(musen: Animal):
    musen.func()
 
 
work01(Cat())
work01(Dog())
work01(Hero())

多态的意义:

  • 对于一个变量,我们只需要知道他是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法(调用方只管调用,不管细节)
  • 当需要新增功能,只需要新增一个Animal的子类实现run()方法,就可以在原来的基础上进行功能扩展,这就是著名的“开放封闭”原则:

类具有继承关系,并且子类类型可以向上转型看做父类类型,如果从 Person 派生出 Student 和 Teacher ,并都写了一个 who() 方法:

 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
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def who(self):
        return 'I am a Person, my name is %s' % self.name

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score
    def who(self):
        return 'I am a Student, my name is %s' % self.name

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course
    def who(self):
        return 'I am a Teacher, my name is %s' % self.name

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

print(p.who())  # I am a Person, my name is Tim
print(s.who())  # I am a Student, my name is Bob
print(t.who())  # I am a Teacher, my name is Alice

这种行为称为多态。从定义上来讲,Student和Teacher都拥有来自父类Person继承的who()方法,以及自己定义的who()方法。但是在实际调用的时候,会首先查找自身的定义,如果自身有定义,则优先使用自己定义的函数;如果没有定义,则顺着继承链向上找。

四、Python中类的特殊方法

4.1 什么是特殊方法

Python 中的 特殊方法 是指 以双下划线开头、且双下划线结尾、每个Python对象都拥有的方法。

常见特殊方法:

1
2
3
4
5
__str__(), __add__(), __sub__(), __mul__(), __truediv__(),
__len__(), __new__(), __init__(), __del__(), __repr__(),
__bytes__(), __format__(), __lt__(), __le__(), __eq__(),
__ne__(), __gt__(), __ge__(), __hash__(), __bool__(), 
__dir__(), __set__(), __call__(), __slots__(), ...

更全面的特殊方法列表 及功能介绍 可以查看官方文档 Python 特殊方法: https://docs.python.org/3/reference/datamodel.html#special-method-names

4.2 Python类的 __str__()__repr__() 方法

对于Python的内建对象,比如int、dict、list等,通过str()方法,可以把这些对象转换为字符串对象输出。

1
2
3
4
5
6
num = 12
str(num) # ==> '12'
d = {1: 1, 2: 2}
str(d) # ==> '{1: 1, 2: 2}'
l = [1,2,3,4,5]
str(l) # ==> '[1, 2, 3, 4, 5]'

对于自定义数据类型的对象,通过str()方法,同样可以得到对象所对应的字符串结果,只不过结果会有些难理解。

1
2
3
4
5
class Person:
    pass

bob = Person()
str(bob) # ==> '<__main__.Person object at 0x7fc77b859c50>'

<main.Person object at 0x7fc77b859c50>这个结果其实是 Person 的实例 bob 在内存中的地址,这是相当难以理解的,不过引发思考的是,通过str()打印的数据,是怎么来的呢?

这其实是对象的内建方法 __str__ 返回的。 通过 dir() 方法,我们可以把对象的所有方法打印出来。

1
2
>>> dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

可以看到,int、dict、list 等的内建对象都实现了自己的 __str__() 方法,可以把相应的字符串返回,如果我们的类也想把容易理解的字符串输出的话,那么我们也需要实现类的 __str__() 方法。

1
2
3
4
5
6
7
8
9
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)

bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'

但是,对于直接在终端输入变量bob,得到的依然是这样结果。

1
2
>>> bob
<__main__.Person object at 0x7fc77b859cc0>

而对于int、list等的对象,直接输入变量也可得到可读的结果。

1
2
3
4
5
6
>>> num = 12
>>> str(num)
'12'
>>> d = {1: 1, 2: 2}
>>> d
{1: 1, 2: 2}

__str__() 函数似乎没有在自定义类Person中生效,这是为什么呢?

其实,这是因为 Python 定义了 __str__()__repr__() 两种方法,__str__() 用于显示给用户,而 __repr__() 用于显示给开发人员,当使用 str() 时,实际调用的是 __str__() 方法,而直接输入变量,调用的是 __repr__() 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)
    def __repr__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)

bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'

>>> bob
'name: Bob, gender: Male'

4.3 类的 __len__() 方法

对于列表List或者元组Tuple,通过内建方法len(),可以得出列表或者元组中元素的个数。如果一个类表现得像一个list,想使用len()函数来获取元素个数时,则需要实现len()方法。

比如我们实现一个班级 Class 的类,初始化把班级的同学名字列表传进去,希望 len() 函数可以返回班级同学的数量时,可以这样实现:

1
2
3
4
5
6
7
8
9
class Class:
    def __init__(self, students):
        self.students = students
    def __len__(self):
        return len(self.students)

students = ['Alice', 'Bob', 'Candy']
classA = Class(students)
len(classA) # ==> 3

通过自定义__len__()方法,可以让len()函数返回相关的结果,如果没有定义__len__()方法的类使用len()函数获取长度时,将会引起异常。

1
2
3
4
5
6
7
8
9
class Class:
    def __init__(self, students):
        self.students = students

classB = Class(students)
len(classB)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: object of type 'Class' has no len()

4.4 类的数学运算

事实上,Python很多的操作都是通过内建函数来实现的,比如最熟悉的加减乘除,都是通过内建函数来实现的,分别是 __add__()__sub__()__mul__()__truediv__()。因此,只要我们的自定义类实现了相关的内建函数,我们的类对象,也可以做到加减乘除。

对于有理数,我们可以使用Rational类来表示:

1
2
3
4
class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q

其中,p、q 都是整数,表示有理数 p/q。 如果要让Rational进行加法运算,需要正确实现 __add__()

1
2
3
4
5
6
7
8
class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q
    def __add__(self, r):
        return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
    def __str__(self):
        return '{}/{}'.format(self.p, self.q)

定义好__add__()方法后,就可以尝试一下有理数的加法了:

1
2
3
4
>>> r1 = Rational(1, 2)
>>> r2 = Rational(2, 3)
>>> print(r1 + r2)
7/6

需要注意 __add__() 函数,它有一个参数,表示的是运算的第二个操作数,比如:r1 + r2,那么在add()方法中的参数,r指的就是r2,这个参数是运算符重载的时候传递的。

另外,细心的同学可能注意到了,相比加减乘的特殊方法,除法的特殊方法名字较长 __truediv__(),并且含有true这样的描述,这其实和Python除法是有关系的。

Python的除法可以分为地板除(你没看错,就是地板)和普通除法,地板除的特殊方法是 __floordiv__(),普通除法是 __truediv__()

地板除法和普通除法不一样,地板除法的结果只会向下取整数。

1
2
3
4
5
6
7
8
>>> num = 5
>>> num.__truediv__(3)
1.6666666666666667
>>> num.__floordiv__(3)
1 # 向下取整
>>> num = 7
>>> num.__floordiv__(3)
2

在运算中,普通除法使用 / 表示,而地板除使用 // 表示。

4.5 类的__slots__()方法

由于Python是动态语言,任何实例在运行期都可以动态地添加属性。比如:

1
2
3
4
5
class Student(object):
    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score

此时,Student类有三个属性,name、gender、score,由于是动态语言,在运行时,可以随意添加属性。

1
2
student = Student('Bob', 'Male', 99)
student.age = 12 # ==> 动态添加年龄age属性

如果要限制添加的属性,例如,Student类只允许添加 name、gender和score 这3个属性,就可以利用Python的一个特殊的 __slots__ 来实现。

1
2
3
4
5
6
class Student(object):
    __slots__ = ('name', 'gender', 'score')
    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score

使用__slots__ = (’name’, ‘gender’, ‘score’)限定Student类的属性,这个时候在外部再次添加动态属性age,将会报错。

1
2
3
4
student = Student('Bob', 'Male', 99)
>>> student.age = 12 # ==> 动态添加年龄age属性
Traceback (most recent call last):
AttributeError: 'Student' object has no attribute 'age'

__slots__ 的目的是限制当前类的实例对象所能拥有的属性,避免因为外部属性的操作导致类属性越来越难以管理。

假设Person类通过__slots__定义了name和gender,请在派生类Student中通过__slots__继续添加score的定义,使Student类可以实现name、gender和score 3个属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person(object):

    __slots__ = ('name', 'gender')

    def __init__(self, name, gender):
        self.name = name
        self.gender = gender


class Student(Person):

    __slots__ = ('score',)

    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score


s = Student('Bob', 'male', 59)
s.name = 'Tim'
s.score = 99
print(s.score)

4.6 类的__call__()方法

在Python中,函数其实是一个对象,我们可以将一个函数赋值给一个变量,而不改变函数的功能。

1
2
3
4
5
6
7
8
9
>>> f = abs
>>> f
<built-in function abs>
>>> abs
<built-in function abs>
>>> f.__name__
'abs'
>>> f(-123)
123

把内建函数 abs() 赋值给变量 f 之后,可以看到 f 就和 abs 一样。

由于 f 可以被调用,所以,f 被称为可调用对象,而事实上,所有的函数都是可调用对象。

如果把一个类实例也变成一个可调用对象,可以实现一个特殊的方法 __call__()

例如,我们把Person类变成一个可调用对象:

1
2
3
4
5
6
7
8
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __call__(self, friend):
        print('My name is {}...'.format(self.name))
        print('My friend is {}...'.format(friend))

接着我们初始化一个Person对象,并对这个对象通过函数的方式调用:

1
2
3
4
>>> p = Person('Bob', 'Male')
>>> p('Alice') # ==> 用函数的方式调用Person类的实例p
My name is Bob...
My friend is Alice...

4.7 类的__getattr__()方法

4.8 类的__getattribute__()方法

Licensed under CC BY-NC-SA 4.0