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

Bra*_*eis 20 python typechecking

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

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

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
Run Code Online (Sandbox Code Playgroud)

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

@accepts(numbers.Number, numbers.Number)
def add(x, y):
    return x + y
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:

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

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

    def __call__(self, func):
        def wrapper(*args):

            try:
                return func(*args)
            except TypeError:
                raise TypeError, message
            except AttributeError:
                raise AttributeError, message

        return wrapper
Run Code Online (Sandbox Code Playgroud)

那更好吗?

Ned*_*der 30

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

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

当然,使用错误参数时得到的错误信息将通过类型检查比使用鸭子打字更清晰,但正如我所说,您的口味可能会有所不同.

  • @Brad Zeis:不 - 不做任何类型检查.让实际的异常实际发生.它们很少见,当它们发生时,它们表明存在严重的设计错误.不要包裹它们.不要检查它们.什么都没有.当它们发生时,修复试图提供完全错误类型的根本原因. (3认同)

ste*_*eha 10

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

以下是包装对象的类的示例.A 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")
Run Code Online (Sandbox Code Playgroud)

如果你明确地测试isinstance(a, A)它将失败,因为a是一个实例LoggedObject.如果你只是让鸭子打字做它的事情,这将是有效的.

如果有人错误地传递了错误的对象,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
Run Code Online (Sandbox Code Playgroud)

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

lst = [0, 1, [0, 1, 2], [0, 1, 2, 3, 4, 5, 6]]
llen(lst)  # returns 7
Run Code Online (Sandbox Code Playgroud)

但是如果你打电话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
Run Code Online (Sandbox Code Playgroud)


Pas*_*sha 5

这是。

“Pythonic”并不是一个明确定义的概念,但它通常被理解为使用适当的语言结构编写代码,不要过于冗长,遵循 Python 风格指南 (PEP 8),并且通常努力编写令人愉快的代码读书。我们还有 Python 之禅 ( import this) 作为指导。

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

在运行时检查类型算作 Pythonic 吗?当然,这会影响执行速度——但 Python 的目标从来都不是产生尽可能高性能的代码,其他一切都该死的。当然,快速的代码比慢速的代码好,但是可读的代码比意大利面条的代码好,可维护的代码比黑客代码好,可靠的代码比有错误的代码好。因此,根据您正在编写的系统,您可能会发现这种权衡是值得的,并且使用运行时类型检查是值得的。

特别是,规则 #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
Run Code Online (Sandbox Code Playgroud)

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

>>> print(p)
TypeError: can only concatenate tuple (not "str") to tuple
Run Code Online (Sandbox Code Playgroud)

如果您刚刚创建了该对象,并且您是经验丰富的 Python 程序员,那么您会很快找出问题所在。但如果没有呢?该错误消息几乎毫无用处(即您需要了解该类的内部结构Person才能使用它)。如果您没有查看这个特定对象,而是将其腌制到一个文件中,然后将该文件发送到另一个部门并在几个月后加载,该怎么办?当错误被识别并纠正时,您的工作可能已经遇到麻烦了......

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