在Python中,用什么来替代接口/协议?

35

我正在制作一个国际象棋游戏,希望能够创建一个标准的棋子接口/协议。Python语言本身没有这些功能,那么我应该使用什么呢?我了解过一些关于工厂模式的知识,但不确定它们如何有所帮助。先提前感谢您的回答!


1
Python 中接口的意义在于什么,因为它无法强制你实现所有方法? - Malik Brahimi
接口对于静态类型的语言非常重要,因为您希望强制继承类实现接口。对于像Python这样的动态语言,类或类的客户端可以查找函数。如果该类有一个函数,则可以执行它,无论它是否继承了它。 - Fred Mitchell
@MalikBrahimi 是的,在Python中无法强制执行,但接口也可以作为记录应实现哪些方法的一种方式。更像是绅士协定。使用ABC,这正是Python所做的。 - mahoriR
相关:https://dev59.com/pG445IYBdhLWcg3wwcyg - Ciro Santilli OurBigBook.com
8个回答

62

Python 3.8 新特性:

接口和协议的一些优点是使用 IDE 内置的工具进行类型提示和静态类型分析,以便在运行时之前检测错误。这样,静态分析工具可以告诉您在检查代码时是否正在尝试访问未定义的对象成员,而不仅仅在运行时发现。

Python 3.8 引入了 typing.Protocol 类,作为 "结构子类型" 的机制。其威力在于它可用作 隐式基类。也就是说,如果任何类的成员与 Protocol 的定义成员匹配,则该类可被视为其子类,以供静态类型分析之用。

PEP 544 中给出的基本示例展示了如何使用此功能。

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None:
        # ...

class Resource:
    # ...
    def close(self) -> None:
        self.file.close()
        self.lock.release()

def close_all(things: Iterable[SupportsClose]) -> None:
    for thing in things:
        thing.close()

file = open('foo.txt')
resource = Resource()
close_all([file, resource])  # OK!
close_all([1])     # Error: 'int' has no 'close' method
注意: typing-extensions 为 Python 3.5+ 回溯了 typing.Protocol

1
好的...这里缺少的是直接告诉代码读者,Resource实现了名为SupportsClose的接口。在Python中是否可能实现呢?是的,有些程序也许可以自动判断,但当我阅读Resource时,我希望直接看到“啊哈,Resource实现了名为SupportsClose的接口”...在Python中是否可能实现呢? - Toskan
4
是的,可以显式地声明一个实现,具体可以参考PEP 544中后面的内容。 - gooberwonder
3
PEP 544提供的示例表明typing.Protocol如何用于支持静态类型分析和类型提示。出于这些目的,类是否显式或隐式地实现了Protocol都是无关紧要的。实际上,我更喜欢不显式继承Protocol,因为这可能会削弱其有用性。如果子类对协议定义的方法进行显式实现,并且在重构中故意将其删除,则其使用者仍然可以调用此方法,而不会被静态类型提示工具警告该方法已不存在。 - gooberwonder
2
是的,这很棒:定义一个协议,然后实现它,并使其难以弄清楚您刚才做了什么,借口是“接口可能会被其他人错误地删除”。您的示例和答案与OP的问题无关,因为您自己将成为消费者。 - Toskan
1
因为您提供的示例是一种反模式。这并不完全正确。协议允许您让类型检查器为您执行鸭子类型验证。这类似于Go,其中接口是隐式实现的 - Rushy Panchal
显示剩余4条评论

21

简而言之,您可能根本不需要担心它。由于Python使用鸭子类型 - 另请参阅维基百科文章获取更广泛的定义 - 如果一个对象有正确的方法,它将简单地工作,否则将引发异常。

您可以可能有一个Piece基类,并具有一些抛出NotImplementedError的方法来指示它们需要重新实现:

class Piece(object):

    def move(<args>):
        raise NotImplementedError(optional_error_message) 

class Queen(Piece):

    def move(<args>):
        # Specific implementation for the Queen's movements

# Calling Queen().move(<args>) will work as intended but 

class Knight(Piece):
    pass

# Knight().move() will raise a NotImplementedError

或者,您可以显式验证接收到的对象,以确保它具有所有正确的方法,或者它是Piece的子类,通过使用isinstanceissubclass

请注意,检查类型可能不被某些人认为是“Pythonic”,并且使用NotImplementedError方法或abc模块-如在这个非常好的答案中提到的-可能更可取。

您的工厂只需要生成具有正确方法的对象实例。


4

我通常不在Python中使用接口,但是如果你必须这样做,你可以使用zope.interface。然后,你可以验证类或对象是否实现了某些接口。此外,它还可以在类没有实现所有方法或属性时引发错误。Twisted和其他框架使用这个库。


最新版本是2014年的py 3.4,主页和CI页面都是404。 - cowlinator

2
我用Python(带有tkinter)编写了一个国际象棋游戏,我的方法是创建一个Piece类、Queen/Knight等继承自Piece类的类、一个Player类、一个Square类以及一个用于tkinter主循环的主程序类。每个Piece都有颜色和位置,还有一个方法来帮助生成直线移动的棋子的移动集合,直到被阻止为止。特定的Piece子类每个都包含一个确定它们移动集合的方法。Square对象包含一个Piece和棋盘上该方格的位置。
主程序类有一个__init__方法,设置棋盘,放置棋子,加载棋子图标和音效,并初始化选项。一个draw_board方法重新绘制棋盘,重置所有棋子并重新绑定热键。然后还有各种其他方法,如加载新图标、开始新游戏、设置音量、保存、撤销、城堡等等。
我还没有完成第10版,但你可以在这里获取第9版的源代码和资源。
你也可以查看开源Shane's Chess Information Database。我从未使用过它,但它看起来相当不错。

1
虽然 Python 是动态语言,可以使用鸭子类型,但仍然可以实现 Java 和 C# 中所谓的“接口”。这可以通过声明抽象基类来完成。https://docs.python.org/2/library/abc.htmlhttps://docs.python.org/3.4/library/abc.html 当您定义 ABC 时,请将所有类似于接口的方法放入其中,并在其主体中使用 passraise NotImplementedError。子类从您的 ABC 继承并像任何其他子类一样覆盖这些方法。(由于 Python 具有多重继承,它们可以从您的 ABC 加上任何其他类继承。)

0
在Python中模仿接口有一个好方法。 当在Python中生成“Interface”类时,使用metaclass = ABCMeta,并对于所有必须为此接口实现的方法使用@abstractmethod装饰器。 两者都来自于abc类。(如果不实现任何这样的@abstractmethod修饰的方法,并且同时继承了实现“I接口”类,则生成此类的实例后将引发NotImplementedError。)
作为命名约定,用大写字母I(代表Interface)开头命名所有这样的类。
from abc import ABCMeta, abstractmethod


class IPiece(metaclass=ABCMeta):
    "The Piece Interface"
    
    @abstractmethod
    def move(<args>):
        "NotImplementedError is superfluous, instead, one can use this space"
        "To write some remarks, comments, annotations..."

class Queen(Piece):

    def move(<args>):
        # Specific implementation for the Queen's movements

0
Python的美妙之处在于接口不是必须的。由于鸭子类型,你可以创建多个类,它们都有相同的方法签名:
class Queen:
  def move(self, x, y):
    #do stuff

class Pawn:
  def move(self, x, y):
    # do stuff

这些类的实例可以互换使用:

def make_move(piece, x, y):
  piece.move(x, y)

q = Queen()
make_move(q, 0, 0)
p = Pawn()
make_move(p, 4, 5)

请注意,上述内容绝不是一个完整的国际象棋游戏的良好设计,仅用于说明目的。

2
但是接口可以让您在编辑时检查是否记得实现了所有必需的方法。此外,如果您有像 board.pieces.move(..) 这样的循环,那么您添加到 pieces 的所有内容都要实现所需的功能。想象每个棋子都是独特的,但具有多种方法,例如“take, retreat,…” - dcsan
@dcsan 我同意如果你正在实现一个需要符合正确协议的新的“piece”类,接口可以帮助。或者,您可以在参数、变量等上使用类型注释。我不确定您所说的“如果您有一个循环...”是什么意思。鸭子类型允许您实现理论上的 board.pieces.move(),如果 board.pieces 是一个具有 move() 方法的对象,该方法循环遍历一组可以是任何实现调用它的方法的类的实例的 piece。 - Code-Apprentice
但是鸭子类型并不能提供现代 IDE 所带来的开发时间便利。它使编码变得乏味,特别是重构。 - dcsan
@dcsan 是的,这就是权衡之处。类型注释有所帮助,但感觉像是一个 hack,接口或抽象基类可能更合适。 - Code-Apprentice

0

typing.Protocol的具体mypy示例(静态类型检查)

https://dev59.com/Gl4b5IYBdhLWcg3whB-V#50255847提供了一个示例,但这里有一个稍微具体一些的示例,包括样本mypy输出。相关:如何在Python中实现虚方法?

protocol.py

from typing import Protocol

class CanFly(Protocol):
    def fly(self) -> str:
        pass

    def fly_fast(self) -> str:
        return 'CanFly.fly_fast'

class Bird(CanFly):
    def fly(self):
        return 'Bird.fly'
    def fly_fast(self):
        return 'Bird.fly_fast'

class FakeBird(CanFly):
    pass

assert Bird().fly() == 'Bird.fly'
assert Bird().fly_fast() == 'Bird.fly_fast'
# mypy error
assert FakeBird().fly() is None
# mypy error
assert FakeBird().fly_fast() == 'CanFly.fly_fast'

如果我们运行这个文件,断言会通过,因为我们没有添加任何动态类型检查。
python protocol.py

但如果我们使用 mypy 进行类型检查:

python -m pip install --user mypy
mypy protocol.py

正如预期,我们遇到了一个错误:

protocol.py:22: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"
protocol.py:24: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"

有点不幸的是,错误检查只在实例化时捕获错误,而不是在类定义时。

typing.Protocol 在方法体“为空”时将其视为抽象方法

我不确定它们认为什么是空的,但以下所有内容都被视为为空:

  • pass
  • ... 省略号对象
  • raise NotImplementedError()

因此,最好的可能性是:

protocol_empty.py

from typing import Protocol

class CanFly(Protocol):
    def fly(self) -> None:
        raise NotImplementedError()

class Bird(CanFly):
    def fly(self):
        return None

class FakeBird(CanFly):
    pass

Bird().fly()
FakeBird().fly()

达到预期的失败状态:

protocol_empty.py:15: error: Cannot instantiate abstract class "FakeBird" with abstract attribute "fly"
protocol_empty.py:15: note: The following method was marked implicitly abstract because it has an empty function body: "fly". If it is not meant to be abstract, explicitly return None.

但是,如果例如我们替换:

raise NotImplementedError()

带有一些随机的“非空”语句,例如:

x = 1

那么mypy不会将它们视为虚拟的,并且不会产生任何错误。

在Python 3.10.7、mypy 0.982和Ubuntu 21.10上进行了测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接