流畅的 Python 第二版(GPT 重译)(八)(2)

简介: 流畅的 Python 第二版(GPT 重译)(八)

流畅的 Python 第二版(GPT 重译)(八)(1)https://developer.aliyun.com/article/1484689

示例 15-10. books.py:带有变量注释的from_json函数。
def from_json(data: str) -> BookDict:
    whatever: BookDict = json.loads(data)  # ①
    return whatever  # ②

当将类型为Any的表达式立即分配给带有类型提示的变量时,--disallow-any-expr不会导致错误。

现在whatever的类型是BookDict,即声明的返回类型。

警告

不要被示例 15-10 的虚假类型安全感所蒙蔽!从静态代码看,类型检查器无法预测json.loads()会返回任何类似于BookDict的东西。只有运行时验证才能保证这一点。

静态类型检查无法防止与本质上动态的代码出现错误,比如json.loads(),它在运行时构建不同类型的 Python 对象,正如示例 15-11、15-12 和 15-13 所展示的。

示例 15-11. demo_not_book.py:from_json返回一个无效的BookDict,而to_xml接受它
from books import to_xml, from_json
from typing import TYPE_CHECKING
def demo() -> None:
    NOT_BOOK_JSON = """
 {"title": "Andromeda Strain",
         "flavor": "pistachio",
         "authors": true}
    """
    not_book = from_json(NOT_BOOK_JSON)  # ①
    if TYPE_CHECKING:  # ②
        reveal_type(not_book)
        reveal_type(not_book['authors'])
    print(not_book)  # ③
    print(not_book['flavor'])  # ④
    xml = to_xml(not_book)  # ⑤
    print(xml)  # ⑥
if __name__ == '__main__':
    demo()

这行代码不会产生有效的BookDict—查看NOT_BOOK_JSON的内容。

让我们揭示一些类型。

这不应该是问题:print可以处理object和其他任何类型。

BookDict没有'flavor'键,但 JSON 源有…会发生什么?

记住签名:def to_xml(book: BookDict) -> str:

XML 输出会是什么样子?

现在我们用 Mypy 检查demo_not_book.py(示例 15-12)。

示例 15-12. demo_not_book.py的 Mypy 报告,为了清晰起见重新格式化
…/typeddict/ $ mypy demo_not_book.py
demo_not_book.py:12: note: Revealed type is
   'TypedDict('books.BookDict', {'isbn': built-ins.str,
                                 'title': built-ins.str,
                                 'authors': built-ins.list[built-ins.str],
                                 'pagecount': built-ins.int})'  # ①
demo_not_book.py:13: note: Revealed type is 'built-ins.list[built-ins.str]'  # ②
demo_not_book.py:16: error: TypedDict "BookDict" has no key 'flavor'  # ③
Found 1 error in 1 file (checked 1 source file)

显式类型是名义类型,而不是not_book的运行时内容。

同样,这是not_book['authors']的名义类型,如BookDict中定义的那样。而不是运行时类型。

这个错误是针对print(not_book['flavor'])这一行的:该键在名义类型中不存在。

现在让我们运行demo_not_book.py,并在示例 15-13 中显示输出。

示例 15-13. 运行 demo_not_book.py 的输出
…/typeddict/ $ python3 demo_not_book.py
{'title': 'Andromeda Strain', 'flavor': 'pistachio', 'authors': True}  # ①
pistachio  # ②
<BOOK>  # ③
        <TITLE>Andromeda Strain</TITLE>
        <FLAVOR>pistachio</FLAVOR>
        <AUTHORS>True</AUTHORS>
</BOOK>

这实际上不是一个 BookDict

not_book['flavor'] 的值。

to_xml 接受一个 BookDict 参数,但没有运行时检查:垃圾进,垃圾出。

示例 15-13 显示 demo_not_book.py 输出了无意义的内容,但没有运行时错误。在处理 JSON 数据时使用 TypedDict 并没有提供太多类型安全性。

如果你通过鸭子类型的视角查看示例 15-8 中to_xml的代码,那么参数book必须提供一个返回类似(key, value)元组可迭代对象的.items()方法,其中:

  • key 必须有一个 .upper() 方法
  • value 可以是任何东西

这个演示的重点是:当处理具有动态结构的数据,比如 JSON 或 XML 时,TypedDict 绝对不能替代运行时的数据验证。为此,请使用pydantic

TypedDict 具有更多功能,包括支持可选键、有限形式的继承以及另一种声明语法。如果您想了解更多,请查看 PEP 589—TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys

现在让我们将注意力转向一个最好避免但有时不可避免的函数:typing.cast

类型转换

没有完美的类型系统,静态类型检查器、typeshed 项目中的类型提示或具有类型提示的第三方包也不是完美的。

typing.cast() 特殊函数提供了一种处理类型检查故障或代码中不正确类型提示的方法。Mypy 0.930 文档解释:

Casts 用于消除杂乱的类型检查器警告,并在类型检查器无法完全理解情况时为其提供一点帮助。

在运行时,typing.cast 绝对不起作用。这是它的实现

def cast(typ, val):
    """Cast a value to a type.
 This returns the value unchanged.  To the type checker this
 signals that the return value has the designated type, but at
 runtime we intentionally don't check anything (we want this
 to be as fast as possible).
 """
    return val

PEP 484 要求类型检查器“盲目相信”cast 中声明的类型。PEP 484 的“Casts”部分提供了一个需要 cast 指导的示例:

from typing import cast
def find_first_str(a: list[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string
    return cast(str, a[index])

对生成器表达式的 next() 调用将返回 str 项的索引或引发 StopIteration。因此,如果没有引发异常,find_first_str 将始终返回一个 str,而 str 是声明的返回类型。

但如果最后一行只是 return a[index],Mypy 将推断返回类型为 object,因为 a 参数声明为 list[object]。因此,需要 cast() 来指导 Mypy。⁷

这里是另一个使用 cast 的示例,这次是为了纠正 Python 标准库中过时的类型提示。在示例 21-12 中,我创建了一个 asyncio Server 对象,并且我想获取服务器正在侦听的地址。我编写了这行代码:

addr = server.sockets[0].getsockname()

但 Mypy 报告了这个错误:

Value of type "Optional[List[socket]]" is not indexable

2021 年 5 月 typeshedServer.sockets 的类型提示对 Python 3.6 是有效的,其中 sockets 属性可以是 None。但在 Python 3.7 中,sockets 变成了一个始终返回 list 的属性,如果服务器没有 sockets,则可能为空。自 Python 3.8 起,getter 返回一个 tuple(用作不可变序列)。

由于我现在无法修复 typeshed,⁸ 我添加了一个 cast,就像这样:

from asyncio.trsock import TransportSocket
from typing import cast
# ... many lines omitted ...
    socket_list = cast(tuple[TransportSocket, ...], server.sockets)
    addr = socket_list[0].getsockname()

在这种情况下使用 cast 需要花费几个小时来理解问题,并阅读 asyncio 源代码以找到正确的 sockets 类型:来自未记录的 asyncio.trsock 模块的 TransportSocket 类。我还必须添加两个 import 语句和另一行代码以提高可读性。⁹ 但代码更安全。

细心的读者可能会注意到,如果 sockets 为空,sockets[0] 可能会引发 IndexError。但就我对 asyncio 的理解而言,在 示例 21-12 中不会发生这种情况,因为 server 在我读取其 sockets 属性时已准备好接受连接,因此它不会为空。无论如何,IndexError 是一个运行时错误。Mypy 甚至在像 print([][0]) 这样的简单情况下也无法发现问题。

警告

不要过于依赖 cast 来消除 Mypy 的警告,因为当 Mypy 报告错误时,通常是正确的。如果你经常使用 cast,那是一个代码异味。你的团队可能在误用类型提示,或者你的代码库中可能存在低质量的依赖项。

尽管存在缺点,cast 也有其有效用途。以下是 Guido van Rossum 关于它的一些观点:

有什么问题,偶尔调用 cast() 或添加 # type: ignore 注释吗?¹⁰

完全禁止使用 cast 是不明智的,特别是因为其他解决方法更糟糕:

  • # type: ignore 提供的信息较少。¹¹
  • 使用 Any 是具有传染性的:由于 Any 与所有类型一致,滥用它可能通过类型推断产生级联效应,削弱类型检查器在代码其他部分检测错误的能力。

当然,并非所有类型错误都可以使用 cast 修复。有时我们需要 # type: ignore,偶尔需要 Any,甚至可以在函数中不留类型提示。

接下来,让我们谈谈在运行时使用注释。

在运行时读取类型提示

在导入时,Python 读取函数、类和模块中的类型提示,并将它们存储在名为 __annotations__ 的属性中。例如,考虑 示例 15-14 中的 clip 函数。¹²

示例 15-14. clipannot.py:clip 函数的带注释签名
def clip(text: str, max_len: int = 80) -> str:

类型提示存储为函数的 __annotations__ 属性中的 dict

>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}

'return' 键映射到 -> 符号后的返回类型提示,在 示例 15-14 中。

请注意,注释在导入时由解释器评估,就像参数默认值也会被评估一样。这就是为什么注释中的值是 Python 类 strint,而不是字符串 'str''int'。注释的导入时评估是 Python 3.10 的标准,但如果 PEP 563PEP 649 成为标准行为,这可能会改变。

运行时的注释问题

类型提示的增加使用引发了两个问题:

  • 当使用许多类型提示时,导入模块会消耗更多的 CPU 和内存。
  • 引用尚未定义的类型需要使用字符串而不是实际类型。

这两个问题都很重要。第一个问题是因为我们刚刚看到的:注释在导入时由解释器评估并存储在 __annotations__ 属性中。现在让我们专注于第二个问题。

有时需要将注释存储为字符串,因为存在“前向引用”问题:当类型提示需要引用在同一模块下定义的类时。然而,在源代码中问题的常见表现根本不像前向引用:当方法返回同一类的新对象时。由于在 Python 完全评估类体之前类对象未定义,类型提示必须使用类名作为字符串。以下是一个示例:

class Rectangle:
    # ... lines omitted ...
    def stretch(self, factor: float) -> 'Rectangle':
        return Rectangle(width=self.width * factor)

将前向引用类型提示写为字符串是 Python 3.10 的标准和必需做法。静态类型检查器从一开始就设计用于处理这个问题。

但在运行时,如果编写代码读取 stretchreturn 注释,你将得到一个字符串 'Rectangle' 而不是实际类型,即 Rectangle 类的引用。现在你的代码需要弄清楚那个字符串的含义。

typing模块包括三个函数和一个分类为内省助手的类,其中最重要的是typing.get_type_hints。其部分文档如下:

get_type_hints(obj, globals=None, locals=None, include_extras=False)

[…] 这通常与obj.__annotations__相同。此外,以字符串文字编码的前向引用通过在globalslocals命名空间中评估来处理。[…]

警告

自 Python 3.10 开始,应该使用新的inspect.get_annotations(…)函数,而不是typing.get_type_hints。然而,一些读者可能尚未使用 Python 3.10,因此在示例中我将使用typing.get_type_hints,自从typing模块在 Python 3.5 中添加以来就可用。

PEP 563—注释的延迟评估已经获得批准,使得不再需要将注释写成字符串,并减少类型提示的运行时成本。其主要思想在“摘要”的这两句话中描述:

本 PEP 建议更改函数注释和变量注释,使其不再在函数定义时评估。相反,它们以字符串形式保留在注释中。

从 Python 3.7 开始,这就是在任何以此import语句开头的模块中处理注释的方式:

from __future__ import annotations

为了展示其效果,我将与顶部的__future__导入行相同的clip函数的副本放在了一个名为clip_annot_post.py的模块中。

在控制台上,当我导入该模块并读取clip的注释时,这是我得到的结果:

>>> from clip_annot_post import clip
>>> clip.__annotations__
{'text': 'str', 'max_len': 'int', 'return': 'str'}

如您所见,所有类型提示现在都是普通字符串,尽管它们在clip的定义中并非作为引号字符串编写(示例 15-14)。

typing.get_type_hints函数能够解析许多类型提示,包括clip中的类型提示:

>>> from clip_annot_post import clip
>>> from typing import get_type_hints
>>> get_type_hints(clip)
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}

调用get_type_hints会给我们真实的类型,即使在某些情况下原始类型提示是作为引号字符串编写的。这是在运行时读取类型提示的推荐方式。

PEP 563 的行为原计划在 Python 3.10 中成为默认行为,无需__future__导入。然而,FastAPIpydantic 的维护者发出警告,称这一变化将破坏依赖运行时类型提示的代码,并且无法可靠使用get_type_hints

在 python-dev 邮件列表上的讨论中,PEP 563 的作者 Łukasz Langa 描述了该函数的一些限制:

[…] 结果表明,typing.get_type_hints()存在一些限制,使得其在一般情况下在运行时成本高昂,并且更重要的是无法解析所有类型。最常见的例子涉及生成类型的非全局上下文(例如,内部类、函数内的类等)。但是,一个前向引用的典型例子是:具有接受或返回其自身类型对象的方法的类,如果使用类生成器,则typing.get_type_hints()也无法正确处理。我们可以做一些技巧来连接这些点,但总体来说并不是很好。¹³

Python 的指导委员会决定将 PEP 563 的默认行为推迟到 Python 3.11 或更高版本,以便开发人员有更多时间提出解决 PEP 563 试图解决的问题的解决方案,而不会破坏运行时类型提示的广泛使用。PEP 649—使用描述符推迟评估注释正在考虑作为可能的解决方案,但可能会达成不同的妥协。

总结一下:截至 Python 3.10,运行时读取类型提示并不是 100%可靠的,可能会在 2022 年发生变化。

注意

在大规模使用 Python 的公司中,他们希望获得静态类型的好处,但不想在导入时评估类型提示的代价。静态检查发生在开发人员的工作站和专用 CI 服务器上,但在生产容器中,模块的加载频率和数量要高得多,这种成本在规模上是不可忽略的。

这在 Python 社区中引发了紧张气氛,一方面是希望类型提示仅以字符串形式存储,以减少加载成本,另一方面是希望在运行时也使用类型提示的人,比如 pydanticFastAPI 的创建者和用户,他们更希望将类型对象存储起来,而不是评估这些注释,这是一项具有挑战性的任务。

处理问题

鉴于目前的不稳定局势,如果您需要在运行时阅读注释,我建议:

  • 避免直接读取__annotations__;而是使用inspect.get_annotations(从 Python 3.10 开始)或typing.get_type_hints(自 Python 3.5 起)。
  • 编写自己的自定义函数,作为inspect.get_annotationstyping.get_type_hints周围的薄包装,让您的代码库的其余部分调用该自定义函数,以便将来的更改局限于单个函数。

为了演示第二点,这里是在 示例 24-5 中定义的Checked类的前几行,我们将在 第二十四章 中学习:

class Checked:
    @classmethod
    def _fields(cls) -> dict[str, type]:
        return get_type_hints(cls)
    # ... more lines ...

Checked._fields 类方法保护模块的其他部分不直接依赖于typing.get_type_hints。如果get_type_hints在将来发生变化,需要额外的逻辑,或者您想用inspect.get_annotations替换它,更改将局限于Checked._fields,不会影响程序的其余部分。

警告

鉴于关于运行时检查类型提示的持续讨论和提出的更改,官方的“注释最佳实践”文档是必读的,并且可能会在通往 Python 3.11 的道路上进行更新。这篇指南是由 Larry Hastings 撰写的,他是 PEP 649—使用描述符延迟评估注释 的作者,这是一个解决由 PEP 563—延迟评估注释 提出的运行时问题的替代提案。

本章的其余部分涵盖了泛型,从如何定义一个可以由用户参数化的泛型类开始。

实现一个通用类

在 示例 13-7 中,我们定义了Tombola ABC:一个类似于宾果笼的接口。来自 示例 13-10 的LottoBlower 类是一个具体的实现。现在我们将研究一个通用版本的LottoBlower,就像在 示例 15-15 中使用的那样。

示例 15-15. generic_lotto_demo.py:使用通用抽奖机类
from generic_lotto import LottoBlower
machine = LottoBlowerint)  # ①
first = machine.pick()  # ②
remain = machine.inspect()  # ③

要实例化一个通用类,我们给它一个实际的类型参数,比如这里的int

Mypy 将正确推断first是一个int

… 而remain是一个整数的元组。

此外,Mypy 还报告了参数化类型的违规情况,并提供了有用的消息,就像 示例 15-16 中显示的那样。

示例 15-16. generic_lotto_errors.py:Mypy 报告的错误
from generic_lotto import LottoBlower
machine = LottoBlowerint
## error: List item 1 has incompatible type "float"; # ①
##        expected "int"
machine = LottoBlowerint)
machine.load('ABC')
## error: Argument 1 to "load" of "LottoBlower" # ②
##        has incompatible type "str";
##        expected "Iterable[int]"
## note:  Following member(s) of "str" have conflicts:
## note:      Expected:
## note:          def __iter__(self) -> Iterator[int]
## note:      Got:
## note:          def __iter__(self) -> Iterator[str]

在实例化LottoBlower[int]时,Mypy 标记了float

在调用.load('ABC')时,Mypy 解释了为什么str不行:str.__iter__返回一个Iterator[str],但LottoBlower[int]需要一个Iterator[int]

示例 15-17 是实现。

示例 15-17. generic_lotto.py:一个通用的抽奖机类
import random
from collections.abc import Iterable
from typing import TypeVar, Generic
from tombola import Tombola
T = TypeVar('T')
class LottoBlower(Tombola, Generic[T]):  # ①
    def __init__(self, items: Iterable[T]) -> None:  # ②
        self._balls = listT
    def load(self, items: Iterable[T]) -> None:  # ③
        self._balls.extend(items)
    def pick(self) -> T:  # ④
        try:
            position = random.randrange(len(self._balls))
        except ValueError:
            raise LookupError('pick from empty LottoBlower')
        return self._balls.pop(position)
    def loaded(self) -> bool:  # ⑤
        return bool(self._balls)
    def inspect(self) -> tuple[T, ...]:  # ⑥
        return tuple(self._balls)

泛型类声明通常使用多重继承,因为我们需要子类化Generic来声明形式类型参数——在本例中为T

__init__中的items参数的类型为Iterable[T],当实例声明为LottoBlower[int]时,变为Iterable[int]

load方法也受到限制。

T的返回类型现在在LottoBlower[int]中变为int

这里没有类型变量。

最后,T设置了返回的tuple中项目的类型。

提示

typing模块文档中的“用户定义的泛型类型”部分很简短,提供了很好的例子,并提供了一些我这里没有涵盖的更多细节。

现在我们已经看到如何实现泛型类,让我们定义术语来谈论泛型。

泛型类型的基本术语

这里有几个我在学习泛型时发现有用的定义:¹⁴

泛型类型

声明有一个或多个类型变量的类型。

例子:LottoBlower[T]abc.Mapping[KT, VT]

形式类型参数

出现在泛型类型声明中的类型变量。

例子:前面例子abc.Mapping[KT, VT]中的KTVT

参数化类型

声明为具有实际类型参数的类型。

例子:LottoBlower[int]abc.Mapping[str, float]

实际类型参数

在声明参数化类型时给定的实际类型。

例子:LottoBlower[int]中的int

下一个主题是如何使泛型类型更灵活,引入协变、逆变和不变的概念。

方差

注意

根据您在其他语言中对泛型的经验,这可能是本书中最具挑战性的部分。方差的概念是抽象的,严谨的表述会使这一部分看起来像数学书中的页面。

在实践中,方差主要与想要支持新的泛型容器类型或提供基于回调的 API 的库作者有关。即使如此,通过仅支持不变容器,您可以避免许多复杂性——这基本上是我们现在在 Python 标准库中所拥有的。因此,在第一次阅读时,您可以跳过整个部分,或者只阅读关于不变类型的部分。

我们首次在“可调用类型的方差”中看到了方差的概念,应用于参数化泛型Callable类型。在这里,我们将扩展这个概念,涵盖泛型集合类型,使用“现实世界”的类比使这个抽象概念更具体。

想象一下学校食堂有一个规定,只能安装果汁分配器。只有果汁分配器是被允许的,因为它们可能提供被学校董事会禁止的苏打水。¹⁵¹⁶

不变的分配器

让我们尝试用一个可以根据饮料类型进行参数化的泛型BeverageDispenser类来模拟食堂场景。请参见例 15-18。

例 15-18. invariant.py:类型定义和install函数
from typing import TypeVar, Generic
class Beverage:  # ①
    """Any beverage."""
class Juice(Beverage):
    """Any fruit juice."""
class OrangeJuice(Juice):
    """Delicious juice from Brazilian oranges."""
T = TypeVar('T')  # ②
class BeverageDispenser(Generic[T]):  # ③
    """A dispenser parameterized on the beverage type."""
    def __init__(self, beverage: T) -> None:
        self.beverage = beverage
    def dispense(self) -> T:
        return self.beverage
def install(dispenser: BeverageDispenser[Juice]) -> None:  # ④
    """Install a fruit juice dispenser."""

BeverageJuiceOrangeJuice形成一个类型层次结构。

简单的TypeVar声明。

BeverageDispenser的类型参数化为饮料的类型。

install是一个模块全局函数。它的类型提示强制执行只有果汁分配器是可接受的规则。

鉴于例 15-18 中的定义,以下代码是合法的:

juice_dispenser = BeverageDispenser(Juice())
install(juice_dispenser)

然而,这是不合法的:

beverage_dispenser = BeverageDispenser(Beverage())
install(beverage_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[Beverage]"
##          expected "BeverageDispenser[Juice]"

任何饮料的分配器都是不可接受的,因为食堂需要专门用于果汁的分配器。

令人惊讶的是,这段代码也是非法的:

orange_juice_dispenser = BeverageDispenser(OrangeJuice())
install(orange_juice_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[OrangeJuice]"
##          expected "BeverageDispenser[Juice]"

专门用于橙汁的分配器也是不允许的。只有BeverageDispenser[Juice]才行。在类型术语中,我们说BeverageDispenser(Generic[T])是不变的,当BeverageDispenser[OrangeJuice]BeverageDispenser[Juice]不兼容时——尽管OrangeJuiceJuice子类型

Python 可变集合类型——如listset——是不变的。来自示例 15-17 的LottoBlower类也是不变的。

一个协变分配器

如果我们想更灵活地建模分配器作为一个通用类,可以接受某种饮料类型及其子类型,我们必须使其协变。示例 15-19 展示了如何声明BeverageDispenser

示例 15-19. covariant.py:类型定义和install函数
T_co = TypeVar('T_co', covariant=True)  # ①
class BeverageDispenser(Generic[T_co]):  # ②
    def __init__(self, beverage: T_co) -> None:
        self.beverage = beverage
    def dispense(self) -> T_co:
        return self.beverage
def install(dispenser: BeverageDispenser[Juice]) -> None:  # ③
    """Install a fruit juice dispenser."""

在声明类型变量时,设置covariant=True_cotypeshed上协变类型参数的常规后缀。

使用T_co来为Generic特殊类进行参数化。

对于install的类型提示与示例 15-18 中的相同。

以下代码有效,因为现在Juice分配器和OrangeJuice分配器都在协变BeverageDispenser中有效:

juice_dispenser = BeverageDispenser(Juice())
install(juice_dispenser)
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
install(orange_juice_dispenser)

但是,任意饮料的分配器也是不可接受的:

beverage_dispenser = BeverageDispenser(Beverage())
install(beverage_dispenser)
## mypy: Argument 1 to "install" has
## incompatible type "BeverageDispenser[Beverage]"
##          expected "BeverageDispenser[Juice]"

这就是协变性:参数化分配器的子类型关系与类型参数的子类型关系方向相同变化。

逆变垃圾桶

现在我们将模拟食堂设置垃圾桶的规则。让我们假设食物和饮料都是用生物降解包装,剩菜剩饭以及一次性餐具也是生物降解的。垃圾桶必须适用于生物降解的废物。

注意

为了这个教学示例,让我们做出简化假设,将垃圾分类为一个整洁的层次结构:

  • 废物是最一般的垃圾类型。所有垃圾都是废物。
  • 生物降解是一种可以随时间被生物分解的垃圾类型。一些废物不是生物降解的。
  • 可堆肥是一种特定类型的生物降解垃圾,可以在堆肥桶或堆肥设施中高效地转化为有机肥料。在我们的定义中,并非所有生物降解垃圾都是可堆肥的。

为了模拟食堂中可接受垃圾桶的规则,我们需要通过一个示例引入“逆变性”概念,如示例 15-20 所示。


流畅的 Python 第二版(GPT 重译)(八)(3)https://developer.aliyun.com/article/1484691

相关文章
|
Linux 数据库 iOS开发
流畅的 Python 第二版(GPT 重译)(二)(4)
流畅的 Python 第二版(GPT 重译)(二)
48 5
|
13天前
|
设计模式 程序员 数据处理
流畅的 Python 第二版(GPT 重译)(九)(1)
流畅的 Python 第二版(GPT 重译)(九)
56 1
|
13天前
|
JavaScript 安全 前端开发
流畅的 Python 第二版(GPT 重译)(六)(1)
流畅的 Python 第二版(GPT 重译)(六)
60 1
|
13天前
|
缓存 算法 Java
流畅的 Python 第二版(GPT 重译)(三)(4)
流畅的 Python 第二版(GPT 重译)(三)
41 4
|
13天前
|
人工智能 安全 程序员
流畅的 Python 第二版(GPT 重译)(一)(3)
流畅的 Python 第二版(GPT 重译)(一)
13 2
|
13天前
|
安全 程序员 API
流畅的 Python 第二版(GPT 重译)(一)(1)
流畅的 Python 第二版(GPT 重译)(一)
97 5
|
13天前
|
机器学习/深度学习 Serverless Python
流畅的 Python 第二版(GPT 重译)(六)(4)
流畅的 Python 第二版(GPT 重译)(六)
12 1
|
13天前
|
设计模式 算法 Java
流畅的 Python 第二版(GPT 重译)(九)(4)
流畅的 Python 第二版(GPT 重译)(九)
34 1
|
13天前
|
存储 设计模式 缓存
流畅的 Python 第二版(GPT 重译)(五)(2)
流畅的 Python 第二版(GPT 重译)(五)
29 1
|
13天前
|
JSON JavaScript Java
流畅的 Python 第二版(GPT 重译)(三)(3)
流畅的 Python 第二版(GPT 重译)(三)
45 5

热门文章

最新文章