正文
fileio
模块中:
-
-
read(fileno: int, length: int)
-
我们假设
fileio.open
返回一个表示文件描述符的整数【注1】,
fileio.read
从打开的文件描述符中读取
length
个字节,而
fileio.close
则关闭该文件描述符,使其失效。
根据我们写了无数个
__init__
方法所形成的思维习惯,我们可能会这样定义
FileReader
类:
class FileReader:
def __init__(self, path: str) -> None:
self._fd = fileio.open(path)
def read(self, length: int) -> bytes:
return fileio.read(self._fd, length)
def close(self) -> None:
fileio.close(self._fd)
对于我们的初始用例,这没问题。客户端代码通过执行类似
FileReader("./config.json")
的操作,来创建一个
FileReader
,它会将文件描述符
int
作为私有状态维护起来。这正是我们期望的;我们不希望用户代码看到或篡改
_fd
,因为这可能会违反
FileReader
的不变性。构造有效
FileReader
所需的所有必要工作——即调用
open
——都由
FileReader.__init__
处理好了。
然而,随着需求增加,
FileReader.__init__
变得越来越尴尬。
最初我们只关心
fileio.open
,但后来,我们可能需要适配一个库,它因为某种原因需要自己管理对
fileio.open
的调用,并想要返回一个
int
作为我们的
_fd
,现在我们不得不采用像这样的奇怪变通方法:
def reader_from_fd(fd: int) -> FileReader:
fr = object.__new__(FileReader)
fr._fd = fd
return fr
这样一来,我们之前通过规范对象创建过程所获得的所有优势都丢失了。
reader_from_fd
的类型签名接收的只是一个普通的
int
,它甚至无法向调用者建议该如何传入的正确的
int
类型。
测试也变得麻烦多了,因为当我们想要在测试中获取
FileReader
的实例而不做实际的文件 I/O 时,都必须打桩替换自己的
fileio.open
副本,即使我们可以(例如)为测试目的在多个
FileReader
之间共享一个文件描述符。
上述例子都假定
fileio.open
是同步操作。但有许多网络资源实际上只能通过异步(因此:可能缓慢,可能容易出错)API 获得,虽然这可能是一个
假设性
[2]
问题。如果你曾经想要写出
async def __init__(self): ...
,那么你已经在实践中碰到了这种限制。
要全面描述这种方法的所有问题,恐怕得写一本关于面向对象设计哲学的专著。所以我简单总结一下:所有这些问题的根源其实是相同的——我们把“创建数据结构”这个行为与“这个数据结构常见的副作用”紧密地绑定在了一起。既然说是“常见的”,那就意味着它们并非“总是”相关联的。而在那些并不相关的情况下,代码就会变得笨重且容易出问题
总而言之,定义
__init__
是一种反模式,我们需要一个替代方案。