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

466

除非我弄错了,创建Python函数的方式如下:

def my_func(param1, param2):
    # stuff

然而,您实际上没有给出这些参数的类型。此外,如果我没记错的话,Python是一种强类型语言,因此看起来Python不应该允许您传递与函数创建者预期的不同类型的参数。但是,Python如何知道函数的用户是否传递了正确的类型?如果类型不正确,程序会崩溃吗?您必须指定类型吗?

14个回答

1103

其他答案已经对鸭子类型进行了很好的解释,以及tzot提供的简单答案

Python不像其他语言中变量拥有类型和值,它有指向对象的名称,对象知道自己的类型。

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

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

这里我们可以看到pick需要 2 个参数,一个是列表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不仅可以提高代码可读性,还有几个适合的用例,您可以在此处阅读有关详细信息。


在Python 3.5中,类型注释引起了更多的关注,这得益于PEP 484的引入,该规范介绍了一个标准模块typing用于类型提示。

这些类型提示来自类型检查器mypy(GitHub),现在符合PEP 484规范。

typing模块提供了一系列相当全面的类型提示,包括:

  • ListTupleSetDict - 分别用于 listtuplesetdict
  • Iterable - 用于生成器。
  • Any - 表示可以是任何类型。
  • Union - 表示可以是指定类型集合中的任何一种类型,与 Any 不同。
  • Optional - 表示可能是 None。等同于 Union[T, None]
  • TypeVar - 用于泛型。
  • Callable - 主要用于函数,但也可用于其他可调用对象。

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

以下是使用 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来变得更加精确,但由于我认为已经在答案中填充了太多关于类型提示启用的新奇功能的信息,因此这留给读者作为一个练习。

以前,当使用 Sphinx 等工具记录Python代码时,可以通过编写格式如下的docstrings来获得上述某些功能:

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 结合使用时,正如我们所见,它提供了一个标准模块,定义了这些类型提示/注释的语法,可以以一种无歧义和精确的方式使用,同时又具有灵活性,从而形成强大的组合。
在我个人的观点中,这是 Python 中最伟大的功能之一。我迫不及待地等待人们开始利用它的力量。对于这个长答案,我感到抱歉,但这就是我兴奋时的情况。

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


2
@rickfoosusa:我怀疑你没有运行Python 3,因为该功能是在Python 3中添加的。 - erb
49
等一下!如果定义参数和返回类型不会引发 TypeError 错误,那么使用像“一行定义”一样的 pick(l: list, index: int) -> int 有什么意义呢?或者我理解错了,我不知道。 - Eray Erdin
40
@Eray Erdin:这是一个常见的误解,不是一个坏问题。它可以用于文档记录,帮助IDE更好地自动完成,并通过使用静态分析(就像我在答案中提到的mypy一样)在运行时找到错误。希望运行时可以利用这些信息,实际上加速程序,但这可能需要很长时间才能得到实现。您可能还可以创建一个装饰器来为您抛出TypeErrors(信息存储在函数对象的__annotations__属性中)。 - erb
2
@ErdinEray 我应该补充一点,抛出TypeError是一个不好的主意(无论异常如何引起,调试都不是一件有趣的事情)。但是不要担心,我回答中描述的新功能的优势使得有更好的方法:不要依赖运行时的任何检查,在运行之前使用mypy做所有的事情,或者使用像PyCharm这样为您执行静态分析的编辑器。 - erb
9
当你返回两个或更多对象时,实际上返回的是一个元组(tuple),因此应该使用元组类型注释,即 def f(a) -> Tuple[int, int]: - erb
显示剩余11条评论

223

Python是强类型的,因为每个对象都有一个类型,每个对象都知道自己的类型,无法意外或故意使用一个类型的对象“伪装”成另一种类型的对象,并且所有基本操作都委托给其类型。

这与名称无关。在Python中,名称没有“类型”:当定义名称时,名称引用一个对象,而对象确实具有类型(但这实际上不会将类型强制到名称上:名称只是名称)。

在Python中,名称可以完全在不同时间引用不同的对象(与大多数编程语言一样,虽然不是所有编程语言都是如此),并且名称上并没有约束,如果它曾经引用过X类型的对象,则永远被限制仅引用其他X类型的对象。对于名称的约束不属于“强类型”的概念,尽管某些静态类型的爱好者(其中名称确实受到约束,而且是以静态,即编译时方式)会误用这个术语。


94
在这种情况下,强类型似乎并不那么强大,它比静态类型更弱。在我看来,对于名称/变量/引用的编译时类型约束实际上非常重要,因此我大胆地声明,在这方面,Python不如静态类型语言。如果我错了,请纠正我。 - liang
30
@liang 这只是一种观点,你不能说对或错。这也是我的观点,我尝试过很多语言。无法使用我的IDE查找参数类型(以及成员变量)是Python的主要缺点。如果这个缺点比鸭子类型的优点更重要,则取决于被问者个人的看法。 - Maarten Bodewes
7
但是这并没有回答以下问题:“然而,Python 如何知道函数的使用者传入了正确的类型?如果参数类型不正确,程序会崩溃吗?需要指定类型吗?” - qPCR4vir
7
@qPCR4vir,任何对象都可以作为参数传递。如果尝试执行该对象不支持的操作,则会发生错误(异常)。如果编码时使用了try/except语句,则程序不会“死亡”。在Python 3.5中,您现在可以选择“指定参数类型”,但是如果违反了规范,不会出现实际上的错误;类型注释只是用于帮助分离执行分析等操作的工具,它不会改变Python本身的行为。 - Alex Martelli
2
在大多数编程语言中,静态类型会给你一种虚假的安全感:“我让编译器满意了,所以它一定是正确的……”然后你就得花个周末来修复空指针异常。如果有一个足够强大的类型系统能够捕捉到人们实际上犯的错误(这在 Haskell 中基本属实,在 Rust 和 Swift 中也大体如此,但在 C++、Java、Go 等语言中则完全不属实),那么静态类型可以使代码更加安全。如果一个类型系统中有一半的程序都是动态转换,那么它只是在愚弄你,让你没有意识到你的代码有多不安全。 - abarnert
显示剩余15条评论

23

您没有指定类型。该方法只会在运行时尝试访问未在传递的参数上定义的属性时失败。

因此,这个简单的函数:

def no_op(param1, param2):
    pass

无论传入什么两个参数,该函数都不会失败。

然而,这个函数:

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

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


1
+1:属性和方法不是静态确定的。如何确定“正确类型”或“错误类型”的概念取决于该类型在函数中是否正常工作。 - S.Lott

16

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

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


14
许多编程语言都有变量,它们属于特定类型并且具有值。但是Python不是这样的,它有对象,并且你使用名称来引用这些对象。
在其他编程语言中,当你说:
a = 1

通常情况下,一个整数变量的值会改变为1。

在Python中,

a = 1

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

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

函数type被调用并传入对象1; 由于每个对象都知道自己的类型,所以type很容易找到该类型并返回它。

同样地,当你定义一个函数时

def funcname(param1, param2):
函数接收两个对象,并将它们命名为 param1param2,无论它们的类型是什么。如果您希望确保所接收的对象是特定类型,请编写函数,就好像它们是所需类型,并捕获抛出的异常(如果它们不是所需类型)。通常抛出的异常是 TypeError(使用了无效操作)和 AttributeError(尝试访问不存在的成员(方法也是成员))。

Python有变量:它们不是存储值的命名内存位置,而是指向值的名称。 - chepner
“...并且您使用名称来引用这些对象”,是的。 - tzot

9
Python在静态或编译时类型检查的意义上并不是强类型语言。大多数Python代码属于所谓的"鸭子类型"--例如,你在一个对象上查找方法read--你不关心这个对象是磁盘上的文件还是套接字,你只想从中读取N个字节。

26
Python是强类型的。同时它也是动态类型的。 - Daniel Newby
2
但这并没有回答任何问题:“然而,Python如何知道函数的用户是否传递了正确的类型?如果是错误的类型,程序会死掉吗?假设函数实际上使用了参数,你必须指定类型吗?” - qPCR4vir

8
正如Alex Martelli所解释的

正常的、Pythonic的、首选的解决方案几乎总是“鸭子类型”:尝试将参数用作某种期望的类型,使用try/except语句执行此操作,捕获可能出现的所有异常,如果参数实际上不是该类型(或任何其他很好地模仿它的类型),则在except子句中尝试其他内容(将参数“视为”其他类型)。

阅读他的帖子以获取有用的信息。

5

Python不会在意你传递给它的函数参数类型。当你调用my_func(a,b)时,param1和param2变量将保存a和b的值。Python并不知道你使用了正确的参数类型,并期望程序员自己处理。如果你的函数将被传入不同类型的参数,你可以使用try/except块包装访问它们的代码,并以任何你想要的方式评估这些参数。


13
Python没有变量,就像其他语言中的变量具有类型和值一样;它有指向对象的“名称”,这些对象知道它们的类型。 - tzot

2
您从未指定类型;Python 有 鸭子类型 的概念;基本上,处理参数的代码将对它们进行某些假设 - 可能通过调用期望参数实现的某些方法。如果参数的类型错误,则会抛出异常。
通常情况下,由您的代码来确保传递正确类型的对象 - 没有编译器可以提前强制执行这一点。

2

在这个页面中,有一个著名的例外不符合鸭子类型的规则。

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在暗示我们,当程序遇到意外类型时应该引发哪种异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接