如何处理Python中的“duck typing”?

时间:2011-07-05 23:24:19

标签: python oop types duck-typing

我通常希望尽可能保持代码的通用性。我目前正在编写一个简单的库,并且能够在我的库中使用不同的类型,这次感觉非常重要。

一种方法是强迫人们继承“接口”类。对我而言,这感觉更像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 个答案:

答案 0 :(得分:17)

我不是python pro,但我相信除非你可以尝试替代参数不实现给定方法的时候,你不应该阻止异常被抛出。让调用者处理这些异常。这样,你就会隐藏开发人员的问题。

正如我在Clean Code中所读到的那样,如果您要搜索集合中的项目,请不要使用ìssubclass(列表)测试您的参数,而是要调用{{1 }}。这将使正在使用您的代码的人有机会传递一个不是列表但已定义getattr(l, "__contains__")方法的参数,并且事情应该同样有效。

所以,我认为您应该使用抽象泛型方式进行编码,尽可能强加少数限制。为此,您必须尽可能少地做出假设。但是,当你面对一些你无法处理的事情时,提出异常并让程序员知道他犯了什么错误!

答案 1 :(得分:12)

如果你的代码需要一个特定的接口,并且用户在没有该接口的情况下传递一个对象,那么十分之九的接收异常是不合适的。大多数情况下,AttributeError不仅合理,而且在接口不匹配方面也是预期的。

有时,出于两个原因之一捕捉AttributeError可能是合适的。您希望接口的某些方面是可选,或者您希望抛出更具体的异常,可能是特定于包的异常子类。当然,如果你没有诚实地处理错误和任何后果,你绝不应该阻止抛出异常。

所以在我看来,这个问题的答案必须是特定于问题和领域的。从根本上讲,使用Cow对象而不是Duck对象是否应该起作用的问题。如果是这样,你处理任何必要的界面捏造,那就没关系。另一方面,没有理由明确检查是否有人通过了Frog对象,除非这会导致灾难性的失败(即比堆栈跟踪更糟糕的事情)。

也就是说,记录您的界面总是一个好主意 - 这就是文档字符串(以及其他内容)的用途。当你考虑它时,为大多数情况抛出一般错误并告诉用户在docstring中执行它的正确方法比尝试预测用户可能犯的每个可能错误并创建自定义错误消息要高效得多。

最后的警告 - 你可能在这里想到 UI - 我认为这是另一个故事。最好检查最终用户提供的输入以确保它不是恶意或可怕的错误,并提供有用的反馈而不是堆栈跟踪。但对于库或类似的东西,你真的必须信任程序员使用你的代码来智能和尊重地使用它,并理解Python生成的错误。

答案 2 :(得分:7)

如果你只是希望未实现的方法什么都不做,你可以尝试这样的事情,而不是多行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抽象为一个单独的函数。

答案 3 :(得分:5)

好问题,而且非常开放。我相信典型的Python风格不是通过isinstance或捕获个别异常来检查。 Cerainly,使用isinstance是相当糟糕的风格,因为它打败了鸭子打字的全部要点(虽然在原语上使用isinstance可以正常 - 确保检查整数输入的int和long,并检查字符串的basetring(base) str和unicode的类。如果你做检查,你应该引发一个TypeError。

不检查通常是正常的,因为它通常会引发TypeError或AttributeError,这就是你想要的。 (虽然它可以延迟那些使客户端代码难以调试的错误。)

你看到TypeErrors的原因是原始代码引发了它,因为它确实存在异常。 for循环是硬编码的,如果某些东西不可迭代,就会引发TypeError。

答案 4 :(得分:2)

首先,你问题中的代码并不理想:

try:
    obj.wag_tail()
except AttributeError:
    ...

您不知道AttributeError是来自wag_tail的查找还是发生在函数内部。你要做的是:

try:
    f = getattr(obj, 'wag_tail')
except AttributeError:
    ...
finally:
    f()

编辑:善意地指出,如果您要检查这一点,您还应该检查f是否可以调用。

一般来说,这不是Pythonic。只需调用并将异常过滤器关闭,堆栈跟踪就足以解决问题。我想你应该问问自己,你的rethrown异常是否足以证明所有这些错误检查代码的合理性。

排序列表的情况就是一个很好的例子。

  • 列表排序很常见,
  • 传递不可解决的类型,其中很大一部分是
  • 在这种情况下抛出AttributeError非常令人困惑。

如果这三个标准适用于你的问题(特别是第三个),我同意建立非常例外的重新驱逐者。

你必须平衡这样一个事实,即抛出这些漂亮的错误会使你的代码难以阅读,这在统计上意味着代码中存在更多错误。这是一个平衡利弊的问题。

如果您需要检查行为(例如__real____contains__),请不要忘记使用collectionsio中的Python抽象基类,和numbers