Python中是否实际存在联合类型?

64

由于Python是动态类型的,因此我们当然可以做出这样的事情:

def f(x):
    return 2 if x else "s"

但这是否是 Python 实际上预期的使用方式呢?换句话说,联合类型是否以它们在 Racket 中的方式存在?还是我们只是像这样使用它们:

def f(x):
    if x:
        return "s"

我们唯一需要的“联合”是与None吗?


2
请澄清一下,您是指来自Typed Racket的联合类型吗?Python没有类似的东西。 - John Y
7个回答

74

只有在使用静态类型语言时,才需要使用Union Typing,因为你需要声明一个对象可以返回多种类型之一(例如intstr,或者在另一个例子中是strNoneType)。

Python只处理对象,所以根本不需要考虑“union types”。Python函数返回什么就是什么,如果程序员希望针对不同的结果返回不同的类型,那就是他们的选择。这个选择是架构的选择,并不会对Python解释器产生影响(因此这里没有什么可以“基准测试”的东西)。

Python 3.5引入了创建可选类型提示的标准,该标准包括Union[...]Optional[...]注释。类型提示在运行时外添加可选的静态类型检查,就像TypeScript中的类型不属于JavaScript运行时一样。


2
谢谢!我知道Python不是静态类型的。但我想知道,在实践中,是否有必要编写一个函数根据参数返回多种类型,或者在Python中总有一种方法可以避免这种情况? - Lana
6
@Lana: 再次强调,函数返回值是软件架构设计的选择。保持一致性和限制返回内容是良好的实践,但是“绕过这种情况”的方法只是使用良好的软件工程实践。例如,如果您的函数可以返回 True、False、None 或整数,则需要重新思考函数设计。 - Martijn Pieters
2
@joel:Python类型提示是静态类型,添加到Python中。typing.Union不是运行时类型。 - Martijn Pieters
3
到了2022年,使用Python 3.10+,我们可以使用“|”作为类型联合运算符。https://docs.python.org/3.10/whatsnew/3.10.html#pep-604-new-type-union-operator - Ziwon
4
Python 中存在运行时类型检查器。我知道这些,因为我是其中之一的作者(https://github.com/beartype/beartype)。typing.Union 绝对是一种可以在运行时理解、反思和推断的运行时东西。实际上,运行时类型检查器比静态类型检查器具有深远的优势:即它们错误报警的次数要少得多。运行时类型检查器消除了“# type: ignore”垃圾邮件的需求,同时增加了对类型检查的信心和信任。这些都是好事情。 - Cecil Curry
显示剩余4条评论

36

由于Python是一种动态类型语言,因此类型本身并不存在,但在更新的Python版本中,联合类型是用于类型提示的选项之一。

from typing import Union,TypeVar

T = TypeVar('T')
def f(x: T) -> Union[str, None]:
    if x:
        return "x"

你可以使用它来注释你的代码,从而实现IDE/编辑器级别的语法检查。

2
能否解释一下 T = TypeVar('T') 的含义? - Alen Paul Varghese
@AlenPaulVarghese 刚刚阅读了手册:https://docs.python.org/3/library/typing.html#typing.TypeVar - Sajuuk
6
T = TypeVar('T') 生成了一个具名泛型。该答案提供的方法接受任何输入,并在所提供的内容不是 None 时返回字符串 "x"。这里使用具名泛型完全没有必要,但我建议您研究一下它们,因为它们允许创建非常有用的模板函数。 - WebWanderer
除了语法验证之外,对于进行单元测试开发的人来说,这些工具在发布前非常有用! - Elysiumplain

30

注意: 如其他人所提到的,Python类型提示(默认情况下)对运行时行为没有任何影响,它用于静态分析等。

从Python 3.10开始,您可以使用|符号表示联合类型。以下是来自Python 3.10新特性的示例:

def square(number: int | float) -> int | float:
    return number ** 2

# Instead of 
def square(number: Union[int, float]) -> Union[int, float]:
    return number ** 2

此外,如果您正在使用Python 3.7+,则可以通过使用__future__包来使用该功能,但有一些限制:

from __future__ import annotations

# Works in Python 3.7+
def square(number: int | float) -> int | float:
    return number ** 2

# Works only in Python 3.10+
isinstance(3.10, int | float)
numeric = int | float

更多信息请参阅Union Types文档PEP 604


13
以下是处理需要在Python中使用tagged union/sum type的用例的几个选项:
  • Enum + Tuples

    from enum import Enum
    Token = Enum('Token', ['Number', 'Operator', 'Identifier', 'Space', 'Expression'])
    
    (Token.Number, 42)                            # int
    (Token.Operator, '+')                         # str
    (Token.Identifier, 'foo')                     # str
    (Token.Space, )                               # void
    (Token.Expression, ('lambda', 'x', 'x+x'))    # tuple[str]
    

    A slight variation on this uses a dedicated SumType class instead of a tuple:

    from dataclasses import dataclass
    from typing import Any
    
    @dataclass
    class SumType:
        enum: Enum
        data: Any
    
    SumType(Token.Number, 42)
    
  • isinstance

    if isinstance(data, int):
        ...
    if isinstance(data, str):
        ...
    

    Or in combination with the "enum" idea from above:

    token = SumType(Token.Number, 42)
    
    if token.enum == Token.Number:
        ...
    
  • sumtypes module

当然,这些方法都有各自的缺点。


4

之前的回答没有讨论到一种用例:从现有类型中构建一个 union 类型,并要求 isinstance() 将任何现有类型的实例视为 union 类型的实例。

Python 通过 抽象基类 支持此功能。例如:

>>> import abc
>>> class IntOrString(abc.ABC): pass
... 
>>> IntOrString.register(int)
<class 'int'>
>>> IntOrString.register(str)
<class 'str'>

现在,intstr可以被看作是IntOrString的子类:
>>> issubclass(int, IntOrString)
True
>>> isinstance(42, IntOrString)
True
>>> isinstance("answer", IntOrString)
True

1

补充@MartijnPieters的答案:

但这是Python实际上要使用的方式吗?

在任何语言中,根据参数返回不同类型永远都不是一个好习惯。这使得测试、维护和扩展代码变得非常困难,并且在我看来是一种反模式(但当然有时是必要的恶)。结果至少应该通过具有共同接口来相关联。

union被引入到C中唯一的原因是性能提升。但是在Python中,由于语言的动态性质(如Martijn所指出的),您不会获得这种性能提升。实际上,引入union会降低性能,因为union的大小始终是最大成员的大小。因此,Python永远不会像C一样拥有union


1
谢谢!这正是我想知道的。在Python中,什么情况下使用union是必要的恶习?当我们谈论“联合”时,是否指的是none的联合?(我在Python中注意到了很多)还是不同类型之间的联合?我想知道是否有任何示例代码可以展示这一点。 - Lana
2
请注意,我认为OP所说的不是C语言中的联合体。我更倾向于他们考虑的是Java或C#类型系统。 - Martijn Pieters
2
@Lana,正如Martijn所指出的那样,json.loads()是一个必要的恶。使用None的“联合”是一种普遍的做法,但在我看来也应该避免。特别是在更大的项目中,你无法停止阅读这些“NoneType object has no attribute xxx”的日志。我的个人观点是:一个函数==一个返回类型。 - freakish
1
@Lana:并不是说json.loads()正好是你要找的。更确切地说,json.loads()返回任意类型,包括那些你作为程序员甚至没有想到过的类型。因此,你甚至不能原则上定义一个联合类型来覆盖它可以返回的所有内容。 - John Y
8
请参阅标记联合/求和类型。它们在应用上与“C联合”大不相同。像Haskell和Rust这样的强大的静态类型语言广泛使用它们。 - Mateen Ulhaq
显示剩余3条评论

1

根据Python 3.10的更新,我们可以通过使用“|”将对象类型分隔来在Python中创建联合类型。

例如:

def method(mobject: int | str) -> int | str | None:
   pass

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