使用 zope.interface 深入了解 Python 接口

Zope.interface 帮助声明接口的存在、哪些对象提供这些接口以及如何查询这些信息。
116 位读者喜欢这篇文章。
Raspberry Pi and Python

Raspberry Pi 基金会。 CC BY-SA 4.0。

zope.interface 库是克服 Python 接口设计中歧义性的一种方法。让我们来看一下它。

隐式接口不符合禅意

Python 之禅 足够宽松,并且自相矛盾,以至于你可以从中证明任何事情。让我们思考一下它最著名的原则之一:“显式优于隐式。”

传统上,Python 中一直是隐式的一件事是预期的接口。函数文档中说明期望一个“类文件对象”或一个“序列”。但是什么是类文件对象?它支持 .writelines 吗?.seek 呢?什么是“序列”?它支持步进切片吗,例如 a[1:10:2]

最初,Python 的答案是所谓的“鸭子类型”,取自短语“如果它走起来像鸭子,叫起来像鸭子,那它可能就是鸭子。” 换句话说,“尝试一下看看”,这可能是你能得到的最隐式的方式。

为了使这些事情变得显式,你需要一种表达预期接口的方法。最早用 Python 编写的大型系统之一是 Zope Web 框架,它迫切需要这些东西来明确渲染代码(例如)对“类用户对象”的期望。

引入 zope.interface,它由 Zope 开发,但作为单独的 Python 包发布。Zope.interface 帮助声明接口的存在、哪些对象提供这些接口以及如何查询这些信息。

想象一下编写一个简单的 2D 游戏,它需要各种东西来支持“精灵”接口;例如,指示边界框,还要指示对象何时与框相交。与其他一些语言不同,在 Python 中,属性访问作为公共接口的一部分是一种常见的做法,而不是实现 getter 和 setter。边界框应该是一个属性,而不是一个方法。

渲染精灵列表的方法可能如下所示

def render_sprites(render_surface, sprites):
    """
    sprites should be a list of objects complying with the Sprite interface:
    * An attribute "bounding_box", containing the bounding box.
    * A method called "intersects", that accepts a box and returns
      True or False
    """
    pass # some code that would actually render

游戏将有许多处理精灵的函数。在每个函数中,你都必须在文档字符串中指定预期的契约。

此外,某些函数可能期望更复杂的精灵对象,也许是具有 Z 顺序的对象。我们将不得不跟踪哪些方法期望 Sprite 对象,哪些方法期望 SpriteWithZ 对象。

如果能够明确和清楚地说明精灵是什么,以便方法可以声明“我需要一个精灵”并严格定义该接口,那不是很好吗?引入 zope.interface

from zope import interface

class ISprite(interface.Interface):

    bounding_box = interface.Attribute(
        "The bounding box"
    )

    def intersects(box):
        "Does this intersect with a box"

这段代码乍一看有点奇怪。这些方法不包含 self(这是一种常见的做法),并且它有一个 Attribute 的东西。这是在 zope.interface 中声明接口的方式。它看起来很奇怪,因为大多数人不习惯严格声明接口。

这种做法的原因是接口显示了方法的调用方式,而不是它的定义方式。因为接口不是超类,所以它们可以用于声明数据属性。

接口的一种可能的实现可以是圆形精灵

@implementer(ISprite)
@attr.s(auto_attribs=True)
class CircleSprite:
    x: float
    y: float
    radius: float

    @property
    def bounding_box(self):
        return (
            self.x - self.radius,
            self.y - self.radius,
            self.x + self.radius,
            self.y + self.radius,
        )

    def intersects(self, box):
        # A box intersects a circle if and only if
        # at least one corner is inside the circle.
        top_left, bottom_right = box[:2], box[2:]
        for choose_x_from (top_left, bottom_right):
            for choose_y_from (top_left, bottom_right):
                x = choose_x_from[0]
                y = choose_y_from[1]
                if (((x - self.x) ** 2 + (y - self.y) ** 2) <=
                    self.radius ** 2):
                     return True
        return False

显式声明了 CircleSprite 类实现了该接口。它甚至使我们能够验证该类是否正确实现了它

from zope.interface import verify

def test_implementation():
    sprite = CircleSprite(x=0, y=0, radius=1)
    verify.verifyObject(ISprite, sprite)

这是可以通过 pytestnose 或其他测试运行程序运行的东西,它将验证创建的精灵是否符合接口。测试通常是部分的:它不会测试任何仅在文档中提及的内容,甚至不会测试方法是否可以在没有异常的情况下调用!但是,它确实检查了正确的方法和属性是否存在。这是单元测试套件的一个很好的补充,并且至少可以防止简单的拼写错误通过测试。

接下来阅读
标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
自 1998 年以来,Moshe 一直参与 Linux 社区,帮助举办 Linux“安装派对”。他自 1999 年以来一直在编写 Python 程序,并为核心 Python 解释器做出了贡献。Moshe 在 DevOps/SRE 这些术语出现之前就一直是 DevOps/SRE,他非常关心软件可靠性、构建可重现性以及其他此类事情。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.