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

472 投票
14 回答
876245 浏览
提问于 2025-04-15 20:41

如果我没记错的话,在Python中创建一个函数是这样的:

def my_func(param1, param2):
    # stuff

不过,你并不需要给这些参数指定类型。而且,如果我没记错,Python是一个强类型的语言,这意味着Python应该不允许你传入与函数创建者预期的类型不同的参数。那么,Python是怎么知道使用这个函数的人传入的参数类型是正确的呢?如果传入了错误的类型,程序会崩溃吗,前提是这个函数确实使用了这个参数?你需要指定类型吗?

14 个回答

23

你没有指定类型。这个方法只有在运行时,尝试访问传入参数上没有定义的属性时才会出错。

所以这个简单的函数:

def no_op(param1, param2):
    pass

... 无论传入什么两个参数都不会出错。

但是,这个函数:

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

... 如果 param1param2 都没有名为 quack 的可调用属性,就会在运行时出错。

1120

其他回答已经很好地解释了鸭子类型和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不仅提高了代码的可读性,还有几个合适的使用案例,你可以在这里阅读。


在Python 3.5中,类型注解得到了更多关注,随着PEP 484的引入,提供了一个标准模块typing用于类型提示。

这些类型提示来自类型检查器mypyGitHub),现在已经符合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代码时,可以通过编写格式如下的文档字符串来获得上述一些功能:

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代码示例可以在这里找到。

225

Python 是一种强类型语言,这意味着每个对象都有自己的类型,而且每个对象都知道自己的类型。你不可能不小心或者故意把一种类型的对象当作另一种类型的对象来使用。而且,对象上的所有基本操作都是由它的类型来决定的。

这和名字没有关系。在 Python 中,一个名字并没有“类型”:当你定义一个名字时,它指向的是一个对象,而这个对象是有类型的(但这并不意味着这个名字也有类型:名字就是名字)。

在 Python 中,一个名字可以在不同的时间指向不同的对象(大多数编程语言都是这样,虽然不是全部)——而且这个名字没有限制,如果它曾经指向过一种类型 X 的对象,它就不必永远只指向其他类型 X 的对象。对名字的限制并不是“强类型”这个概念的一部分,尽管一些喜欢静态类型的人(在这种情况下,名字确实会受到限制,而且是在静态的,也就是编译时的方式)有时会错误地使用这个术语。

撰写回答