Python 函数如何处理你传入的参数类型?
Posted
技术标签:
【中文标题】Python 函数如何处理你传入的参数类型?【英文标题】:How do Python functions handle the types of parameters that you pass in? 【发布时间】:2011-01-30 04:28:40 【问题描述】:除非我弄错了,否则在 Python 中创建函数的工作方式如下:
def my_func(param1, param2):
# stuff
但是,您实际上并没有给出这些参数的类型。另外,如果我记得,Python 是一种强类型语言,因此,Python 似乎不应该让您传入与函数创建者预期不同类型的参数。然而,Python 是如何知道函数的用户传递了正确的类型呢?假设函数实际使用参数,如果类型错误,程序会死吗?必须指定类型吗?
【问题讨论】:
【参考方案1】:其他答案在解释鸭子打字和the simple answer by 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]
更多关于为什么你应该很少这样做以及你应该做什么将在下一节和 cmets 中讨论。
PEP 3107 不仅提高了代码的可读性,而且还有几个合适的用例,您可以阅读有关here 的信息。
随着PEP 484 的引入,类型注释在 Python 3.5 中得到了更多关注,该模块引入了用于类型提示的标准模块 typing
。
这些类型提示来自类型检查器 mypy (GitHub),现在符合 PEP 484。
typing
模块带有非常全面的类型提示集合,包括:
List
、Tuple
、Set
、Dict
- 分别代表list
、tuple
、set
和dict
。
Iterable
- 对生成器有用。
Any
- 什么时候都可以。
Union
- 可以是指定类型集中的任何内容,而不是 Any
。
Optional
- 当它可能为 None 时。 Union[T, None]
的简写。
TypeVar
- 与泛型一起使用。
Callable
- 主要用于函数,但也可用于其他可调用对象。
这些是最常见的类型提示。完整列表可在documentation for the typing module 中找到。
这是使用打字模块中引入的注释方法的旧示例:
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 代码(例如 Sphinx)可以通过编写格式如下的文档字符串来获得上述某些功能时:
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 中最伟大的特性之一。我等不及人们开始利用它的力量了。很抱歉回答太长了,但是当我兴奋时会发生这种情况。
可以在here 找到大量使用类型提示的 Python 代码示例。
【讨论】:
@rickfoosusa:我怀疑您没有运行添加了该功能的 Python 3。 等一下!如果定义参数和返回类型不会引发TypeError
,那么像单行定义那样使用pick(l: list, index: int) -> int
有什么意义呢?或者我弄错了,我不知道。
@Eray Erdin:这是一个常见的误解,并不是一个坏问题。它可用于文档目的,帮助 IDE 更好地自动完成并通过使用静态分析在运行前发现错误(就像我在答案中提到的 mypy 一样)。希望运行时可以利用这些信息并实际加速程序,但这可能需要很长时间才能实现。您也许还可以创建一个装饰器,为您抛出 TypeErrors(信息存储在函数对象的 __annotations__
属性中)。
@ErdinEray 我应该补充一点,抛出 TypeErrors 是一个坏主意(调试永远不会有趣,无论引发多么好的预期异常)。但是不要害怕,我的回答中描述的新功能的优势可以提供更好的方法:不要依赖运行时的任何检查,使用 mypy 在运行前完成所有操作,或者使用为您进行静态分析的编辑器,例如 PyCharm .
@Tony:当你返回两个或多个对象时,你实际上返回了一个元组,所以你应该使用元组类型注解,即def f(a) -> Tuple[int, int]:
【参考方案2】:
如果有人想指定变量类型,我已经实现了一个包装器。
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'>)
编辑
如果未声明任何参数(或返回)类型,上述代码将不起作用。另一方面,以下编辑可以提供帮助,它仅适用于 kwargs 而不会检查 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
【讨论】:
【参考方案3】:为了有效地使用打字模块(Python 3.5 中的新功能)包括所有 (*
)。
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.
但是,您仍然可以使用 int
、list
、dict
、...等类型名称。
【讨论】:
【参考方案4】:我没有在其他答案中看到这个,所以我将这个添加到锅中。
正如其他人所说,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++ 这样的静态语言,这样的事情将是一场噩梦。
【讨论】:
【参考方案5】:在 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
【讨论】:
【参考方案6】:Python 并不关心你传递给它的函数的是什么。当您调用 my_func(a,b)
时,param1 和 param2 变量将保存 a 和 b 的值。 Python 不知道您正在调用具有正确类型的函数,并希望程序员处理好这一点。如果您的函数将使用不同类型的参数调用,您可以使用 try/except 块包装访问它们的代码,并以您想要的任何方式评估参数。
【讨论】:
Python 没有变量,就像其他语言一样,变量有类型和值;它有 names 指向 objects,它们知道它们的类型。【参考方案7】:Python 是强类型的,因为每个对象都有一个类型,每个对象知道它的类型,不可能意外或故意使用“好像”类型的对象它是一个不同类型的对象,并且对象上的所有基本操作都委托给它的类型。
这与名称无关。 Python 中的 name 没有“类型”:如果定义了名称,则该名称指的是 object,而 object 确实有一个类型(但这实际上并不强制 name 上的类型:一个名字就是一个名字)。
Python 中的名称可以很好地在不同时间引用不同的对象(就像在大多数编程语言中一样,但不是全部)——并且对于名称没有任何限制,例如,如果它曾经引用过X 类型,那么它就永远只能引用 X 类型的其他对象。names 上的约束不是“强类型”概念的一部分,尽管 static的一些爱好者> 打字(其中名称确实受到限制,并且在静态,AKA 编译时,时尚也是如此)确实以这种方式滥用了这个术语。
【讨论】:
所以看起来强类型并不那么强,在这种特殊情况下,它比静态类型弱。恕我直言,名称/变量/引用的编译时类型约束实际上非常重要,因此我大胆声称python 在这方面不如静态类型。如果我错了,请纠正我。 @liang 这是一个观点,所以你不能对或错。这当然也是我的意见,我尝试过很多语言。我无法使用我的 IDE 来找出参数的类型(以及成员)这一事实是 python 的一个主要缺点。这个缺点是否比鸭式打字的优点更重要取决于你问的人。 但这并不能回答任何问题:“但是,Python 怎么知道函数的用户传入了正确的类型?如果类型错误,程序会死吗?假设函数实际使用参数?你必须指定类型吗?或者.. @qPCR4vir,任何对象都可以作为参数传递。如果尝试执行对象不支持的操作,则会发生错误(一个异常,如果程序被编码为捕获它,程序将不会“死”,请参阅try
/except
)。在 Python 3.5 中,您现在可以选择“指定类型”的参数,但如果违反规范,则不会发生错误;键入符号仅用于帮助执行分析等的单独工具,它不会改变 Python 本身的行为。
@AlexMartelli。谢谢!对我来说,这是正确的答案:“错误(一个异常,程序不会“死”,如果它被编码来捕获它,请参阅 try/except)..”【参考方案8】:
在本页值得一提的鸭式打字有一个臭名昭著的例外。
当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 提示我们程序遇到意外类型时应该引发哪个异常。
【讨论】:
【参考方案9】:许多语言都有变量,这些变量具有特定的类型并具有值。 Python没有变量;它有对象,您可以使用名称来引用这些对象。
在其他语言中,当你说:
a = 1
然后一个(通常是整数)变量将其内容更改为值 1。
在 Python 中,
a = 1
表示“使用名称a来指代对象1”。您可以在交互式 Python 会话中执行以下操作:
>>> type(1)
<type 'int'>
函数type
被对象1
调用;因为每个对象都知道它的类型,type
很容易找到所说的类型并返回它。
同样,每当你定义一个函数时
def funcname(param1, param2):
该函数接收两个对象,并将它们命名为param1
和param2
,无论它们的类型如何。如果您想确保接收到的对象是特定类型的,请将您的函数编码为好像它们是所需类型并捕获如果不是则抛出的异常。抛出的异常通常是TypeError
(你使用了无效操作)和AttributeError
(你试图访问一个不存在的成员(方法也是成员))。
【讨论】:
【参考方案10】:作为Alex Martelli explains,
正常的、Pythonic 的首选解决方案几乎总是“鸭子类型”:尝试使用参数,就好像它是某种所需类型一样,在 try/except 语句中执行此操作,捕获所有可能出现的异常,如果参数是实际上不是那种类型(或任何其他类型很好地模仿它;-),并且在 except 子句中,尝试其他东西(使用参数“好像”它是某种其他类型)。
阅读他帖子的其余部分以获取有用的信息。
【讨论】:
【参考方案11】:您没有指定类型。如果该方法尝试访问未在传入的参数上定义的属性,则该方法只会失败(在运行时)。
所以这个简单的函数:
def no_op(param1, param2):
pass
...无论传入什么两个参数都不会失败。
但是,这个函数:
def call_quack(param1, param2):
param1.quack()
param2.quack()
...如果param1
和param2
不具有名为quack
的可调用属性,则会在运行时失败。
【讨论】:
+1:属性和方法不是静态确定的。这种“正确类型”或“错误类型”的概念是如何通过该类型在函数中是否正常工作来建立的。【参考方案12】:你从不指定类型; Python有duck typing的概念;基本上,处理参数的代码将对它们做出某些假设——可能是通过调用参数预期实现的某些方法。如果参数类型错误,则会抛出异常。
一般而言,确保您传递正确类型的对象取决于您的代码 - 没有编译器可以提前强制执行此操作。
【讨论】:
【参考方案13】:在静态或编译时类型检查的意义上,Python 不是强类型。
大多数 Python 代码属于所谓的"Duck Typing" ——例如,你在一个对象上寻找一个方法read
——你不关心对象是磁盘上的文件还是套接字,你只想从中读取 N 个字节。
【讨论】:
Python 是强类型。它也是动态类型的。 但这并不能回答任何问题:“但是,Python 怎么知道函数的用户传入了正确的类型?如果类型错误,程序会死吗?假设函数实际使用参数?你必须指定类型吗?或者..以上是关于Python 函数如何处理你传入的参数类型?的主要内容,如果未能解决你的问题,请参考以下文章