Python函数如何处理传入的参数类型?

除非我弄错了,在Python中创建一个函数是这样的:

def my_func(param1, param2):
# stuff

但是,实际上并没有给出这些参数的类型。而且,如果我没记错的话,Python是一种强类型语言,因此,Python似乎不应该让你传入与函数创建者期望的不同类型的参数。然而,Python如何知道函数的用户正在传递正确的类型呢?假设函数实际使用了形参,如果它是错误的类型,程序会死吗?必须指定类型吗?

697387 次浏览

从静态或编译时类型检查的意义上讲,Python不是强类型的。

大多数Python代码属于所谓的“Duck Typing”——例如,你在一个对象上寻找read方法——你不关心对象是磁盘上的文件还是套接字,你只想从中读取N个字节。

你从不指定类型;Python有duck typing的概念;基本上,处理参数的代码将对参数做出某些假设——可能通过调用参数期望实现的某些方法。如果参数类型错误,则会引发异常。

一般来说,这取决于你的代码来确保你传递的对象是正确类型的——没有编译器提前强制这一点。

您不需要指定类型。该方法只有在试图访问未在传入参数上定义的属性时才会失败(在运行时)。

这个简单的函数:

def no_op(param1, param2):
pass

... 无论传入哪两个参数都不会失败。

然而,这个函数:

def call_quack(param1, param2):
param1.quack()
param2.quack()

... 如果param1param2都没有名为quack的可调用属性,则将在运行时失败。

Python并不关心你将什么传递给它的函数。当你调用my_func(a,b)时,param1和param2变量将保存a和b的值。Python不知道你正在用正确的类型调用函数,并期望程序员处理这一点。如果函数将使用不同类型的参数调用,您可以使用try/except块包装访问它们的代码,并以您想要的任何方式计算参数。

Alex Martelli解释道,

正常的、Pythonic的、首选的解决方案几乎总是“duck typing”:尝试使用参数,好像它是某种所需的类型,在try/except语句中执行它,捕捉如果参数实际上不是该类型(或任何其他类型,很好地模仿它;-)可能出现的所有异常,并在except子句中尝试其他内容(使用参数“as if”它是其他类型)。

阅读他的文章以获得有用的信息。

Python是强类型的,因为每个对象是类型,每个对象知道是类型,不可能意外或故意使用类型为“好像”的对象是不同的类型的对象,并且对象上的所有基本操作都委托给它的类型。

这与的名字无关。Python中的的名字没有“类型”:如果定义了名称,则名称引用对象,并且对象确实具有类型(但这实际上并不强制的名字具有类型:名称就是名称)。

一个名字在Python中可以很好地参考不同的对象在不同的时间(在大多数编程语言中,尽管不是所有的),没有约束的名字,这样,如果它曾经被称为一个类型的对象X,然后永远地限制只引用其他对象类型的X限制的名字不属于“强类型”的概念,尽管一些爱好者静态打字(名字得到约束,在一个静态的,即编译时,时尚也是如此)确实这样误用了这个词。

在Python中,所有东西都有类型。如果参数类型支持,Python函数将执行它被要求执行的任何操作。

示例:foo将添加所有可以__add__ed的;),而不太关心它的类型。这意味着,为了避免失败,你应该只提供那些支持加法的东西。

def foo(a,b):
return a + b


class Bar(object):
pass


class Zoo(object):
def __add__(self, other):
return 'zoom'


if __name__=='__main__':
print foo(1, 2)
print foo('james', 'bond')
print foo(Zoo(), Zoo())
print foo(Bar(), Bar()) # Should fail

许多语言都有特定类型的变量,变量有一个值。Python没有变量;它有对象,你用名字来指代这些对象。

在其他语言中,当你说:

a = 1

然后,变量(通常是整数)将其内容更改为值1。

在Python中,

a = 1

意思是“使用名称一个来引用对象1”。你可以在交互式Python会话中执行以下操作:

>>> type(1)
<type 'int'>

函数type使用对象1;因为每个对象都知道它的类型,所以type很容易找到该类型并返回它。

同样,无论何时定义函数

def funcname(param1, param2):

函数接收两个对象,并将它们命名为param1param2,不管它们的类型如何。如果您想要确保接收到的对象是特定类型的,那么就按照它们是所需的类型来编写函数,如果不是,则捕获抛出的异常。抛出的异常通常是TypeError(您使用了无效的操作)和AttributeError(您试图访问不存在的成员(方法也是成员))。

其他答案已经很好地解释了duck键入和tzot给出了简单的答案:

Python没有变量,不像其他语言,变量有类型和值;它具有指向对象的名称,这些对象知道它们的类型。

然而,自2010年(第一次提出这个问题的时候)以来,一个有趣的事情发生了变化,即PEP 3107的实现(在Python 3中实现)。你现在实际上可以像这样指定形参的类型和函数的返回类型:

def pick(l: list, index: int) -> int:
return l[index]

这里我们可以看到pick接受两个参数,一个列表l和一个整数index。它还应该返回一个整数。

因此,这里暗示l是一个整数列表,我们可以不费多大力气就能看到,但对于更复杂的函数,列表应该包含什么可能有点令人困惑。我们还希望index的默认值为0。为了解决这个问题,你可以选择像这样写pick:

def pick(l: "list of ints", index: int = 0) -> int:
return l[index]

请注意,我们现在放入一个字符串作为l类型,这在语法上是允许的,但它不适用于以编程方式解析(稍后我们将回到这一点)。

值得注意的是,如果你将一个浮点数传递给index, Python不会引发TypeError,原因是Python设计哲学中的一个要点:“我们都是自愿的成年人”;,这意味着你应该知道什么可以传递给函数,什么不能。如果你真的想编写抛出TypeErrors的代码,你可以使用isinstance函数来检查传递的参数是否属于正确的类型或它的子类,如下所示:

def pick(l: list, index: int = 0) -> int:
if not isinstance(l, list):
raise TypeError
return l[index]

更多关于为什么你应该很少这样做,你应该做什么,在下一节和评论中讨论。

PEP 3107不仅提高了代码的可读性,而且有几个合适的用例,你可以阅读有关这里<强> < / >强的内容。


随着PEP 484的引入,类型注释在Python 3.5中得到了更多的关注,它为类型提示引入了一个标准模块typing

这些类型提示来自类型检查器mypy (GitHub),它现在是PEP 484兼容的。

typing模块提供了一个非常全面的类型提示集合,包括:

  • ListTupleSetDict -分别对应listtuplesetdict
  • Iterable -对生成器有用。
  • Any -当它可以是任何东西。
  • Union -当它可以是指定类型集中的任何东西时,而不是Any
  • Optional -当可能为None时。Union[T, None]的简写。
  • TypeVar -与泛型一起使用。
  • Callable -主要用于函数,但也可以用于其他可调用对象。

这些是最常见的类型提示。一个完整的列表可以在类型模块的文档中找到。

下面是使用typing模块中引入的注释方法的旧示例:

from typing import List


def pick(l: List[int], index: int) -> int:
return l[index]

一个强大的特性是Callable,它允许你输入以函数为参数的注释方法。例如:

from typing import Callable, Any, Iterable


def imap(f: Callable[[Any], Any], l: Iterable[Any]) -> List[Any]:
"""An immediate version of map, don't pass it any infinite iterables!"""
return list(map(f, l))

使用TypeVar而不是Any,上面的例子可以变得更精确,但这已经留给读者练习了,因为我相信我的答案中已经包含了太多关于类型提示所支持的美妙新特性的信息。


以前,当一个Python代码以斯芬克斯为例进行文档化时,上面的一些功能可以通过编写如下格式的文档字符串来获得:

def pick(l, index):
"""
:param l: list of integers
:type l: list
:param index: index at which to pick an integer from *l*
:type index: int
:returns: integer at *index* in *l*
:rtype: int
"""
return l[index]

正如您所看到的,这需要一些额外的行数(确切的行数取决于您想要的显式程度以及如何格式化文档字符串)。但是现在你应该清楚PEP 3107如何提供了一个在许多(所有?)方面更优越的替代方案。在与PEP 484结合使用时尤其如此,正如我们所看到的,PEP 484提供了一个标准模块,为这些类型提示/注释定义了语法,可以以明确、精确而灵活的方式使用,从而形成一个强大的组合。

在我个人看来,这是Python有史以来最伟大的特性之一。我等不及人们开始利用它的力量了。抱歉回答这么长,但这就是我兴奋时发生的事情。


大量使用类型提示的Python代码示例可以在在这里中找到。

在这个页面上,有一个臭名昭著的例外值得提及。

str函数调用__str__类方法时,它会巧妙地检查其类型:

>>> class A(object):
...     def __str__(self):
...         return 'a','b'
...
>>> a = A()
>>> print a.__str__()
('a', 'b')
>>> print str(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type tuple)

就好像Guido提示我们,如果程序遇到意外类型,应该引发哪个异常。

我在其他答案中没有看到这个,所以我把这个加到锅里。

正如其他人所说,Python不会对函数或方法参数强制执行类型。假设您知道自己在做什么,如果您确实需要知道传入的内容的类型,您将检查它并决定自己要做什么。

完成此任务的主要工具之一是isinstance()函数。

例如,如果我编写了一个希望获得原始二进制文本数据的方法,而不是普通的utf-8编码字符串,那么我可以在进入时检查参数的类型,并根据所找到的参数进行调整,或者引发一个异常来拒绝。

def process(data):
if not isinstance(data, bytes) and not isinstance(data, bytearray):
raise TypeError('Invalid type: data must be a byte string or bytearray, not %r' % type(data))
# Do more stuff

Python还提供了各种深入对象的工具。如果您足够勇敢,甚至可以使用importlib动态地为任意类创建自己的对象。我这样做是为了从JSON数据重新创建对象。这样的事情在像c++这样的静态语言中是一场噩梦。

要有效地使用typing模块(Python 3.5新增),请包含all (*)。

from typing import *

你将准备使用:

List, Tuple, Set, Map - for list, tuple, set and map respectively.
Iterable - useful for generators.
Any - when it could be anything.
Union - when it could be anything within a specified set of types, as opposed to Any.
Optional - when it might be None. Shorthand for Union[T, None].
TypeVar - used with generics.
Callable - used primarily for functions, but could be used for other callables.

然而,你仍然可以使用类型名,如intlistdict,…

如果有人想指定变量类型,我已经实现了一个包装器。

import functools
    

def type_check(func):


@functools.wraps(func)
def check(*args, **kwargs):
for i in range(len(args)):
v = args[i]
v_name = list(func.__annotations__.keys())[i]
v_type = list(func.__annotations__.values())[i]
error_msg = 'Variable `' + str(v_name) + '` should be type ('
error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
if not isinstance(v, v_type):
raise TypeError(error_msg)


result = func(*args, **kwargs)
v = result
v_name = 'return'
v_type = func.__annotations__['return']
error_msg = 'Variable `' + str(v_name) + '` should be type ('
error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
if not isinstance(v, v_type):
raise TypeError(error_msg)
return result


return check

使用它作为:

@type_check
def test(name : str) -> float:
return 3.0


@type_check
def test2(name : str) -> str:
return 3.0


>> test('asd')
>> 3.0


>> test(42)
>> TypeError: Variable `name` should be type (<class 'str'>) but instead is type (<class 'int'>)


>> test2('asd')
>> TypeError: Variable `return` should be type (<class 'str'>) but instead is type (<class 'float'>)

编辑

如果没有声明任何参数的(或返回值的)类型,上面的代码就不能工作。下面的编辑可以提供帮助,另一方面,它只对kwarg有效,不检查args。

def type_check(func):


@functools.wraps(func)
def check(*args, **kwargs):
for name, value in kwargs.items():
v = value
v_name = name
if name not in func.__annotations__:
continue
                

v_type = func.__annotations__[name]


error_msg = 'Variable `' + str(v_name) + '` should be type ('
error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ') '
if not isinstance(v, v_type):
raise TypeError(error_msg)


result = func(*args, **kwargs)
if 'return' in func.__annotations__:
v = result
v_name = 'return'
v_type = func.__annotations__['return']
error_msg = 'Variable `' + str(v_name) + '` should be type ('
error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
if not isinstance(v, v_type):
raise TypeError(error_msg)
return result


return check


无论您是否指定类型提示,都将在运行时失败。

但是,您可以为函数参数及其返回类型提供类型提示。例如,def foo(bar: str) -> List[float]暗示bar应该是一个字符串,函数返回一个浮点值列表。如果类型不匹配(在函数中使用形参或返回类型之前),在调用方法时将导致类型检查错误。这个IMOHO在捕获此类错误时比在方法调用中某个地方丢失字段或方法的错误更有帮助。我建议阅读Python官方文档Typing——支持类型提示

此外,如果你使用类型提示,你可以使用静态类型检查器来验证代码的正确性。python内置的一个这样的工具是Mypy (官方文档)。这一部分是关于静态类型检查的文章很好地介绍了如何使用它。