如何处理Python中的“鸭子类型”?

29 投票
5 回答
7867 浏览
提问于 2025-04-16 20:54

我通常希望我的代码尽量通用一些。这次我正在写一个简单的库,能够使用不同类型的数据对我来说特别重要。

一种做法是强制用户去继承一个“接口”类。对我来说,这感觉更像是Java而不是Python,而且在每个方法里用issubclass检查也不是很吸引人。

我更喜欢的方式是信任用户使用对象,但这样会引发一些AttributeErrors错误。我可以把每个可能出错的调用放在一个try/except块里处理,但这样做也显得有点麻烦:

def foo(obj):
    ...
    # it should be able to sleep
    try:
        obj.sleep()
    except AttributeError:
        # handle error
    ...
    # it should be able to wag it's tail
    try:
        obj.wag_tail()
    except AttributeError:
        # handle this error as well

我是不是应该就不处理错误,期待用户只使用那些有必要方法的对象呢?如果我做了像[x**2 for x in 1234]这样的傻事,我其实会得到一个TypeError而不是AttributeError(因为整数是不可迭代的),所以在某个地方肯定有类型检查在进行——如果我也想做同样的事情呢?

这个问题有点开放,但处理上述问题的最佳方法是什么呢?有没有什么公认的最佳实践?比如,上面提到的可迭代的“类型检查”是怎么实现的?

编辑

虽然AttributeError错误可以接受,但本地函数引发的TypeErrors通常能提供更多关于如何解决错误的信息。举个例子:

>>> ['a', 1].sort()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: int() < str()

我希望我的库能尽可能地提供帮助。

5 个回答

8

如果你只是想让那些还没实现的方法什么都不做,可以试试下面这种写法,而不是用那种很长的 try/except 结构:

getattr(obj, "sleep", lambda: None)()

不过,这种写法可能不太明显,像是一个函数调用,所以你也可以考虑:

hasattr(obj, "sleep") and obj.sleep()

或者如果你想在调用某个东西之前更确定它真的可以被调用,可以这样做:

hasattr(obj, "sleep") and callable(obj.sleep) and obj.sleep()

这种“先看看再跳”的写法在Python中通常不是最推荐的方式,但它非常易读,而且只用一行就能写完。

当然,另一个选择是把 try/except 抽象成一个单独的函数。

18

我不是Python专家,但我觉得除非你能提供一个替代方案来处理那些没有实现某个方法的参数,否则就不应该阻止异常的发生。让调用者来处理这些异常。这样做可以避免让开发者看不到问题。

我在《代码整洁之道》中读到,如果你想在一个集合中查找某个项目,不要用issubclass(来判断是否是列表)来测试你的参数,而是应该调用getattr(l, "__contains__")。这样,使用你代码的人就有机会传入一个不是列表但定义了__contains__方法的参数,这样也能正常工作。

所以,我认为你应该以抽象通用的方式来编写代码,尽量少设定限制。为此,你需要尽量减少假设。然而,当你遇到无法处理的情况时,抛出异常,让程序员知道他犯了什么错误!

12

如果你的代码需要一个特定的接口,而用户传入了一个没有这个接口的对象,那么十有八九,捕获这个异常是不合适的。大多数情况下,当接口不匹配时,出现AttributeError不仅是合理的,还是可以预期的。

偶尔,有两种情况可能适合捕获AttributeError。第一种是你希望接口的某些部分是可选的;第二种是你想抛出一个更具体的异常,比如某个特定包的异常子类。当然,如果你没有认真处理错误及其后果,就绝对不应该阻止异常的抛出。

所以我觉得这个问题的答案必须根据具体问题和领域来定。这根本上是一个关于使用Cow对象代替Duck对象是否应该有效的问题。如果可以,并且你处理了必要的接口调整,那就没问题。另一方面,除非传入Frog对象会导致灾难性的失败(也就是说,远比堆栈跟踪更糟糕的事情),否则没有必要明确检查用户是否传了这个对象。

话虽如此,记录你的接口总是个好主意——这就是文档字符串(docstrings)等的用途。仔细想想,对于大多数情况,抛出一个通用错误并在文档字符串中告诉用户正确的做法,比试图预见用户可能犯的每一个错误并创建自定义错误信息要高效得多。

最后一点警告——你可能在这里考虑的是用户界面(UI)——我觉得那是另一个故事。检查最终用户给你的输入,以确保它不是恶意的或严重畸形的,并提供有用的反馈,而不是堆栈跟踪,这是很好的。但对于库或类似的东西,你真的必须信任使用你代码的程序员,能够聪明而尊重地使用它,并理解Python生成的错误。

撰写回答