我知道,类型检查函数参数在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
这样更好吗?
答案 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)
,则会失败,因为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
这应该返回序列的最长长度,或嵌套在其中的任何序列。它有效:
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
类的内部以使用它)。如果你没有查看这个特定的对象,但是把它腌制成一个文件,几个月后被发送到另一个部门并加载,该怎么办?当识别并纠正错误时,您的工作可能已经陷入困境......
话虽如此,你不必自己编写类型检查装饰器。已经存在专门用于此目的的模块,例如