首页/Python 与数据实践/类型判定与抽象基类
Python 与数据实践 · 第 07 章

类型判定与抽象基类

前面我们已经会造类、会造对象了。这一章往上再走一层,回答几个更本质的问题:对象的"类型"到底由什么决定?类自己又是谁造出来的? 把这条"制造链"弄清楚之后,我们再学一个工程里非常值钱的工具——抽象基类(Abstract …

约 16 分钟6,551 字17 个代码示例

前面我们已经会造类、会造对象了。这一章往上再走一层,回答几个更本质的问题:对象的"类型"到底由什么决定?类自己又是谁造出来的? 把这条"制造链"弄清楚之后,我们再学一个工程里非常值钱的工具——抽象基类(Abstract Base Class,简称 ABC):它专门用来给一族类"立规矩"。


一、万物皆有出处:类型从哪来

在 Python 的世界里,任何对象都是被某个类造出来的,造出它的那个类,就叫它的类型。用 type(对象) 就能看到:

book = EBook("Python 从入门到实践")

print(type(book))    # <class '__main__.EBook'>,book 由 EBook 造出
print(type(123))     # <class 'int'>
print(type(len))     # <class 'builtin_function_or_method'>

把这条规则往上追,会得到一张很有意思的"族谱":

  • 所有函数的类型是 function
  • 所有类的类型是 type
  • type 自己的类型还是 type(自己造自己)

给"造类的类"起了个名字:元类(metaclass)。 type 就是 Python 里默认的元类——类是对象的图纸,而元类是图纸的图纸。

可以用下面几行验证这张族谱(完整版见本章 demo.py 第一部分):

print(type(EBook))    # <class 'type'>   类是 type 造的
print(type(type))     # <class 'type'>   type 也是 type 造的
print(type(len))      # <class 'builtin_function_or_method'>

几个容易绕晕、但面试常考的结论(A 是一个普通类,a = A()):

  • isinstance(A, type) 是 True:类是 type 的实例
  • isinstance(A, A) 是 False:类不是自己造出来的对象
  • isinstance(a, object) 是 True:一切对象都源自 object
  • isinstance(A, object) 也是 True:类本身也是对象

二、type() 的另一副面孔:现场造类

上一章我们用过 type() 的三参数形式,这里再巩固一遍——它不仅能"查"类型,还能"造"类:

# type(类名, (父类元组,), {名字字典})

def intro(self):
    print(f"《{self.title}》正在上架")

Book = type("Book", (), {"title": "数据实践", "intro": intro})

b = Book()
print(b.title)  # 数据实践
b.intro()       # 《数据实践》正在上架

三个参数:类名字符串、父类元组、装着属性和方法的字典。ORM 框架根据数据库表配置批量生成模型类,用的就是这一手。


三、成员查找顺序:属性到底从哪来

实例找成员(属性或方法)的顺序:

  1. 先看实例自己身上(实例的 __dict__)
  2. 再沿着它所属类及父类构成的 MRO 链一路向上

类找成员的顺序:

  1. 先查自身的 MRO 链
  2. 再查元类的 MRO 链

下面这个实验能直观说明"方法沿着 MRO 向上找"。A 自己没有定义 __repr__,可它依然能用——因为最终找到的是 object 里那份,两个名字指向同一个对象:

class A:
    pass

print(id(object.__repr__))   # 例如 43521... 
print(id(A.__repr__))       # 与上面完全相同:A 沿 MRO 借用了 object 的 __repr__

查看 MRO 的几种姿势

class Base:
    pass

class Left(Base):
    pass

class Right(Base):
    pass

class Child(Left, Right):
    pass

print(Child.__mro__)    # (Child, Left, Right, Base, object)
print(Child.mro())      # 结果同上,返回列表形式
print(Child.__bases__)  # (Left, Right):所有直接父类
print(Child.__base__)   # Left:__bases__ 的第一个

绑定方法 vs 类里的普通函数

一个有意思的细节:同一个方法,通过实例取和通过类取,类型不一样:

class A:
    def foo(self):
        pass

print(type(A().foo))   # <class 'method'>:绑定方法(已经绑定了具体实例)
print(type(A.foo))     # <class 'function'>:类里存的其实是个普通函数
print(issubclass(type(A().foo), type(A.foo)))  # False:method 并不是 function 的子类

print(A().foo is A.foo)  # False:每次取绑定方法,都是新造的对象

四、类成员的三种下划线写法

类体里的名字,按前缀分成三档(规则对属性和方法都适用):

class Config:
    _env = "dev"        # 单下划线开头:约定俗成的"内部使用",外部仍可访问,日常开发用它
    __key = "abc123"    # 双下划线开头:触发名称改写,外部无法直接访问
    __version__ = 3     # 前后双下划线:特殊用途,多为系统预留

    @classmethod
    def check(cls):
        # 类内部三个都能访问,畅通无阻
        print(cls._env, cls.__key, cls.__version__)


Config.check()              # dev abc123 3
print(Config._env)          # dev(可以访问,但不建议)
print(Config.__version__)   # 3
# print(Config.__key)      # AttributeError!外部拿不到 __key
print(Config._Config__key)  # abc123(知道名称改写规则就能强行访问)

五、抽象基类:只立规矩,不出活

前面讲的都是"类怎么用"。现在换个视角:如果我们在写一个框架,希望所有接入方都遵守同一套接口,该怎么强制约束?答案是抽象基类。

抽象类是不许直接实例化的类,存在的意义就是给子类立规矩:这几个方法你必须实现。Python 靠标准库 abc 模块提供支持:

from abc import ABC, abstractmethod

class Reporter(ABC):        # 继承 ABC,表明这是个抽象类
    @abstractmethod
    def render(self):
        """子类必须给出自己的实现"""
        pass

# r = Reporter()  # TypeError:抽象类不能实例化

class TableReporter(Reporter):
    def render(self):       # 老老实实实现抽象方法
        print("渲染成表格")

reporter = TableReporter()
reporter.render()  # 渲染成表格

建议在 VS Code 里打开 python.analysis.typeCheckingMode,编辑器会帮你做更严格的类型检查。

四条要点:

  • 继承 ABC,才算抽象类
  • 被 @abstractmethod 标记的方法,子类必须实现
  • 抽象类里也可以写普通方法(带现成实现,子类直接白拿)
  • 抽象类不能实例化

抽象类里可以夹带"现成品"

from abc import ABC, abstractmethod

class Staff(ABC):
    @abstractmethod
    def weekly_hours(self):
        """每周工时,子类自己算"""
        pass

    @abstractmethod
    def hourly_rate(self):
        """时薪,子类自己定"""
        pass

    def monthly_pay(self):
        """普通方法:直接用子类实现的两个抽象方法拼出月薪"""
        return self.weekly_hours() * 4 * self.hourly_rate()

抽象属性

除了方法,属性也能立成抽象的:

from abc import ABC, abstractmethod

class Machine(ABC):
    @property
    @abstractmethod
    def capacity(self):
        """子类必须实现 capacity 属性"""
        pass

class ServerMachine(Machine):
    def __init__(self, slots):
        self._slots = slots

    @property
    def capacity(self):
        return self._slots

server = ServerMachine(16)
print(server.capacity)  # 16

注意: @property 在上、@abstractmethod 在下,两个装饰器的顺序不能颠倒。

少实现一个,就还是抽象类

子类只要漏掉任何一个抽象方法,它自己也会"继承"抽象身份,照样不能实例化:

class FixedStaff(Staff):
    def __init__(self, hours):
        self._hours = hours

    def weekly_hours(self):
        return self._hours

    # 忘了实现 hourly_rate

# s = FixedStaff(40)  # TypeError:FixedStaff 仍是抽象类,不能实例化

六、工程实战:用抽象类统一接口

抽象类最常见的用武之地是插件体系和框架扩展点——框架只认接口,不关心具体实现:

from abc import ABC, abstractmethod

class Exporter(ABC):
    """导出器抽象基类:所有导出器必须实现这三个方法"""

    @abstractmethod
    def setup(self):
        pass

    @abstractmethod
    def export(self, rows):
        pass

    @abstractmethod
    def teardown(self):
        pass

class CsvExporter(Exporter):
    def setup(self):
        print("准备 CSV 环境")

    def export(self, rows):
        return "CSV 内容: " + str(rows)

    def teardown(self):
        print("清理 CSV 环境")

class JsonExporter(Exporter):
    def setup(self):
        print("准备 JSON 环境")

    def export(self, rows):
        return "JSON 内容: " + str(rows)

    def teardown(self):
        print("清理 JSON 环境")


def run_export(exporter: Exporter, rows):
    """统一的导出流程,不关心底层格式"""
    exporter.setup()
    print(exporter.export(rows))
    exporter.teardown()

run_export(CsvExporter(), [1, 2, 3])
run_export(JsonExporter(), [1, 2, 3])

以后要加 Excel 导出、PDF 导出,只要照着接口写新的子类,run_export 一行都不用动——这就是抽象类换来的扩展性。


作业

作业一:费曼复述

合上课件,用自己的话把本章内容讲给同事(或想象中的初学者)听,重点覆盖:type 与元类的关系、实例和类各自的成员查找顺序、三种下划线成员、抽象基类的价值。

作业二:预测输出

不运行代码,逐行写出打印结果,再用本章知识解释每一行为什么:

function = type(lambda: None)

class A:
    pass

a = A()

print(type(A))
print(type(type))
print(isinstance(A, type))
print(isinstance(A, A))
print(isinstance(A, object))
print(isinstance(a, A))
print(isinstance(a, type))
print(isinstance(type, type))
print(isinstance(type, object))
print(isinstance(object, type))

def func():
    pass

print(type(func))
print(isinstance(func, function))
print(isinstance(func, type))

作业三:会话存储抽象类

编写抽象基类 SessionStore,定义会话存储的通用接口,然后实现 MemorySessionStore(字典存储)和 FileSessionStore(文件存储):

from abc import ABC, abstractmethod

class SessionStore(ABC):
    @abstractmethod
    def fetch(self, session_id):
        pass

    @abstractmethod
    def save(self, session_id, data):
        pass

    @abstractmethod
    def discard(self, session_id):
        pass

# 实现 MemorySessionStore(数据放字典里)
# 实现 FileSessionStore(每个会话存成一个 JSON 文件)

作业四:观影清单抽象类

编写抽象基类 WatchList,然后实现 ArrayWatchList(基于 Python 列表)和 LinkedWatchList(基于链表):

from abc import ABC, abstractmethod

class WatchList(ABC):
    @abstractmethod
    def add(self, title):
        pass

    @abstractmethod
    def at(self, index):
        pass

    @abstractmethod
    def total(self):
        pass

    @abstractmethod
    def __iter__(self):
        pass

    def is_empty(self):
        return self.total() == 0

# 实现 ArrayWatchList(内部用 list)
# 实现 LinkedWatchList(内部用单链表,需要自己写节点类)

作业五:思考题

下面代码的输出是什么?为什么?

from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def charge(self):
        pass

    def refund(self):
        print("原路退回")

class Alipay(Payment):
    def charge(self):
        print("支付宝收款")

class WechatPay(Alipay):
    pass

w = WechatPay()
w.charge()
w.refund()

如果把结尾换成下面的代码,会发生什么?

class BankCard(Payment):
    pass

b = BankCard()

下一章:08 · 魔术方法全景