类型判定与抽象基类
前面我们已经会造类、会造对象了。这一章往上再走一层,回答几个更本质的问题:对象的"类型"到底由什么决定?类自己又是谁造出来的? 把这条"制造链"弄清楚之后,我们再学一个工程里非常值钱的工具——抽象基类(Abstract …
前面我们已经会造类、会造对象了。这一章往上再走一层,回答几个更本质的问题:对象的"类型"到底由什么决定?类自己又是谁造出来的? 把这条"制造链"弄清楚之后,我们再学一个工程里非常值钱的工具——抽象基类(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:一切对象都源自 objectisinstance(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 框架根据数据库表配置批量生成模型类,用的就是这一手。
三、成员查找顺序:属性到底从哪来
实例找成员(属性或方法)的顺序:
- 先看实例自己身上(实例的
__dict__) - 再沿着它所属类及父类构成的 MRO 链一路向上
类找成员的顺序:
- 先查自身的 MRO 链
- 再查元类的 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 · 魔术方法全景