【问题标题】:Is it Pythonic to check function argument types?检查函数参数类型是 Pythonic 吗?
【发布时间】:2009-12-23 02:31:03
【问题描述】:

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

在我的项目中,我有一个抽象基类Coord,有一个子类Vector,它具有更多功能,如旋转、改变大小等。数字列表和元组也将为isinstance(x, Coord). 返回True有许多接受这些 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

这个版本很简单,还是有一些bug。它只是为了说明这一点。它会像这样使用:

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

注意:我只是根据抽象基类检查参数类型。

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

编辑:

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

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

这样更好吗?

【问题讨论】:

  • 个人口味:我会把for i in len(args): if not isinstance(args[i], self.types[i]):改成for arg, type in zip(args, self.types): if not isinstance(arg, type):
  • 同时使用消息引发 TypeError。

标签: python typechecking


【解决方案1】:

您的品味可能会有所不同,但 Pythonic(tm) 风格是继续前进并根据需要使用对象。如果它们不支持您尝试的操作,则会引发异常。这被称为duck typing

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

当然,使用错误参数时得到的错误消息使用类型检查会比使用鸭子类型更清楚,但正如我所说,你的口味可能会有所不同。

【讨论】:

  • 您是否建议将会引发错误类型异常的部分包装在 try..except 块中,并在 except 子句中进行类型检查以创建更清晰的错误消息?
  • 首先,看看现有的异常是否足够清晰。接下来,这正是TypeError 异常的用途。这种情况的一个很好的例子是尝试对字符串进行项目分配。 'foo'[1]; 'foo'[1] = 'a';
  • @Brad Zeis:不——不要做任何类型检查。让实际的异常实际发生。它们很少见,当它们发生时,它们表明存在严重的设计错误。不要包裹它们。不要检查它们。什么都不要。当它们发生时,解决试图提供完全错误类型的根本原因。
【解决方案2】:

在 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, str):  # Python 3.x; for 2.x use isinstance(arg, basestring)
        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

【讨论】:

  • "问题是字符串有一个特殊的属性,它们总是像一个序列一样,即使你从字符串中取出最小的元素;一个字符的字符串仍然是一个序列" 就是这样我搜索类型检查指南的问题,因为字符串在作为列表参数传递时通过了我的单元测试。
【解决方案3】:

是的。

“Being 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

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

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

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

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

【讨论】:

    【解决方案4】:

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

    根据我的判断,您制作一个用于类型检查的装饰器通常意味着您将大量使用它。所以在这种情况下,虽然将通用代码分解到装饰器中是 Python 风格,但它用于类型检查的事实并不是很 Python 风格。

    【讨论】:

    • 由于这是项目的性能关键部分,我可能会使用 Cython 将它们转换为 C 扩展。不过,我不确定装饰器是否可以与 Cython 一起使用。
    【解决方案5】:

    因为 Py3k 支持 function annotations 类型注释是一个应用程序,所以有一些讨论。还努力在 Python2 中滚动 type checking

    我认为它从未成功,因为您尝试解决的基本问题(“查找类型错误”)要么一开始就微不足道(您看到 TypeError),要么非常困难(类型接口略有不同)。另外,为了正确,您需要类型类并对 Python 中的每种类型进行分类。大部分工作都是徒劳的。更不用说你会一直在做 runtime 检查。

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

        猜你喜欢
        • 2022-01-05
        • 2014-01-07
        • 2011-11-24
        • 2021-12-09
        • 2020-09-21
        • 2021-05-29
        • 2012-02-08
        • 1970-01-01
        • 2012-12-05
        相关资源
        最近更新 更多