本文将主要介绍 Python 面向对象,包括类定义、各类属性、各类方法、继承、多态、封装、单例模式等。阅读本文预计需要 30 min


一文了解Python面向对象

  • 1. 前言
  • 2. 面向对象 VS 面向过程
  • 3. 初窥类
  • 3.1 类的定义
  • 3.2 类对象和实例对象
  • 4. 属性
  • 4.1 类属性和实例属性
  • 4.2 公有属性、半私有属性和私有属性
  • 4.3 魔法属性
  • 4.3.1 `__slots__` 魔法属性
  • 5. 方法
  • 5.1 类方法、实例方法和静态方法
  • 5.2 公有方法、半私有方法和私有方法
  • 5.3 魔法方法
  • 5.3.1 `__init__()` 方法
  • 5.3.2 `__new__()` 方法
  • 5.3.3 `__str__()` 方法
  • 5.3.4 `__del__()` 方法
  • 5.3.5 `__iter__()` 和 `__next__()` 方法
  • 5.4 @property
  • 6. 继承
  • 6.1 单继承
  • 6.2 多继承
  • 6.3 多层继承
  • 6.4 方法解析顺序(MRO)
  • 6.5 `isinstance()` 和 `issubclass` 函数
  • 6.6 子类重写父类同名属性和方法
  • 6.7 子类调用父类同名属性和方法
  • 6.8 super()的使用
  • 7. 封装
  • 8. 多态
  • 9. 单例模式
  • 10. 小结
  • 11. 巨人的肩膀


1. 前言

Python 是一门高级语言,掌握类的知识是我们使用 Python 进行面向对象编程非常重要的一步。这篇文章主要是结合官方文档类一节的教程,总结面向对象 VS 面向过程、类的创建、属性、方法、继承、多态、封装等方面的知识,并尽可能的详细,当然有些地方由于本人水平有限,不够完善,欢迎各位读者补充、指正。接下来就开始进入正文学习。

  • 类对象、实例对象
  • 类属性、实例属性、半私有属性、公有属性、私有属性、魔法属性(__slots____doc__)
  • 类方法、实例方法、私有方法、公有方法、半公有方法、私有方法、魔法方法(__init__()/__new__()/__del__()/__str__()等)
  • 继承、单继承、多继承、装饰器(@property@classmethod@staticmethod)
  • 单例模式
  • 方法解析顺序(MRO)
  • 多态
  • 封装

2. 面向对象 VS 面向过程

先让我们了解一下什么是面向对象和面向过程。
面向对象编程(Object oriented Programming, OOP):是一种程序设计思想,它把一个个对象作为基本单元,其中对象是由若干数据和若干个操作数据的函数组成的集合体。面向对象把计算机程序看作一组对象的集合,对象之间可以互相接收信息,并对信息进行处理,而计算机的执行过程就是一系列消息在各个对象之间传递。

面向过程编程(Procedure oriented programming, POP):是把计算机程序视为一系列的命令集合,即一组函数的顺序执行。

下面通过【处理学生成绩】这个例子来说明面向过程和面向对象的不同

面向过程的做法是考虑如何一步步执行流程,从而得到结果。

student1 = {'name': 'Jock', 'score': 100}
student2 = {'name': 'Kobe', 'score': 98}

def print_score(student):
    """打印学生成绩信息"""
    print(f"{student['name']}: {student['score']}")

print_score(student1)
print_score(student2)

结果输出:
Jock: 100
Kobe: 98

面向对象的做法是先考虑将 Student 作为一个对象(object),这个对象有 namescore 两个属性(attribute),然后还有一个“行为”,即 print_score 方法(method),在实例对象中的函数,一般我们称之为方法。具体做法如下:

class Student(object):
    """自定义一个 Student 对象"""

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

    def print_score(self):
        print(f"{self.name}: {self.score}")


Jock = Student('Jock', 100)
Kobe = Student('Kobe', 99)
Jock.print_score()
Kobe.print_score()

结果输出:
Jock: 100
Kobe: 99

两种方法各有优劣,不过面向对象的编程相对更加接近我们人类社会的习惯。
面向对象的三大特点是:

  • 封装
  • 继承
  • 多态

上面的代码还不明白?不要紧,下面我们都会详细讲解。

3. 初窥类

计算机发展本身是一个不断抽象的过程,最终实现让计算机如同人一般思考,为人类服务。是对对象的进一步抽象,类是具有相似内部状态和运动规律的实体的集合,或者说具有相同属性和行为事务的统称。自然界中有类(Class)和实例(Instance),Class 是抽象的概念,比如:我们刚刚定义的 Student,是指学生这个概念,这跟我们初中学的生物分类“界门纲目科属种”差不多,Student 是类,Jock 和 Kobe 是 Student 这个类的实例(Instance)。
Python 中的类是 C++ 和 Modula-3 两种语言中类机制的结合。

3.1 类的定义

最简单的类定义是这样的:

class ClassName:  # 经典类(旧式类) 定义形式
    <statement-1>
    ...
    <statement-N>


class ClassName(object):  # 新式类定义形式
    <statement-1>
    ...
    <statement-N>

这样我们就完成了类定义。有几点需要说明一下:

  • class 是定义类的关键字,如 def 是定义函数的关键字。类名的命名规则按照大驼峰命名法,即所有单词首字母大写。
  • 定义类有两种形式:新式类和旧式类,两种方式都可以,目前个人倾向于新式类。其中object 是 Python 中所有类的最顶级父类,继承的时候会再讲。
  • 类定义的时候,会创建一个命名空间,作为局部作用域。类中的变量和函数都会放到这个命名空间。
  • 当完成类定义时,就会创建一个类对象(class object)。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器。原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称。

掌握怎么定义类,以及相应的命名约定。后面两点暂时不太明白也没关系,这里我翻译的不是很好,以后大家可以再看看英文文档,慢慢理解。接下来我们学习完成定义类之后可以做什么?

3.2 类对象和实例对象

类对象(class object)支持两种操作:属性引用(attribute references)实例化(instantiation)

属性引用的语法是:obj.name。有效的属性名称是类对象被创建时存在于类命名空间中的所有名称(所有的变量名和函数名)。不过,通常我们把类中的变量当做属性(attribute),把类中定义的函数当做方法(method)。函数和方法在很多中文教程中基本不加区分,大家能明白它们的意思就行。

类的实例化语法是:ClassName(),可以发现类的实例化使用了函数表示法。 把类对象当做一个不带参数的函数,这个函数返回的是该类的一个新实例。

举个栗子:

class Hero(object):  # 新式类定义形式
    """A simple example class"""
    game = "王者荣耀"

    def move(self):
        """实例方法"""
        print("全军出击!")


# Hero.game是类数据属性引用
print(Hero.game)  # 打印Hero.game

# Hero.move类函数属性引用
print(type(Hero.move))  # 打印Hero.move的类型,结果:function

# 类实例化,创建实例对象 hero
hero = Hero()

# 类的文档说明
print(Hero.__doc__)

print(hero.game)  # 打印实例对象hero的game属性
print(type(hero.move))  # 打印hero.move的类型,结果:method
hero.move()  # 调用实例对象hero的方法move()

结果输出:
王者荣耀
<class 'function'>
A simple example class
王者荣耀
<class 'method'>
全军出击!

说明:

  • “”“A simple example class”"" 是类的文档字符串,和函数文档字符串一样,可以通过__doc__这个魔法属性获取。
  • game 是类 Hero 的属性,move 是类 Hero 的函数。注意这里我们不能通过 Hero.move()的形式调用 move 函数。
  • Hero.nameHero.move 都是有效的属性引用,将分别返回一个字符串和一个函数对象。
  • hero = Hero() 创建类 Hero 的实例对象并赋值给局部变量 hero。hero 称为实例对象。
  • 实例对象只有一种操作:属性引用(attribute reference)。包括数据属性(data attribute)和方法(method)。
  • 数据属性:对应于 Smalltalk 中的实例变量或者 C++中的数据成员,Python 实例对象数据属性不需要声明,和局部变量一样,它们在第一次赋值时产生。
  • 方法:实例对象中的函数称为方法。在上面的例子中,hero.move 是有效的方法引用,因为 Hero.move 是一个函数,但 hero.game 不是方法,因为 Hero.name 不是一个函数。 但是 hero.move 与 Hero.move 并不是一回事,hero.move 是一个方法对象,Hero.move 是函数对象。简单理解就是类对象定义里面函数叫函数,实例对象里面的函数叫做方法。

看到这里我们梳理一下对象、类对象、实例对象三者的关系和区别:

  • 对象(object):在 Python 中一切皆对象。类对象、实例对象都属于对象,函数、列表、元组等等也都可以当做对象。所以当别人说对象的时候,你最好反应一下是哪种对象?
  • 类对象(class object):用关键字 class 定义类,当完成类定义时就会创建一个类对象,并且把类名绑定到这个类对象。上面这个例子中 Hero 就是一个类对象。
  • 实例对象(instance object):类对象实例化生成的对象,是一个具体的存在。如 hero 就是类 Hero 的一个实例,Jock = Hero(),则 Jock 是另一个实例。类对象可以被当做一个模板,可以用它生成出多个实例对象。

4. 属性

是对对象的进一步抽象,是具有相同属性和行为事务的统称。类的属性根据不同的分类方式可分为不同的类别。

  • 根据是否被每个实例所共享分类,可以把属性分为类属性实例属性
  • 根据访问权限分,可以分为共有属性半私有属性私有属性

4.1 类属性和实例属性

一般来说,实例属性是每个实例对象单独拥有的属性,而类属性是类对象所拥有的属性,也是该类所有实例共享的属性,在内存中只存在一个副本,这个和 C++中类的静态成员变量有点类似。这有点像,实例属性是个性,类属性是共性。

举个栗子:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 类属性,所有实例对象共有

    def __init__(self, name):
        self.name = name  # 实例属性,每个实例都单独有一个


zhuang_zhou = Hero('庄周')
hou_yi = Hero('后裔')
print(f"zhuang_zhou.game是{zhuang_zhou.game},hou_yi.name是{hou_yi.game}")
print(f"zhuang_zhou.game是的内存地址是{id(zhuang_zhou.game)},hou_yi.game的内存地址是{id(hou_yi.game)}")
print(f"zhuang_zhou.name是{zhuang_zhou.name},hou_yi.name是{hou_yi.name}")
print(f"zhuang_zhou.name是的内存地址是{id(zhuang_zhou.name)},hou_yi.name的内存地址是{id(hou_yi.name)}")

结果输出:
zhuang_zhou.game是王者荣耀,hou_yi.name是王者荣耀
zhuang_zhou.game是的内存地址是1989739646864,hou_yi.game的内存地址是1989739646864
zhuang_zhou.name是庄周,hou_yi.name是后裔
zhuang_zhou.name是的内存地址是1989738801072,hou_yi.name的内存地址是19897385531368

可以看到,类属性 game 被所有实例对象(zhuang_zhou 和 hou_yi)共享,zhuang_zhou.game 和 hou_yi.game 指向同一个内存地址。类属性是各个实例的共性!
self.name 是实例属性,每个实例之间不共享实例属性。实例属性是各个实例的个性!

实例属性和类属性怎么区分呢?

实际上 self 代表的就是实例对象本身,以在函数内以 self.xxx = value 形式赋值的就是实例属性,在函数外以 xxx = value 形式赋值的就是类属性。

但是在函数外,以self.xxx = value 形式赋值会报错,同时在函数内以 xxx = value 形式赋值并不会得到相应的类属性,也不会得到一个实例属性。

举例如下:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 类属性,所有实例对象共有
    # self.hp = 6666  # NameError: name 'self' is not defined
    def __init__(self, name):
        self.name = name  # 实例属性,每个实例都单独有一个
        age = 25  # 这既不是类属性,也不是实例属性


zhuang_zhou = Hero('庄周')
print(zhuang_zhou.age)

输出结果:
AttributeError: 'Hero' object has no attribute 'age'

所以我们要设置实例属性一定要在函数里面以 self.xxx = value 的形式赋值。self 就代表实例。
age = 25 在函数里面,所以它不是一个类属性,它也不是一个实例属性,因为它前面没有 self.,没有这个前缀,这个变量就无法绑定到实例对象上。

注意,共享数据在涉及可变对象时会引发意想不到的结果。所以类属性不要设置为可变对象,如列表,字典等,这个和函数里面默认参数值不要设置为可变类型一样。

如下面代码中的 tricks 列表不应该作为类变量,因为所有的 Dog 实例将共享这唯一一个单独的列表。下面是官网的例子:

class Dog(object):

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name  # 实例属性

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的做法如下:

class Dog(object):

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

我们如何访问类属性和实例属性呢?
答案是可以通过类引用或者实例引用直接访问相应的属性。
举个栗子:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 类属性,所有实例对象共有

    def __init__(self, name):
        self.name = name  # 实例属性,每个实例都单独有一个


zhuang_zhou = Hero('庄周')  # 创建实例化对象
print(f"通过类引用访问类属性game:{Hero.game},id是:{id(Hero.game)}")
print(f"通过实例引用访问类属性game:{zhuang_zhou.game},id是:{id(zhuang_zhou.game)}")
print(f"通过实例引用访问实例属性name:{zhuang_zhou.name},id是:{id(zhuang_zhou.name)}")
# print(f"通过类引用访问实例属性name:{Hero.name},id是:{id(Hero.name)}")  # AttributeError: type object 'Hero' has no attribute 'name'

结果输出:
通过类引用访问类属性game:王者荣耀,id是:2403988901776
通过实例引用访问类属性game:王者荣耀,id是:2403988901776
通过实例引用访问实例属性name:庄周,id是:2403986759472

从上面可以发现,类属性可以被类对象和实例对象访问,而实例属性只能被实例对象访问,不能通过类对象访问。

那么我们如何修改类属性和实例属性呢?
方法如下:

  • 修改类属性:必须通过类对象的引用来修改类属性。
  • 实例属性:通过实例对象的引用来修改实例属性。
class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 类属性,所有实例对象共有

    def __init__(self, name):
        self.name = name  # 实例属性,每个实例都单独有一个

zhuang_zhou = Hero('庄周')  # 创建实例化对象
print(f"通过类引用修改前,类属性game为:{Hero.game}")
print(f"通过实例对象访问修改前类属性game,类属性game为:{zhuang_zhou.game}")
Hero.game = "刺激战场"
print(f"通过类引用修改后,类属性game为:{Hero.game}")
print(f"通过实例对象访问修改后类属性game,类属性game为:{zhuang_zhou.game}")

输出结果:
通过类引用修改前,类属性game为:王者荣耀
通过实例对象访问修改前类属性game,类属性game为:王者荣耀
通过类引用修改后,类属性game为:刺激战场
通过实例对象访问修改后类属性game,类属性game为:刺激战场

我们可以发现,通过类引用修改了类属性,会导致所有的实例对象类属性都会改变,因为类属性是所有实例对象共享的。
那么能不能通过实例引用来修改类属性呢?
答案是不能。
测试如下:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 类属性,所有实例对象共有

    def __init__(self, name):
        self.name = name  # 实例属性,每个实例都单独有一个


zhuang_zhou = Hero('庄周')  # 创建实例化对象
hou_yi = Hero('后裔')  # 创建实例化对象
print(f"通过实例引用修改类属性前,类属性game为:{Hero.game}")
print(f"通过zhuang_zhou引用修改类属性前,类属性game为:{zhuang_zhou.game}")
print(f"通过hou_yi实例访问修改前类属性,类属性game为:{hou_yi.game}")
print("-" * 20)
# 这会为zhuang_zhou这个实例对象创建一个实例属性,而不会修改类属性
zhuang_zhou.game = "刺激战场"
print(f"通过实例引用修改类属性后,类属性game为:{Hero.game}")
print(f"通过zhuang_zhou引用尝试修改类属性后,类属性game为(实际上这里访问的是实例属性game):{zhuang_zhou.game}")
print(f"通过hou_yi实例访问修改后类属性,类属性game为:{hou_yi.game}")
print("-" * 20)
del zhuang_zhou.game  # 删除实例对象zhuang_zhou的实例属性game,但类属性game依然存在
print(f"通过类对象访问类属性,类属性game为:{Hero.game}")
print(f"删除zhuang_zhou实例属性game后,类属性game为:{zhuang_zhou.game}")
print(f"通过hou_yi实例访问类属性,类属性game为:{hou_yi.game}")

输出结果:
通过实例引用修改类属性前,类属性game为:王者荣耀
通过zhuang_zhou引用修改类属性前,类属性game为:王者荣耀
通过hou_yi实例访问修改前类属性,类属性game为:王者荣耀
--------------------
通过实例引用修改类属性后,类属性game为:王者荣耀
通过zhuang_zhou引用尝试修改类属性后,类属性game为(实际上这里访问的是实例属性game):刺激战场
通过hou_yi实例访问修改后类属性,类属性game为:王者荣耀
--------------------
通过类对象访问类属性,类属性game为:王者荣耀
删除zhuang_zhou实例属性game后,类属性game为:王者荣耀
通过hou_yi实例访问类属性,类属性game为:王者荣耀

可以看到,并不能通过实例对象修改类属性,尝试使用实例对象修改类属性,只会在该实例对象中创建一个新的实例属性,原有的类属性依然存在。当实例属性和类属性同名时(产生冲突),优先访问的是实例属性,屏蔽掉类属性。

小结:

  1. 类属性不能设置为可变类型。
  2. 实例属性以在函数内 self.xxx = value 的形式定义,类对象在函数外以 xxx = value 的形式定义。
  3. 类属性可以被类对象和实例对象访问,而实例属性只能被实例对象访问,不能通过类对象访问。
  4. 通过类引用修改了类属性,会导致所有的实例对象类属性都会改变,因为类属性是所有实例对象共享的。
  5. 当实例属性和类属性同名时(产生冲突),优先访问的是实例属性,屏蔽掉类属性。
  6. 实例对象修改类属性,尝试使用实例对象修改类属性,只会在该实例对象中创建一个新的实例属性,原有的类属性依然存在。因此,在类外修改类属性,必须通过类引用。

4.2 公有属性、半私有属性和私有属性

公有、半私有、私有的区别在于能否在类外被访问。

  • 公有属性:在类外能够被调用者访问或者修改的属性,包括公有类属性和公有实例属性。通常形式是 xxx = value 或者 self.xxx = value
  • 半私有属性:其实属于私有属性,这里只是多做了个区分。尽管在类外能够被调用者访问或者修改,包括半私有类属性和半私有实例属性,但是一般不建议调用者访问或者修改,因为这些半私有属性通常属于开发者测试阶段,可能会后期被删除。通常形式是 _xxx = value 或者 self._xxx = value。大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
  • 私有属性:在类外不能够被调用者访问或者修改的属性,但是可以在本类内部访问,包括私有类属性和私有实例属性。通常形式是 __xxx = value 或者 self.__xxx = value。类的私有属性,都不会被子类继承,子类也无法访问。私有属性往往来处理类的内部事情,不通过对象处理,起到安全作用。

举栗子:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 公有类属性,可以在类外访问
    _version = 2020.05  # 半私有类属性,可以在类外访问
    __money = 6666  # 私有类属性,类外部无法访问

    def __init__(self, name):
        self.name = name  # 公有实例属性,可以在类外访问
        self._sex = "男"  # 半私有实例属性,可以在类外访问
        self.__age = 4  # 私有实例属性,无法在类外访问


zhuang_zhou = Hero('庄周')  # 创建实例化对象
print(Hero.game)  # 王者荣耀
print(Hero._version)  # 2020.05
# print(Hero.__money)  # AttributeError: type object 'Hero' has no attribute '__money'
print(zhuang_zhou.game)  # 王者荣耀
print(zhuang_zhou._version)  # 2020.05
# print(zhuang_zhou.__money)  # AttributeError: 'Hero' object has no attribute '__money'
print(zhuang_zhou.name)  # 庄周
print(zhuang_zhou._sex)  # 男
# print(zhuang_zhou.__age)  # AttributeError: 'Hero' object has no attribute '__age'

从上面可以看到公有和半私有属性都是可以访问的,但是私有的属性都是无法访问。

实际上,私有的也是可以访问的,那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为当前类名称,不是实例对象的名字。这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。

所以私有属性实际上也是可以访问的:

class Hero(object):  # 新式类定义形式
    game = "王者荣耀"  # 公有类属性,可以在类外访问
    _version = 2020.05  # 半私有类属性,可以在类外访问
    __money = 6666  # 私有类属性,类外部无法访问

    def __init__(self, name):
        self.name = name  # 公有实例属性,可以在类外访问
        self._sex = "男"  # 半私有实例属性,可以在类外访问
        self.__age = 4  # 私有实例属性,无法在类外访问


zhuang_zhou = Hero('庄周')  # 创建实例化对象
print(Hero._Hero__money)
print(zhuang_zhou._Hero__money)
print(zhuang_zhou._Hero__age)


结果输出:
6666
6666
4

从上面我们可以看到,私有属性其实也是可以访问到的,但是一般我们不这么干。除非在测试中,我们可以这么访问私有属性。

名称改写有助于让子类重载方法而不破坏类内方法调用。以下是官网的例子,例如:

class Mapping(object):
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

上面的示例即使在 MappingSubclass 引入了一个 __update 标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update。这里涉及到继承,暂时弄不懂也没关系,可以先放着,以后再看。

请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。

如果需要修改一个对象的属性值,通常有两种方法:

  1. 直接修改:对象名.属性名 = 数据
  2. 间接修改:对象名.方法名()

私有属性不能直接访问,所以无法通过第一种方式修改,一般通过第二种方式修改私有属性的值:定义一个公有方法 set(),在这个公有方法内修改私有属性,再定义一个公有方法 get()访问这个私有属性。

举个栗子:

class Person(object):
    def __init__(self):
        self.name = "Jock"  # 公有实例属性
        self.__age = 25  # 私有实例属性,可以在类内部通过self调用,但不能通过对象访问

    # 一般会定义 get_xxx()方法和set_XXX()方法来获取和修改私有属性
    def get_age(self):
        """返回私有属性值"""
        return self.__age

    def set_age(self, age):
        """接收参数修改私有属性"""
        self.__age = age


Jock = Person()
# print(Jock.__age)  # 实例对象不能访问私有权限的属性和方法

# 可以通过访问公有方法set_age()来修改私有属性的值
Jock.set_age(18)

# 可以通过公有方法get_age()来获取公有属性的值
print(Jock.get_age())

结果输出:
18

小结:

  1. 公有属性:在类外能够被调用者访问或者修改的属性,包括公有类属性和公有实例属性。通常形式是 xxx = value 或者 self.xxx = value
  2. 半私有属性:在类外能够被调用者访问或者修改的属性,包括半私有类属性和半私有实例属性,但是一般不建议调用者访问或者修改,因为这些半私有属性通常属于开发者测试阶段,可能会后期被删除。通常形式是 _xxx = value 或者 self._xxx = value。大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
  3. 私有属性:在类外不能够被调用者访问或者修改的属性,但是可以在本类内部访问,包括私有类属性和私有实例属性。通常形式是 __xxx = value 或者 self.__xxx = value
  4. 类的私有属性,都不会被子类继承,子类也无法访问。私有属性往往来处理类的内部事情,不通过对象处理,起到安全作用。
  5. 私有属性一般通过 get()和 set()方法进行访问和修改。
  6. 私有属性可以非常访问,通过 obj._ClassName__xxx 来实现访问。

4.3 魔法属性

在 Python 中有一些属性是以双下划线开头,和双下划线结尾的,类似于 __xxx__这样的属性,具有特殊功能,我们称为魔法属性。这部分建议看了继承部分后再回来看。

4.3.1 __slots__ 魔法属性

__slots__:可以用来限制类能添加的属性。

In [1]: class Student(object):
   ...:     __slots__ = ('name', 'age')  # 用tuple定义允许绑定的属性名称
   ...:

In [2]: s = Student()  # 创建实例对象

IIn [3]: s.name = 'Jock'  # 绑定属'name'

IIn [4]: s.age = 25  # 绑定属'age'

In [6]: s.score = 100  # 绑定属'score'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-312c53518669> in <module>
----> 1 s.score = 100  # 绑定属'score'

AttributeError: 'Student' object has no attribute 'score'

由于 score 没有被放到 __slots__ 中,所以不能绑定 score 属性,试图绑定 score 将得到 AttributeError 的错误。

使用 __slots__ 要注意,__slots__定义的属性仅对当前实例起作用,对继承的子类是不起作用的。

In [7]: class GraduateStudent(Student):
   ...:     pass
   ...:

In [8]: g = GraduateStudent()

In [9]: g.score = 100  # 可以绑定上去

除非子类中也定义 __slots__,这样子类实例允许定义的属性就是自身的 __slots__加上父类的__slots__

In [12]: class UndergraduateStudent(Student):
    ...:     __slots__ = ('score', 'gender')
    ...:
    ...:

In [13]: f = UndergraduateStudent()

In [14]: f.name = 'Bob'

In [15]: f.age = 18

In [16]: f.score = 66

In [18]: f.gender = 'male'

In [19]: f.city = '武汉'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-19-94672d8905ba> in <module>
----> 1 f.city = '武汉'

AttributeError: 'UndergraduateStudent' object has no attribute 'city'

__slots__ 小结:

  1. 通过在类定义中,用 __slots__ = ('attr1', 'attr1', ...) 来限制该类能添加的属性。如果添加('attr1', 'attr1', ...)外的属性将得到AttributeError的错误。
  2. __slots__ 只作用于本类的实例对象,对于继承类(子类)不起限制作用。
  3. 如果子类中也定义 __slots__,那么子类实例允许定义的属性就是自身的 __slots__加上父类的__slots__

5. 方法

方法按照类似于属性的分标准,可以分为类方法、实例方法、静态方法以及公有方法、半私有方法和私有方法。

5.1 类方法、实例方法和静态方法

类方法:需要用装饰器 @classmethod 标识,对于类方法,第一个参数必须是类对象,一般以cls作为第一个参数(当然也可以用其他的名称的变量,但是大部分人都习惯以cls作为第一个参数的名字,默认就好),能够通过实例对象和类对象去访问。类方法可以直接类名.方法名直接调用,也可以创建实例调用,类方法不能访问实例属性。
静态方法:需要用装饰器 @staticmethod 标识,对第一个参数没有要求,和普通函数一样。静态方法可以直接类名.方法名直接调用,也可以创建实例调用,静态方法不能访问实例属性。
实例方法:第一个参数必须为 self,代表实例对象,所以实例方法必须要创建实例才能调用。

举个栗子:

class Person(object):

    def instance_method(self):
        print(f"我是类{Person}的实例方法,只能被实例对象调用")

    @staticmethod
    def static_method():
        print("我是静态方法")

    @classmethod
    def class_method(cls):
        print("我是类方法")


Person.static_method()
Person.class_method()
# Person.instance_method()  # TypeError: instance_method() missing 1 required positional argument: 'self'
Person.instance_method(Person)
print('----------------')
p = Person()
p.instance_method()
p.static_method()
p.class_method()


结果输出:
我是静态方法
我是类方法
我是类<class '__main__.Person'>的实例方法,只能被实例对象调用
----------------
我是类<class '__main__.Person'>的实例方法,只能被实例对象调用
我是静态方法
我是类方法

下面我们看一下,如何在三个方法中调用类属性和实例属性,以及互相调用。

class Person(object):
    country = "中国"  # 公有类属性

    def __init__(self):
        self.name = "Jock"  # 公有实例属性

    def instance_method(self):
        # print(f"在实例方法中通过self.类属性方式调用类属性{country}")  # NameError: name 'country' is not defined
        print(f"在实例方法中通过self.类属性方式调用类属性{self.country}")  # 实例方法中调用类属性
        # print(f"在实例方法中通过self.实例属性方式调用实例属性{name}")  # NameError: name 'name' is not defined
        print(f"在实例方法中通过self.实例属性方式调用实例属性{self.name}")  # 实例方法中调用实例属性
        self.static_method()  # 实例方法中调用静态方法
        self.class_method()  # 实例方法中调用类方法

    @staticmethod
    def static_method():
        # print(f"在静态方法中调用类属性{country}")  # NameError: name 'country' is not defined
        print(f"在静态方法中通过类名.类属性方式调用类属性{Person.country}")

    @classmethod
    def class_method(cls):
        # print(f"在类方法中通过cls.类属性方式调用类属性{country}")  # NameError: name 'country' is not defined
        print(f"在类方法中通过cls.类属性方式调用类属性{cls.country}")


p = Person()
print('----------------')
p.instance_method()
print('----------------')
p.static_method()
print('----------------')
p.class_method()
print('----------------')
Person.static_method()
print('----------------')
Person.class_method()
print('----------------')
# Person.instance_method()  # TypeError: instance_method() missing 1 required positional argument: 'self'
Person.instance_method(p)

结果输出:
----------------
在实例方法中通过self.类属性方式调用类属性中国
在实例方法中通过self.实例属性方式调用实例属性Jock
在静态方法中通过类名.类属性方式调用类属性中国
在类方法中通过cls.类属性方式调用类属性中国
----------------
在静态方法中通过类名.类属性方式调用类属性中国
----------------
在类方法中通过cls.类属性方式调用类属性中国
----------------
在静态方法中通过类名.类属性方式调用类属性中国
----------------
在类方法中通过cls.类属性方式调用类属性中国
----------------
在实例方法中通过self.类属性方式调用类属性中国
在实例方法中通过self.实例属性方式调用实例属性Jock
在静态方法中通过类名.类属性方式调用类属性中国
在类方法中通过cls.类属性方式调用类属性中国

可以发现,由于实例属性和实例方法是个性,所以静态方法和类方法是无法访问调用实例方法和实例属性的。在静态方法中要调用类方法和类属性,必须用 类名.类属性 或者 类名.类方法 的形式调用访问。在类方法中要调用类方法和类属性,用 cls.类属性 或者 cls.类方法 的形式调用访问,其中 cls 就代表类对象本身,所以 cls 也可以用类名来替换。在实例方法中调用类属性和类方法或者实例属性和实例方法,只要前面加上 self. 即可,self 代表实例对象本身。

类方法还有一个用途就是可以在实例对象中对类属性进行修改:

class Person(object):
    country = "中国"  # 类属性

    # 类方法,用 @classmethod进行装饰
    @classmethod
    def get_country(cls):
        return cls.country

    @classmethod
    def set_country(cls, country):
        cls.country = country


p = Person()
print(p.get_country())  # 可以通过实例对象引用
print(Person.get_country())  # 可以通过类访问

p.set_country("日本")
print(p.get_country())
print(Person.get_country())

结果输出:
中国
中国
日本
日本

实例对象调用类方法进行修改类属性后,通过类对象和实例对象访问的类属性都发生了改变。

小结:

  • 从定义形式上:类方法的第一个参数是类对象cls,实例方法的第一个参数是实例对象self,静态方法中不需要额外定义参数,和普通函数一样。
  • 类方法和实例方法不能调用实例属性和实例方法。实例方法全部属性和方法都可以调用。
  • 类方法通过 cls.xxx 形式调用类属性和类方法,静态方法通过 ClassName.xxx 形式调用类属性和类方法,实例方法通过 self.xxx 的形式代用所有属性和方法。
  • 如果实例对象调用类方法,那么产生的影响会作用于类对象及其所有的实例对象。即类对象可以通过调用类方法来修改类属性或者类方法。
  • 从定义形式上:类方法的第一个参数是类对象cls,那么通过cls引用的必定是类对象的属性和方法;实例方法的第一个参数是实例对象self,那么通过self引用的可能是类属性、也有可能是实例属性(这个需要具体分析)。静态方法中不需要额外定义参数,因此在静态方法中引用类属性的话,必须通过类对象来引用。

5.2 公有方法、半私有方法和私有方法

这部分的规则和公有属性、半私有属性和私有属性一样。
公有方法:可以在类外部访问调用。通常以 xxx() 形式定义。
半私有方法:其实也属于私有方法,尽管能够在类外部调用访问,但是一般不建议使用。通常以 _xxx() 形式定义。
私有方法:只能在类中调用访问,不能在类外调用访问。通常以 __xxx() 形式定义。

因为跟属性那部分类似,这里简单举个例子:

class Person(object):

    def print_name(self):
        print("我是公有方法!")
        print(f"在公有方法中访问半私有方法:{self._print_age()}")
        print(f"在公有方法中访问私有方法:{self.__print_sex()}")

    def _print_age(self):
        print("我是半私有方法!")

    def __print_sex(self):
        print("我是私有方法!")


p = Person()
print("-" * 20)
p.print_name()
print("-" * 20)
p._print_age()
print("-" * 20)
# p.__print_sex()  # AttributeError: 'Person' object has no attribute '__print_sex'
p._Person__print_sex()  # 非常规访问私有方法

结果输出:
--------------------
我是公有方法!
我是半私有方法!
在公有方法中访问半私有方法:None
我是私有方法!
在公有方法中访问私有方法:None
--------------------
我是半私有方法!
--------------------
我是私有方法!

从上面可以看到,私有方法也是可以通过非常规途径访问的。

小结:

  • 在属性名和方法名前面加上两个下划线 __ 为私有属性和私有方法,都不能通过在类外直接访问,类的私有属性和私有方法,都不会被子类继承,子类也无法访问;私有属性和私有方法往往来处理类的内部事情,不通过实例对象处理,起到安全作用。

5.3 魔法方法

有魔法属性自然就有魔法方法(magic method),Python 的类中,两个下划线开始,两个下划线结束的方法,就是魔法方法,魔法方法通常以 __xxx__() 的形式出现。它们具有特殊的一些功能和用途。

5.3.1 __init__() 方法

实例化操作(“调用”类对象)会创建一个空对象。但是我们想创建带有特定初始状态的自定义实例,拥有自己的个性怎么办呢?__init__()魔法方法就是干着事的。
__init__() :通常用来做属性初始化或赋值操作。创建带有特定初始状态的自定义实例。即定制化实例对象。如果为了能够完成自己想要的初始状态,可以自己定义 __init__() 方法。如果类没有写 __init__() 方法,Python 会自动创建,但是不执行任何操作。因此,一个类里无论自己是否编写 __init__() 方法,一定有 __init__() 方法。

class Hero(object):
    """定义一个英雄类"""
    def __init__(self, name, skill, hp, atk, armor):
        """__init() 方法用来做变量初始化 或 赋值操作,在类实例化对象的时候自动被调用"""
        self.name = name  # 姓名
        self.skill = skill  # 技能
        self.hp = hp  # 生命值
        self.atk = atk  # 攻击力
        self.armor = armor  # 护甲值

    def info(self):
        """在类的实例方法中,通过self获取该对象的属性"""
        print(f"英雄{self.name}的生命值:{self.hp}")
        print(f"英雄{self.name}的攻击力:{self.atk}")
        print(f"英雄{self.name}的护甲值:{self.armor}")


# 实例化英雄对象时,参数会传递到对象的 __init__() 方法里
zhuang_zhou = Hero("庄周", "化蝶", 6666, 1000, 900)
hou_yi = Hero("后裔", "炙热之风", 5000, 666, 999)

print("调用不同实例对象的方法")
zhuang_zhou.info()
print("-" * 20)
hou_yi.info()
print("-" * 20)

结果输出:
调用不同实例对象的方法
英雄庄周的生命值:6666
英雄庄周的攻击力:1000
英雄庄周的护甲值:900
--------------------
英雄后裔的生命值:5000
英雄后裔的攻击力:666
英雄后裔的护甲值:999
--------------------
  • __init__() 方法第一个参数一定是 self,表示实例对象本身,它在实例对象完成创建后,自动调用,从而初始化实例对象。
  • __init__() 方法,在创建一个实例对象时默认被调用,不需要手动调用;
  • __init__() 方法中 self 参数不需要开发者传递, Python 解释器会自动把当前对象引用传递过去。
5.3.2 __new__() 方法

__new__() 方法是用来创建一个实例对象的。

class Person(object):
    def __init__(self):
        print("这是__init__()方法")
        self.country = "中国"

    def __new__(cls):
        print("这是__new__()方法")
        return object.__new__(cls)


Person()

输出:
这是__new__()方法
这是__init__()方法

__new__()__init__() 小结:

  1. 从分类上,__new__() 方法是一个类方法,__init__() 是一个实例方法。
  2. 从作用上,__new__() 方法用于创建新实例对象,__init__() 用于初始化一个实例化对象。
  3. 从语法上,__new__() 方法第一个参数是 cls,代表要实例化的类,此参数在实例化时由 Python 解释器自动提供,同时必须要有返回值,返回实例化出来的实例,这点在自己实现 __new__() 时要特别注意,可以 return 父类 new 出来的实例对象,或者直接是 object 的 new 出来的实例对象。__init__() 第一个参数是 self,就是这个 __new__() 返回的实例对象,__init__()__new__() 的基础上可以完成一些其它初始化动作,__init__() 不需要返回值。
  4. 从执行的顺序上,先执行 __new__(),再执行 __init__()
5.3.3 __str__() 方法

__str()__ 方法也是一个魔法方法,用来显示信息。该方法需要 return 一个数据,并且只有 self 一个参数,当在类的外部 print(对象),则打印这个数据。如果没有自定义这个 __str()__ 方法,则默认打印实例对象的内存地址。
当类的实例化对象,拥有 __str()__ 方法后,那么打印实例对象,则打印 __str()__ 方法的返回值。

举例如下:

class Foo(object):
    pass


class Hero(object):
    """定义一个英雄类,可以移动和攻击"""
    def __init__(self, name, skill, hp, atk, armor):
        """__init() 方法用来做变量初始化 或 赋值操作,在类实例化对象的时候自动被调用"""
        self.name = name  # 姓名
        self.skill = skill  # 技能
        self.hp = hp  # 生命值
        self.atk = atk  # 攻击力
        self.armor = armor  # 护甲值

    def move(self):
        """实例方法"""
        print("全军出击!")

    def attack(self):
        """实例方法"""
        print(f"放出{self.skill}!")

    def __str__(self):
        """魔法方法,用来显示信息"""
        return f"英雄{self.name}数据:生命值 {self.hp},攻击力 {self.atk},护甲值 {self.armor}"


f = Foo()
print(f)  # 没有自定义__str__(),默认打印实例对象f的内存地址
zhuang_zhou = Hero("庄周", "化蝶", 6666, 1000, 900)
hou_yi = Hero("后裔", "炙热之风", 5000, 666, 999)

# 自定义 __str__() 方法后,打印 __str()__ 方法返回值
print(zhuang_zhou)
print(hou_yi)

# 查看类的文档说明
print(Hero.__doc__)

结果输出:
<__main__.Foo object at 0x0000028F8665BF08>
英雄庄周数据:生命值 6666,攻击力 1000,护甲值 900
英雄后裔数据:生命值 5000,攻击力 666,护甲值 999
定义一个英雄类,可以移动和攻击

小结:

  • print(object) 时,默认打印该对象的内存地址。如果类定义了 __str__(self) 方法,那么就会打印 __str__(self) 方法的返回值。
  • __str__(self) 方法通常返回一个字符串,作为这个对象的描述信息。
5.3.4 __del__() 方法

__new__()方法创建实例对象, __init__() 方法初始化实例对象。当彻底删除对象时,Python 解释器也会默认调用 __del__() 方法。

class Hero(object):
    """定义一个英雄类,可以移动和攻击"""
    def __init__(self, name):
        """__init() 方法是一个初始化方法,用来做变量初始化 或 赋值操作。
        在创建完对象后自动被调用"""
        print("__init__()方法被调用")
        self.name = name

    def __del__(self):
        """当对象删除时,会被自动调用"""
        print("___del__()方法被调用")
        print(f"{self.name}被淘汰了!")

    def __str__(self):
        """魔法方法,用来显示信息"""
        return f"英雄{self.name}淘汰!"


# 创建对象
zhuang_zhou = Hero("庄周")
print("-" * 20)

# 删除对象
print(f"{id(zhuang_zhou)}被删除1次")
del(zhuang_zhou)
print("-" * 20)

hou_yi = Hero("后裔")
hou_yi1 = hou_yi
hou_yi2 = hou_yi

print(f"{id(hou_yi)}被删除第1次")
del(hou_yi)
print(f"{id(hou_yi1)}被删除第2次")
del(hou_yi1)
print(f"{id(hou_yi2)}被删除第3次")
del(hou_yi2)

结果输出:
__init__()方法被调用
--------------------
2133117525512被删除1次
___del__()方法被调用
庄周被淘汰了!
--------------------
__init__()方法被调用
2133117525512被删除第1次
2133117525512被删除第2次
2133117525512被删除第3次
___del__()方法被调用
后裔被淘汰了

总结:

  • 当有变量保存了一个对象的引用时,该对象的引用计数就会 +1。
  • 当使用 del() 删除变量指向的对象时,则会减少对象的引用计数。如果对象的引用计数不为 1,那么会让这个对象的引用计数 -1,当对象的引用计数为 0 时,这个对象才会被真正的删除(内存被回收)。
  • ___del__() 方法在实例对象引用计数为 0 时会自动调用。
5.3.5 __iter__()__next__() 方法

__iter__()__next__() 方法用于给类添加迭代器行为。定义一个 __iter__() 方法来返回一个带有 __next__() 方法的对象。如果类已定义了 __next__(),则 __iter__() 可以简单地返回 self,这里给出官方的例子。

class Reverse(object):
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

5.4 @property

【问题】
在绑定属性时,我们直接把属性暴露出去,虽然写起来简单,但是没办法检查参数,导致属性可以随意修改。如对于学生 Student 这个类:
s = Student()s.score = 66666 学生分数为 6666,这显然不合逻辑。
【解决方案】
为了限制 score 范围,可以通过一个 set_score 方法来设置成绩,再通过一个get_score方法来获取成绩,这样在set_score 方法里就可以检查参数:

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('Score must be an integer')
        if value < 0 or value > 100:
            raise ValueError('Score must between 0~100')
        self._score = value

这样,对任意的 Student 实例进行操作,都不能随心所欲地设置 score 了。

In [2]: s = Student()

In [3]: s.set_score(100)  # ok!

In [4]: s.get_score()
Out[4]: 100

In [5]: s.set_score(6666)  # error
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-497d0a8bc0b3> in <module>
----> 1 s.set_score(6666)  # error

<ipython-input-1-edf7716397d7> in set_score(self, value)
      8             raise ValueError('Score must be an integer')
      9         if value < 0 or value > 100:
---> 10             raise ValueError('Score must between 0~100')
     11         self._score = value

ValueError: Score must between 0~100

但是这样的调用方法又略显复杂,没有直接用属性,那么简单直接。
如何实现既能检查参数,又能用类似属性这样简单的方式来访问类的变量呢?
通过装饰器(decorator)可以动态和函数加上功能。对于类方法,装饰器一样起作用。

Python 内置的 @property 装饰器就是负责把一个方法变成属性调用的:

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('Score must be an integer')
        if value < 0 or value > 100:
            raise ValueError('Score must between 0~100')
        self._score = value

@property 的实现比较复杂,我们先考虑如何使用。把一个 getter 方法变成属性,只需要加上@property 就可以了,此时,@property 本身又创建了另一个装饰器 @score.setter,负责把一个 setter 方法变成属性赋值,于是我们就拥有一个可控的属性操作:

In [2]: s = Student()

In [3]: s.score = 99  # OK!,实际转化为s.set_score(99)

In [4]: s.score  # OK,实际转化为s.get_score()
Out[4]: 99

In [5]: s.score = 666
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-3afa376b49cc> in <module>
----> 1 s.score = 666

<ipython-input-1-9bf23040a06a> in score(self, value)
     10             raise ValueError('Score must be an integer')
     11         if value < 0 or value > 100:
---> 12             raise ValueError('Score must between 0~100')
     13         self._score = value
     14

ValueError: Score must between 0~100

注意到这个神奇的 @property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过 gettersetter 方法来实现的。
还可以定义只读属性,只定义 getter 方法,不定义 setter 方法就是一个只读属性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2020 - self._birth

上面的 birth 是可读写属性,而 age 就是一个只读属性,因为 age 可以根据 birth 和当前时间计算出来。

小结:
@property 广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

6. 继承

继承(inheritance)是描述多个类之间的所属关系。好比爷爷、爸爸、儿子这样的关系。如果不支持继承,语言特性就不值得称为类。
如果一个类 A 的属性和方法可以复用,则可以通过继承的方式,传递到类 B。
那么类 A 就是基类(base class),也叫做父类;类 B 就是派生类(derived class),也叫做子类。

class Father(object):
    """这是父类"""
    def __init__(self):
        self.age = 50

    def print_age(self):
        print(self.age)


class Son(Father):
    """这是子类,继承 Father 类"""
    pass


son = Son()
print(son.age)
son.print_age()

结果输出:
50
50

6.1 单继承

子类只继承一个父类。

子类可以继承父类的所有属性和方法,哪怕子类没有自己的属性和方法,也可以使用父类的属性和方法。

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        # 属性
        self.car = "自行车"

    def go_work(self):
        print(f"通过{self.car}去上班")


class Son(Father):
    """定义 Son类,继承 Father 类"""
    pass

Jock = Son()  # 创建子类实例对象
print(Jock.car)  # 子类对象直接使用父类的属性
Jock.go_work()  # 子类对象直接使用父类的方法

结果输出:
自行车
通过自行车去上班

小结:

  • 虽然子类没有自定义 __init__() 方法,也没有定义实例方法,但是父类有。所以只要创建子类的对象,就默认执行了那个继承过来的 __init__() 方法。
  • 子类在继承的时候,在定义类时,小括号()中为父类的名字。
  • 父类的属性、方法、会被继承给子类。

6.2 多继承

子类继承多个父类。

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        self.car = "自行车"

    def go_work(self):
        print(f"通过{self.car}去上班")

    def black_hair(self):
        print("爸爸的黑发!")


class Mother(object):
    """定义 Mother 类"""
    def __init__(self):
        self.car = "电动车"

    def go_work(self):
        print(f"通过{self.car}去上班")

    def big_eyes(self):
        print("妈妈的大眼睛!")


class Son(Father, Mother):
    """定义 Son类,继承 Father 类 和 Mother 类"""
    pass


Jock = Son()  # 创建子类实例对象
print(Jock.car)  # 执行 Father 属性
Jock.go_work()  # 执行 Father 的实例方法

# 子类的魔法属性 __mro__ 决定了属性和方法的查找顺序
print(Son.__mro__)

Jock.black_hair()  # 不重名不受影响
Jock.big_eyes()

结果输出:
自行车
通过自行车去上班
(<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class 'object'>)
爸爸的黑发!
妈妈的大眼睛!

小结:

  • 多继承可以继承多个父类,也继承了所有父类的属性和方法。
  • 注意多个父类中有同名的属性和方法,则默认使用第一个父类的属性和方法。根据类的模法属性 __mro__ 的顺序来查找。
  • 多个父类中,不重名的属性和方法,不会有任何影响。

6.3 多层继承

多层继承即多次继承。

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        self.car = "自行车"

    def go_work(self):
        print(f"爸爸通过{self.car}去上班")


class Mother(object):
    """定义 Mother 类"""
    def __init__(self):
        self.car = "电动车"

    def go_work(self):
        print(f"妈妈通过{self.car}去上班")


class Son(Father, Mother):
    """定义 Son类,继承 Father 类 和 Mother 类"""
    def __init__(self):
        self.car = "小轿车"
        self.age = 25

    def go_work(self):
        self.__init__()  # 执行本类的__init__方法,做属性初始化 self.car = "小轿车"
        print(f"儿子通过{self.car}去上班")

    # 调用父类方法格式:父类类名.父类方法(self)
    def dad_go_work(self):
        Father.__init__(self)  # 调用父类Father的__init__方法,self.car = "自行车"
        Father.go_work(self)  # 调用父类Father的实例方法

    def mom_go_work(self):
        Mother.__init__(self)  # 调用父类 Mother 的__init__方法,self.car = "电动车"
        Mother.go_work(self)  # 调用父类Mother的实例方法

class Grandson(Son):
    """定义类 Grandson, 多层继承"""
    pass

Bob = Grandson()
Bob.go_work()
Bob.dad_go_work()
Bob.mom_go_work()

print(Bob.age)

输出:
儿子通过小轿车去上班
爸爸通过自行车去上班
妈妈通过电动车去上班
25
6.4 方法解析顺序(MRO)

方法解析顺序(Method Resolution Order, MRO),对于多继承和多层继承很重要的。

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的当层次结构中存在重叠时,不会在同一个类中搜索两次。因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。

具体官网文档是:Method Resolution Order

在写多层继承时,需要注意一下父类的定义顺序和继承顺序,可以参看TypeError: Cannot create a consistent method resolution order (MRO) 在查找实例属性/方法时,Python 需要确定以哪种顺序搜索(直接和间接)基类。 它通过使用 C3 或 MRO 算法将继承图线性化(即通过将基类的图转换为序列)来实现此目的。 MRO 算法是一种独特的算法,可以实现几个理想的属性:

  1. 每个祖先类只出现一次
  2. 一个类总是出现在其祖先之前(“单调性”),即继承时,
  3. 同一类的直接父级应该以与类定义中列出的顺序相同的顺序出现(“一致的本地优先顺序”)
  4. 如果 A 类的子代始终出现在 B 类的子代之前,则 A 应该出现在 B 子之前(“一致的扩展优先顺序”)

换句话说就是(这部分有待进一步深入了解):

  1. 多个父类不能有重复,如 Son(Father, Father)。
  2. 多个父类之间是继承关系时,子类写在父类前面,如 A(object), B(A), 那么 C(B, A),如果写成 C(A, B)则会报错 TypeError,具体可以看前面给出的链接。
  3. 多个父类是并列关系时,继承顺序要和定义父类的顺序一致。
  4. 如果 A 类的子代始终出现在 B 类的子代之前,则 A 应该出现在 B 子之前;

6.5 isinstance()issubclass 函数

Python 有两个内置函数可被用于继承机制:

isinstance():用来检查一个实例对象的类型,如:isinstance(obj, int) ,obj 为 int 类型或者继承自 int 类型时,返回 True,反之返回 False。

issubclass():用来检查类的继承关系,如:issubclass(bool, int) 为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。

class A(object):
    pass


class B(object):
    pass


class C(A):
    pass

a = A()
print(f"a是A的实例:{isinstance(a, A)}")
print(f"a是B的实例:{isinstance(a, B)}")

print(f"C是A的子类:{issubclass(C, A)}")
print(f"C是B的子类:{issubclass(C, B)}")

结果输出:
a是A的实例:True
a是B的实例:False
C是A的子类:True
C是B的子类:False

6.6 子类重写父类同名属性和方法

如果子类和父类的方法名和属性名相同,则默认使用子类的,叫做子类重写父类的同名方法和属性。

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        self.car = "自行车"

    def go_work(self):
        print(f"通过{self.car}去上班")

    def black_hair(self):
        print("爸爸的黑发!")


class Mother(object):
    """定义 Mother 类"""
    def __init__(self):
        self.car = "电动车"

    def go_work(self):
        print(f"通过{self.car}去上班")

    def big_eyes(self):
        print("妈妈的大眼睛!")


class Son(Father, Mother):
    """定义 Son类,继承 Father 类 和 Mother 类"""
    def __init__(self):
        self.car = "小轿车"

    def go_work(self):
        print(f"通过{self.car}去上班")


Jock = Son()  # 创建子类实例对象
print(Jock.car)  # 子类和父类有同名属性,则默认使用子类的
Jock.go_work()  # 子类和父类有同名方法,则默认使用子类的

# 子类的魔法属性 __mro__ 决定了属性和方法的查找顺序
print(Son.__mro__)

输出结果:
小轿车
通过小轿车去上班
(<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class 'object'>)

6.7 子类调用父类同名属性和方法

重点:无论何时何地,self 都是表示子类的对象。在调用父类方法时,通过传递 self 参数,来控制方法和属性的访问修改。

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        self.car = "自行车"  # 实例变量,属性

    def go_work(self):
        """实例方法,方法"""
        print(f"通过{self.car}去上班")


class Mother(object):
    """定义 Mother 类"""
    def __init__(self):
        self.car = "电动车"

    def go_work(self):
        print(f"通过{self.car}去上班")


class Son(Father, Mother):
    """定义 Son类,继承 Father 类 和 Mother 类"""
    def __init__(self):
        self.car = "小轿车"

    def go_work(self):
        print(f"执行子类__init__方法前,self.car属性:{self.car}")
        self.__init__()  # 执行本类的__init__方法,做属性初始化,self.car = "小轿车"
        print(f"执行子类__init__方法后,self.car 属性:{self.car}")
        print(f"son 是通过{self.car}去上班!")

    def dad_go_work(self):
        # 不推荐这样访问父类的实例属性,相当于创建了一个新的父类对象
        # print(f"直接调用 Father 类的 car 属性:{Father().car}")

        # 可以通过执行 Father 类的 __init__ 方法,来修改 self 的属性值
        print(f"执行 Father 的__init__方法前,self.car属性:{self.car}")
        Father.__init__(self)  # 调用父类 Father 的__init__方法,self.car = "自行车"
        print(f"执行 Father 的__init__方法后,self.car属性:{self.car}")
        Father.go_work(self)  # 调用父类Father的实例方法

    def mom_go_work(self):
        # 不推荐这样访问父类的实例属性,相当于创建了一个新的父类对象
        # print(f"直接调用 Mother 类的 car 属性:{Mother().car}")

        # 可以通过执行 Mother 类的 __init__ 方法,来修改 self 的属性值
        print(f"执行 Mother 的__init__方法前,self.car属性:{self.car}")
        Mother.__init__(self)  # 调用父类 Mother 的__init__方法,self.car = 电动车"
        print(f"执行 Mother 的__init__方法后,self.car属性:{self.car}")
        Mother.go_work(self)  # 调用父类Mother的实例方法

Jock = Son()  # 实例化对象,自动执行子类的__init__方法

Jock.go_work()  # 调用子类的方法(默认重写了父类的同名方法)

print("-" * 20)
Jock.dad_go_work()  # 进入实例方法去调用父类Father的方法

print("-" * 20)
Jock.mom_go_work()  # 进入实例方法去调用父类Mother的方法

print("-" * 20)
Jock.go_work()  # 调用本类的实例方法

结果输出:
执行子类__init__方法前,self.car属性:小轿车
执行子类__init__方法后,self.car 属性:小轿车
son 是通过小轿车去上班!
--------------------
执行 Father 的__init__方法前,self.car属性:小轿车
执行 Father 的__init__方法后,self.car属性:自行车
通过自行车去上班
--------------------
执行 Mother 的__init__方法前,self.car属性:自行车
执行 Mother 的__init__方法后,self.car属性:电动车
通过电动车去上班
--------------------
执行子类__init__方法前,self.car属性:电动车
执行子类__init__方法后,self.car 属性:小轿车
son 是通过小轿车去上班!

6.8 super()的使用

class Father(object):
    """定义 Father 类"""
    def __init__(self):
        self.car = "自行车"

    def go_work(self):
        print(f"爸爸通过{self.car}去上班")


# 父类是Father
class Son(Father):
    """定义 Son 类,单继承 Father"""
    def __init__(self):
        self.car = "电动车"

    def go_work(self):
        print(f"儿子通过{self.car}去上班")
        super().__init__()  # 执行父类的构造方法
        super().go_work()  # 执行父类的实例方法


# 父类是 Father 和 Son
# class Grandson(Father, Son):  # 报错,TypeError: Cannot create a consistent method resolution order (MRO) for bases Father, Son
class Grandson(Son, Father):
    """定义类 Grandson, 多继承,继承多个父类"""
    def __init__(self):
        self.car = "小轿车"

    def go_work(self):
        self.__init__()  # 执行本类的__init__方法,做属性初始化 self.car = "小轿车"
        print(f"孙子通过{self.car}去上班")

    def all_go_work(self):
        # 方式1.指定执行父类的方法 (代码臃肿)
        # Son.__init__(self)
        # Son.go_work(self)  # 这里打印儿子通过电动车去上班 和 爸爸通过自行车去上班
        #
        # Father.__init__(self)
        # Father.go_work(self)
        #
        # self.__init__()
        # self.go_work()

        # 方法2. super() 带参数版本,只支持新式类
        # super(Grandson, self).__init__()  # 执行父类的__init__方法
        # super(Grandson, self).go_work()
        # self.go_work()

        # 方法3. super()简化版,只支持新式类
        super().__init__()  # 执行父类的__init__方法
        super().go_work()  # 执行父类的实例方法
        self.go_work()  # 执行本类的实例方法


Jock = Grandson()
Jock.go_work()
print("-" * 20)
Jock.all_go_work()

结果输出:
孙子通过小轿车去上班
--------------------
儿子通过电动车去上班
爸爸通过自行车去上班
孙子通过小轿车去上班

小结:

  • 子类继承了多个父类,如果父类的类名更改了,那么子类也要多次修改,而且需要重复写多次调用,显得代码臃肿。
  • 使用 super() 可以逐一调用所有的父类方法,并且只执行一次。调用顺序遵循 mro 类属性的顺序。
  • 注意:如果继承了多个父类,且父类都有同名方法,则默认只执行第一个父类的(同名方法只执行一次,目前 super() 不支持执行多个父类的同名方法)
  • super() 是在 Python 2.3 之后才有的机制,用于通常单继承的多层继承。

7. 封装

封装是一种思想。生活中,出去旅行,要把所用到的东西分门别类摆放整齐的装进旅行箱里,就是封装。

计算机中,封装是指将数据与具体操作的实现代码放在某个对象内部,使这些代码的实现细节不被外界发现,外界只能通过接口使用该对象,而不能通过任何形式修改对象内部实现。
由于封装机制,程序在使用某一对象时,不需要关心该对象的数据结构细节及实现操作的方法。使用封装能隐藏对象实现细节,使代码更易维护。
同时因为不能直接调用、修改对象内部的私有属性,在一定程度上保证了系统安全性。
类通过将函数和变量封装在内部,实现了比函数更高一级的封装。

8. 多态

多态:定义时的类型和运行时的类型不一样,即称为多态。多态的概念是应用于 Java 和 C# 这一类强类型的语言中,而 Python 崇尚“鸭子类型”。

鸭子类型:我想要一只鸭子,虽然你给了我一只鸟,但是只要这只鸟走路像鸭子,叫起来像鸭子,游泳也像鸭子,我就认为这是鸭子。

Python 的多态就是弱化类型,重点在于对象参数是否有指定的属性和方法,如果有就认定合适,而不关心对象的类型是否正确。

class F1(object):
    def show(self):
        print('F1.show')


class S1(F1):
    def show(self):
        print('S1.show')


class S2(F1):
    def show(self):
        print('S2.show')

# 由于在Java或C#中定义函数参数时,必须指定参数的类型,为了让Func函数既可以执行S1对象的show方法,又可以执行S2对象的show方法
# 所以在 def Func 的形参中obj的类型是S1和S2的父类,即F1
def Func(F1 obj):  # 这在 Python 里面会报错,需要写成 Func(obj)
    """Func 函数需要接收一个F1类型或者F1子类的类型"""
    obj.show()

s1_obj = S1()
Func(s1_obj)  # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S1.show

s2_obj = S2()
Func(s2_obj)  # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S2.show

理解:定义 obj 这个变量是说的类型是 F1 的类型,但是在真正调用 Func 函数时给其传递的不一定是 F1 类的实例对象,有可能是其子类的实例对象,这种情况就是所谓的多态。

class F1(object):
    def show(self):
        print('F1.show')


class S1(F1):
    def show(self):
        print('S1.show')


class S2(F1):
    def show(self):
        print('S2.show')


def Func(obj):
    """Python 是弱类型,即无论传递过来的是什么,obj变量都能够指向它,这也就没有所谓的多态了(弱化了这个概念)"""
    obj.show()

s1_obj = S1()
Func(s1_obj)  # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S1.show

s2_obj = S2()
Func(s2_obj)  # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S2.show

9. 单例模式

单例是什么:我们电脑中的回收站,在整个操作系统中,回收站只能有一个实例,真个系统都是用这个唯一的实例,而且回收站自行提供自己的实例。因此回收站是单例模式的应用。

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,单例模式是一种对象创建型模式。

创建单例:保证只有 1 个实例对象

class Singleton(object):
    __instance = None

    def __new__(cls, age, name):
        """如果类属性__instance的值为None,
        那么就创建一个对象,并且赋值为这个对象的引用,保证下次调用这个方法时,
        能够指导之前已经创建过对象了,这样就保证了只有1个对象"""
        if not cls.__instance:
            cls.__instance = object.__new__(cls)
        return cls.__instance


a = Singleton(18, "Jock")
b = Singleton(18, "Jock")

print(id(a))
print(id(b))

a.age = 19  # 给a指向的对象添加一个属性
print(b.age)  # 获取b指向的对象的age属性

运行结果:
2438110470920
2438110470920
19

创建单例时,只执行一次__init__()方法。

class Singleton(object):
    """单例"""
    __instance = None
    __is_first = True

    def __new__(cls, age, name):
        if not cls.__instance:
            cls.__instance = object.__new__(cls)
        return cls.__instance

    def __init__(self, age, name):
        if self.__is_first:
            self.age = age
            self.name = name
            Singleton.__is_first = False


a = Singleton(18, "Jcok")
b = Singleton(25, "Jock")

print(id(a))
print(id(b))

print(a.age)
print(b.age)

a.age = 19
print(b.age)

输出结果:
1696365766536
1696365766536
18
18
19

10. 小结

  1. 掌握类的定义以及类的实例化。
  2. 区分类对象、实例对象、对象。
  3. 掌握类属性、实例属性、公有属性、私有属性的定义、访问和修改。
  4. 掌握类方法、静态方法、实例方法的定义、调用。
  5. 掌握__slots____doc____mro__等魔法属性。
  6. 掌握 __init__(self)__new__(cls)的用法和区别。
  7. 掌握 @classmethode@staticmethod@property等装饰器在类中的使用。
  8. 掌握单继承、多继承以及多层继承。
  9. 掌握单例模式
  10. 了解方法解析顺序(MRO)是深度优先和从左到右。
  11. 了解封装的思想。
  12. 了解 Python 的多态。

面向对象需要不断的学习和实践,才能更好地理解里面的思想。总结的不对的,希望各位读者能批评指正!

11. 巨人的肩膀

  1. The Python Tutorial
  2. 廖雪峰 Python3 教程