检查函数参数类型是Pythonic吗?

时间:2009-12-23 02:31:03

标签: python typechecking

我知道,类型检查函数参数在Python中通常是不受欢迎的,但我想我已经想出了这样做的情况。

在我的项目中,我有一个抽象基类Coord,子类Vector,它有更多的功能,如旋转,变化幅度等。数字的列表和元组也将返回True { {1}}我还有许多函数和方法接受这些Coord类型作为参数。我已经设置了装饰器来检查这些方法的参数。这是一个简化版本:

isinstance(x, Coord).

这个版本很简单,它仍然有一些bug。它就是为了说明这一点。它将被用作:

class accepts(object):
    def __init__(self, *types):
        self.types = types

    def __call__(self, func):
        def wrapper(*args):
            for i in len(args):
                if not isinstance(args[i], self.types[i]):
                    raise TypeError

            return func(*args)

        return wrapper

注意:我只是针对Abstract Base Classes检查参数类型。

这是个好主意吗?有没有更好的方法来做到这一点,而不必在每个方法中重复类似的代码?

修改

如果我要做同样的事情,但是不是事先在装饰器中检查类型,我会在装饰器中捕获异常:

@accepts(numbers.Number, numbers.Number)
def add(x, y):
    return x + y

这样更好吗?

6 个答案:

答案 0 :(得分:28)

你的口味可能会有所不同,但Pythonic(tm)风格就是继续使用你想要的物品。如果他们不支持您正在尝试的操作,则会引发异常。这称为duck typing

支持这种风格有几个原因:首先,只要新对象支持正确的操作,它就允许您使用现有代码的新类型对象来实现多态性。其次,它通过避免大量检查来简化成功路径。

当然,使用错误参数时得到的错误信息会比使用鸭子打字更清晰,但正如我所说,你的口味可能会有所不同。

答案 1 :(得分:9)

在Python中鼓励Duck Typing的原因之一是有人可能会包装你的一个对象,然后它会看起来像是错误的类型,但仍然可以工作。

以下是包装对象的类的示例。 LoggedObject以各种方式行事,例如它所包裹的对象,但是当您拨打LoggedObject时,它会在执行呼叫之前记录呼叫。

from somewhere import log
from myclass import A

class LoggedObject(object):
    def __init__(self, obj, name=None):
        if name is None:
            self.name = str(id(obj))
        else:
            self.name = name
        self.obj = obj
    def __call__(self, *args, **kwargs):
        log("%s: called with %d args" % (self.name, len(args)))
        return self.obj(*args, **kwargs)

a = LoggedObject(A(), name="a")
a(1, 2, 3)  # calls: log("a: called with 3 args")

如果您明确测试isinstance(a, A),则会失败,因为aLoggedObject的实例。如果你只是让鸭子打字做它的事情,这将有效。

如果有人错误地传递了错误的对象,则会引发AttributeError之类的异常。如果你明确地检查类型,那么异常可能会更清楚,但我认为这种情况总体来说是鸭子打字的胜利。

有时您确实需要测试类型。我最近学到的一个问题是:当你编写与序列一起工作的代码时,有时你真的需要知道你是否有一个字符串,或者它是任何其他类型的序列。考虑一下:

def llen(arg):
    try:
        return max(len(arg), max(llen(x) for x in arg))
    except TypeError: # catch error when len() fails
        return 0 # not a sequence so length is 0

这应该返回序列的最长长度,或嵌套在其中的任何序列。它有效:

lst = [0, 1, [0, 1, 2], [0, 1, 2, 3, 4, 5, 6]]
llen(lst)  # returns 7

但是如果你打电话给llen("foo")它会永远递归直到堆栈溢出。

问题是字符串具有特殊属性,即使从字符串中取出最小元素,它们也总是像序列一样;一个字符的字符串仍然是一个序列。因此,如果没有对字符串进行显式测试,我们就无法编写llen()。

def llen(arg):
    if isinstance(arg, basestring):  # Python 2.x; for 3.x use isinstance(arg, str)
        return len(arg)
    try:
        return max(len(arg), max(llen(x) for x in arg))
    except TypeError: # catch error when len() fails
        return 0 # not a sequence so length is 0

答案 2 :(得分:2)

如果这是规则的例外,那没关系。但是如果你的项目的工程/设计围绕每个函数(或大多数函数)的类型检查,那么你可能不想使用Python,而不是C#呢?

根据我的判断,你做一个类型检查的装饰器通常意味着你将要使用它很多。因此,在这种情况下,虽然将公共代码分解为装饰器是pythonic,但它用于类型检查的事实并不是非常pythonic。

答案 3 :(得分:1)

有一些关于此的讨论,因为Py3k支持function annotations类型注释是一个应用程序。还尝试在Python2中滚动type checking

我认为它从来没有用过,因为你试图解决的基本问题(“查找类型错误”)要么开始很简单(你看到一个TypeError)还是相当难(稍微差别一点)类型接口)。另外,为了使它正确,你需要类型类并在Python中对每种类型进行分类。这对于大多数事情来说都是很重要的。更不用说你一直在做运行时检查。

Python已经拥有强大且可预测的类型系统。如果我们看到更强大的东西,我希望它通过类型注释和聪明的IDE来实现。

答案 4 :(得分:1)

除了已经提到的想法之外,您可能希望将输入数据“强制”为具有所需操作的类型。例如,您可能希望将坐标元组转换为Numpy数组,以便可以对其执行线性代数运算。强制代码非常通用:

input_data_coerced = numpy.array(input_data)  # Works for any input_data that is a sequence (tuple, list, Numpy array…) 

答案 5 :(得分:1)

是。

"成为Pythonic"这不是一个明确定义的概念,但它通常被理解为使用适当的语言结构编写代码,而不是比Python样式指南(PEP 8)更加冗长,并且通常努力使代码具有令人愉快的阅读。我们还有Python的Zen(import this)作为指导。

@accepts(...)注释放在函数顶部是否有助于或损害可读性?可能有帮助,因为规则#2说"Explicit is better than implicit"。还有PEP-484专为完全相同的目的而设计。

运行时的检查类型是否算作Pythonic?当然,它会对执行速度产生影响 - 但Python的目标永远不会产生最高性能的代码,其他一切都是该死的。当然,快速代码比慢速更好,但是可读代码比意大利面条代码更好,可维护代码优于hackish代码,可靠代码优于bug。因此,根据您正在编写的系统,您可能会发现权衡是值得的,并且使用运行时类型检查是值得的。

特别是,规则#10 "Errors should never pass silently."可能被视为支持额外的类型检查。例如,请考虑以下简单情况:

class Person:
    def __init__(self, firstname: str, lastname: str = ""):
        self.firstname = firstname
        self.lastname = lastname

    def __repr__(self) -> str:
        return self.firstname + " " + self.lastname

当你这样称呼时会发生什么:p = Person("John Smith".split())?好吧,一开始没什么。 (这已经成问题了:创建了一个无效的Person对象,但是这个错误已经无声地传递了。然后一段时间后你尝试查看这个人,并获得

>>> print(p)
TypeError: can only concatenate tuple (not "str") to tuple

如果您已经创建了该对象,并且如果您有经验的Python程序员,那么您将很快找出错误。但如果不是呢?错误消息是临界无用的(即您需要知道Person类的内部以使用它)。如果你没有查看这个特定的对象,但是把它腌制成一个文件,几个月后被发送到另一个部门并加载,该怎么办?当识别并纠正错误时,您的工作可能已经陷入困境......

话虽如此,你不必自己编写类型检查装饰器。已经存在专门用于此目的的模块,例如