流畅的 Python 第二版(GPT 重译)(十)(1)

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

第十八章:with、match 和 else 块

上下文管理器可能几乎与子例程本身一样重要。我们只是初步了解了它们。[…] Basic 有一个 with 语句,在许多语言中都有 with 语句。但它们的功能不同,它们都只是做一些非常浅显的事情,它们可以避免重复的点式[属性]查找,但它们不进行设置和拆卸。仅仅因为它们有相同的名称,不要认为它们是相同的东西。with 语句是一件大事。

Raymond Hettinger,Python 雄辩的传道者

本章讨论的是在其他语言中不太常见的控制流特性,因此往往在 Python 中被忽视或未充分利用。它们包括:

  • with 语句和上下文管理器协议
  • 使用 match/case 进行模式匹配
  • forwhiletry 语句中的 else 子句

with 语句建立了一个临时上下文,并可靠地在上下文管理器对象的控制下将其拆除。这可以防止错误并减少样板代码,同时使 API 更安全、更易于使用。Python 程序员发现 with 块除了自动关闭文件外还有许多其他用途。

我们在之前的章节中已经看到了模式匹配,但在这里我们将看到语言的语法如何可以表示为序列模式。这一观察解释了为什么 match/case 是创建易于理解和扩展的语言处理器的有效工具。我们将研究 Scheme 语言的一个小但功能齐全的子集的完整解释器。相同的思想可以应用于开发模板语言或在更大系统中编码业务规则的 DSL(领域特定语言)。

else 子句并不是一件大事,但在与 forwhiletry 一起正确使用时有助于传达意图。

本章新内容

“lis.py 中的模式匹配:案例研究” 是一个新的部分。

我更新了“contextlib 实用工具”,涵盖了自 Python 3.6 以来添加到 contextlib 模块的一些功能,以及 Python 3.10 中引入的新的带括号的上下文管理器语法。

让我们从强大的 with 语句开始。

上下文管理器和 with 块

上下文管理器对象存在以控制 with 语句,就像迭代器存在以控制 for 语句一样。

with 语句旨在简化一些常见的 try/finally 用法,它保证在代码块结束后执行某些操作,即使代码块由 return、异常或 sys.exit() 调用终止。finally 子句中的代码通常释放关键资源或恢复一些临时更改的先前状态。

Python 社区正在为上下文管理器找到新的创造性用途。标准库中的一些示例包括:

上下文管理器接口由 __enter____exit__ 方法组成。在 with 的顶部,Python 调用上下文管理器对象的 __enter__ 方法。当 with 块完成或由于任何原因终止时,Python 调用上下文管理器对象的 __exit__ 方法。

最常见的例子是确保文件对象会关闭。示例 18-1 是使用 with 关闭文件的详细演示。

示例 18-1。文件对象作为上下文管理器的演示
>>> with open('mirror.py') as fp:  # ①
...     src = fp.read(60)  # ②
...
>>> len(src)
60 >>> fp  # ③
<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'> >>> fp.closed, fp.encoding  # ④
(True, 'UTF-8') >>> fp.read(60)  # ⑤
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

fp绑定到打开的文本文件,因为文件的__enter__方法返回self

fp中读取60个 Unicode 字符。

fp变量仍然可用——with块不像函数那样定义新的作用域。

我们可以读取fp对象的属性。

但是我们无法从fp中读取更多文本,因为在with块结束时,调用了TextIOWrapper.__exit__方法,它关闭了文件。

示例 18-1 中的第一个标注点提出了一个微妙但至关重要的观点:上下文管理器对象是在评估with后的表达式的结果,但绑定到目标变量(在as子句中)的值是上下文管理器对象的__enter__方法返回的结果。

恰好open()函数返回TextIOWrapper的一个实例,其__enter__方法返回self。但在不同的类中,__enter__方法也可能返回其他对象,而不是上下文管理器实例。

无论以何种方式退出with块的控制流,__exit__方法都会在上下文管理器对象上调用,而不是在__enter__返回的任何对象上调用。

with语句的as子句是可选的。在open的情况下,我们总是需要它来获得文件的引用,以便我们可以在其上调用方法。但是一些上下文管理器返回None,因为它们没有有用的对象可以返回给用户。

示例 18-2 展示了一个完全轻松的上下文管理器的操作,旨在突出上下文管理器和其__enter__方法返回的对象之间的区别。

示例 18-2. 测试LookingGlass上下文管理器类
>>> from mirror import LookingGlass
    >>> with LookingGlass() as what:  # ①
    ...      print('Alice, Kitty and Snowdrop')  # ②
    ...      print(what)
    ...
    pordwonS dna yttiK ,ecilA
    YKCOWREBBAJ
    >>> what  # ③
    'JABBERWOCKY'
    >>> print('Back to normal.')  # ④
    Back to normal.

上下文管理器是LookingGlass的一个实例;Python 在上下文管理器上调用__enter__,结果绑定到what

打印一个str,然后打印目标变量what的值。每个print的输出都会被反转。

现在with块已经结束。我们可以看到__enter__返回的值,保存在what中,是字符串'JABBERWOCKY'

程序输出不再被反转。

示例 18-3 展示了LookingGlass的实现。

示例 18-3. mirror.py:LookingGlass上下文管理器类的代码
import sys
class LookingGlass:
    def __enter__(self):  # ①
        self.original_write = sys.stdout.write  # ②
        sys.stdout.write = self.reverse_write  # ③
        return 'JABBERWOCKY'  # ④
    def reverse_write(self, text):  # ⑤
        self.original_write(text[::-1])
    def __exit__(self, exc_type, exc_value, traceback):  # ⑥
        sys.stdout.write = self.original_write  # ⑦
        if exc_type is ZeroDivisionError:  # ⑧
            print('Please DO NOT divide by zero!')
            return True  # ⑨
        # ⑩

Python 会以除self之外没有其他参数调用__enter__

保留原始的sys.stdout.write方法,以便稍后恢复。

用我们自己的方法替换sys.stdout.write,进行 Monkey-patch。

返回'JABBERWOCKY'字符串,这样我们就有了一些东西可以放在目标变量what中。

我们替换了sys.stdout.write,将text参数反转并调用原始实现。

如果一切顺利,Python 会用None, None, None调用__exit__;如果引发异常,则三个参数将获得异常数据,如本示例后所述。

恢复原始方法sys.stdout.write

如果异常不是None且其类型是ZeroDivisionError,则打印一条消息…

…并返回True以告诉解释器异常已被处理。

如果__exit__返回None或任何假值,则with块中引发的任何异常都将传播。

提示

当真实应用接管标准输出时,它们通常希望将sys.stdout替换为另一个类似文件的对象一段时间,然后再切换回原始状态。contextlib.redirect_stdout上下文管理器正是这样做的:只需将它传递给将替代sys.stdout的文件类对象。

解释器使用无参数—除了隐式的self—调用__enter__方法。传递给__exit__的三个参数是:

exc_type

异常类(例如ZeroDivisionError)。

exc_value

异常实例。有时,传递给异常构造函数的参数—如错误消息—可以在exc_value.args中找到。

traceback

一个traceback对象。²

要详细了解上下文管理器的工作原理,请参见示例 18-4,其中LookingGlasswith块之外使用,因此我们可以手动调用其__enter____exit__方法。

示例 18-4. 在没有with块的情况下使用LookingGlass
>>> from mirror import LookingGlass
    >>> manager = LookingGlass()  # ①
    >>> manager  # doctest: +ELLIPSIS
    <mirror.LookingGlass object at 0x...>
    >>> monster = manager.__enter__()  # ②
    >>> monster == 'JABBERWOCKY'  # ③
    eurT
    >>> monster
    'YKCOWREBBAJ'
    >>> manager  # doctest: +ELLIPSIS
    >... ta tcejbo ssalGgnikooL.rorrim<
    >>> manager.__exit__(None, None, None)  # ④
    >>> monster
    'JABBERWOCKY'

实例化并检查manager实例。

调用管理器的__enter__方法并将结果存储在monster中。

monster是字符串'JABBERWOCKY'True标识符出现颠倒,因为所有通过stdout输出的内容都经过我们在__enter__中打补丁的write方法。

调用manager.__exit__以恢复先前的stdout.write

Python 3.10 中的括号上下文管理器

Python 3.10 采用了一个新的、更强大的解析器,允许新的语法超出旧的LL(1)解析器所能实现的范围。一个语法增强是允许括号上下文管理器,就像这样:

with (
    CtxManager1() as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):
    ...

在 3.10 之前,我们必须编写嵌套的with块。

标准库包括contextlib包,其中包含用于构建、组合和使用上下文管理器的方便函数、类和装饰器。

contextlib实用程序

在自己编写上下文管理器类之前,请查看 Python 文档中的contextlib—“用于with语句上下文的实用程序”。也许您即将构建的内容已经存在,或者有一个类或一些可调用对象可以让您的工作更轻松。

除了在示例 18-3 之后提到的redirect_stdout上下文管理器之外,Python 3.5 中还添加了redirect_stderr—它的功能与前者相同,但用于指向stderr的输出。

contextlib包还包括:

closing

一个函数,用于从提供close()方法但不实现__enter__/__exit__接口的对象构建上下文管理器。

suppress

一个上下文管理器,用于临时忽略作为参数给出的异常。

nullcontext

一个什么都不做的上下文管理器,用于简化围绕可能不实现合适上下文管理器的对象的条件逻辑。当with块之前的条件代码可能或可能不为with语句提供上下文管理器时,它充当替代品—Python 3.7 中添加。

contextlib模块提供的类和装饰器比刚才提到的装饰器更广泛适用:

@contextmanager

一个装饰器,让您可以从简单的生成器函数构建上下文管理器,而不是创建一个类并实现接口。参见“使用@contextmanager”。

AbstractContextManager

一个正式化上下文管理器接口的 ABC,并通过子类化使得创建上下文管理器类变得更容易——在 Python 3.6 中添加。

ContextDecorator

用于定义基于类的上下文管理器的基类,也可以用作函数修饰符,将整个函数在受控上下文中运行。

ExitStack

一个允许您进入可变数量上下文管理器的上下文管理器。当with块结束时,ExitStack以 LIFO 顺序(最后进入,最先退出)调用堆叠的上下文管理器的__exit__方法。当您不知道在with块中需要进入多少上下文管理器时,请使用此类;例如,当同时打开来自任意文件列表的所有文件时。

在 Python 3.7 中,contextlib添加了AbstractAsyncContextManager@asynccontextmanagerAsyncExitStack。它们类似于名称中不带async部分的等效实用程序,但设计用于与新的async with语句一起使用,该语句在第二十一章中介绍。

这些实用程序中最常用的是@contextmanager修饰符,因此它值得更多关注。该修饰符也很有趣,因为它展示了与迭代无关的yield语句的用法。

使用@contextmanager

@contextmanager修饰符是一个优雅且实用的工具,它将三个独特的 Python 特性结合在一起:函数修饰符、生成器和with语句。

使用@contextmanager减少了创建上下文管理器的样板代码:不需要编写一个具有__enter__/__exit__方法的整个类,只需实现一个生成器,其中包含一个应该生成__enter__方法返回的内容。

在使用@contextmanager修饰的生成器中,yield将函数体分为两部分:yield之前的所有内容将在解释器调用__enter__时在with块的开头执行;yield之后的代码将在块结束时调用__exit__时运行。

示例 18-5 用生成器函数替换了示例 18-3 中的LookingGlass类。

示例 18-5. mirror_gen.py:使用生成器实现的上下文管理器
import contextlib
import sys
@contextlib.contextmanager  # ①
def looking_glass():
    original_write = sys.stdout.write  # ②
    def reverse_write(text):  # ③
        original_write(text[::-1])
    sys.stdout.write = reverse_write  # ④
    yield 'JABBERWOCKY'  # ⑤
    sys.stdout.write = original_write  # ⑥

应用contextmanager修饰符。

保留原始的sys.stdout.write方法。

reverse_write稍后可以调用original_write,因为它在其闭包中可用。

sys.stdout.write替换为reverse_write

产生将绑定到with语句中as子句的目标变量的值。生成器在此处暂停,而with的主体执行。

当控制退出with块时,执行继续在yield之后;这里恢复原始的sys.stdout.write

示例 18-6 展示了looking_glass函数的运行。

示例 18-6. 测试looking_glass上下文管理器函数
>>> from mirror_gen import looking_glass
    >>> with looking_glass() as what:  # ①
    ...      print('Alice, Kitty and Snowdrop')
    ...      print(what)
    ...
    pordwonS dna yttiK ,ecilA
    YKCOWREBBAJ
    >>> what
    'JABBERWOCKY'
    >>> print('back to normal')
    back to normal

与示例 18-2 唯一的区别是上下文管理器的名称:looking_glass而不是LookingGlass

contextlib.contextmanager修饰符将函数包装在一个实现__enter____exit__方法的类中。³

该类的__enter__方法:

  1. 调用生成器函数以获取生成器对象——我们将其称为gen
  2. 调用next(gen)来驱动它到yield关键字。
  3. 返回由next(gen)产生的值,以允许用户将其绑定到with/as形式中的变量。

with块终止时,__exit__方法:

  1. 检查是否将异常作为exc_type传递;如果是,则调用gen.throw(exception),导致异常在生成器函数体内的yield行中被引发。
  2. 否则,调用next(gen),恢复yield后生成器函数体的执行。

示例 18-5 存在一个缺陷:如果在with块的主体中引发异常,Python 解释器将捕获它并在looking_glass内的yield表达式中再次引发它。但那里没有错误处理,因此looking_glass生成器将在不恢复原始sys.stdout.write方法的情况下终止,使系统处于无效状态。

示例 18-7 添加了对ZeroDivisionError异常的特殊处理,使其在功能上等同于基于类的示例 18-3。

示例 18-7. mirror_gen_exc.py:基于生成器的上下文管理器实现异常处理,与示例 18-3 具有相同的外部行为
import contextlib
import sys
@contextlib.contextmanager
def looking_glass():
    original_write = sys.stdout.write
    def reverse_write(text):
        original_write(text[::-1])
    sys.stdout.write = reverse_write
    msg = ''  # ①
    try:
        yield 'JABBERWOCKY'
    except ZeroDivisionError:  # ②
        msg = 'Please DO NOT divide by zero!'
    finally:
        sys.stdout.write = original_write  # ③
        if msg:
            print(msg)  # ④

为可能的错误消息创建一个变量;这是与示例 18-5 相关的第一个更改。

处理ZeroDivisionError,设置一个错误消息。

撤消对sys.stdout.write的猴子补丁。

如果已设置错误消息,则显示错误消息。

回想一下,__exit__方法告诉解释器已通过返回一个真值处理了异常;在这种情况下,解释器会抑制异常。另一方面,如果__exit__没有显式返回一个值,解释器会得到通常的None,并传播异常。使用@contextmanager,默认行为被反转:装饰器提供的__exit__方法假定任何发送到生成器中的异常都已处理并应该被抑制。

提示

yield周围有一个try/finally(或with块)是使用@contextmanager的不可避免的代价,因为你永远不知道你的上下文管理器的用户会在with块内做什么。⁴

@contextmanager的一个鲜为人知的特性是,用它装饰的生成器也可以作为装饰器使用。⁵ 这是因为@contextmanager是使用contextlib.ContextDecorator类实现的。

示例 18-8 展示了从示例 18-5 中使用的looking_glass上下文管理器作为装饰器。

示例 18-8. looking_glass上下文管理器也可以作为装饰器使用
>>> @looking_glass()
    ... def verse():
    ...     print('The time has come')
    ...
    >>> verse()  # ①
    emoc sah emit ehT
    >>> print('back to normal')  # ②
    back to normal

looking_glassverse主体运行之前和之后执行其工作。

这证实了原始的sys.write已经恢复。

将示例 18-8 与示例 18-6 进行对比,在其中looking_glass被用作上下文管理器。

@contextmanager在标准库之外的一个有趣的现实生活示例是 Martijn Pieters 的使用上下文管理器进行原地文件重写。示例 18-9 展示了它的使用方式。

示例 18-9. 用于原地重写文件的上下文管理器
import csv
with inplace(csvfilename, 'r', newline='') as (infh, outfh):
    reader = csv.reader(infh)
    writer = csv.writer(outfh)
    for row in reader:
        row += ['new', 'columns']
        writer.writerow(row)

inplace函数是一个上下文管理器,为您提供两个句柄—在示例中为infhoutfh—指向同一个文件,允许您的代码同时读取和写入。它比标准库的fileinput.input函数更容易使用(顺便说一句,它也提供了一个上下文管理器)。

如果您想研究 Martijn 的inplace源代码(列在帖子中),请查找yield关键字:在它之前的所有内容都涉及设置上下文,这包括创建备份文件,然后打开并生成可读和可写文件句柄的引用,这些引用将由__enter__调用返回。yield后的__exit__处理关闭文件句柄,并在出现问题时从备份中恢复文件。

这结束了我们对with语句和上下文管理器的概述。让我们转向完整示例中的match/case

lis.py 中的模式匹配:一个案例研究

在“解释器中的模式匹配序列”中,我们看到了从 Peter Norvig 的lis.py解释器的evaluate函数中提取的序列模式的示例,该解释器已移植到 Python 3.10。在本节中,我想更广泛地介绍lis.py的工作原理,并探讨evaluate的所有case子句,不仅解释模式,还解释解释器在每个case中的操作。

除了展示更多的模式匹配,我写这一部分有三个原因:

  1. Norvig 的lis.py是惯用 Python 代码的一个很好的例子。
  2. Scheme 的简单性是语言设计的典范。
  3. 了解解释器如何工作让我更深入地理解了 Python 和一般编程语言——无论是解释型还是编译型。

在查看 Python 代码之前,让我们稍微了解一下 Scheme,这样您就可以理解这个案例研究——如果您以前没有见过 Scheme 或 Lisp 的话。

Scheme 语法

在 Scheme 中,表达式和语句之间没有区别,就像我们在 Python 中所看到的那样。此外,没有中缀运算符。所有表达式都使用前缀表示法,如(+ x 13)而不是x + 13。相同的前缀表示法用于函数调用—例如,(gcd x 13)—和特殊形式—例如,(define x 13),我们在 Python 中会写成赋值语句x = 13。Scheme 和大多数 Lisp 方言使用的表示法称为S 表达式。⁶

示例 18-10 展示了 Scheme 中的一个简单示例。

示例 18-10. Scheme 中的最大公约数
(define (mod m n)
    (- m (* n (quotient m n))))
(define (gcd m n)
    (if (= n 0)
        m
        (gcd n (mod m n))))
(display (gcd 18 45))

示例 18-10 展示了三个 Scheme 表达式:两个函数定义—modgcd—以及一个调用display,它将输出 9,即(gcd 18 45)的结果。示例 18-11 是相同的 Python 代码(比递归欧几里德算法的英文解释更短)。

示例 18-11. 与示例 18-10 相同,用 Python 编写
def mod(m, n):
    return m - (m // n * n)
def gcd(m, n):
    if n == 0:
        return m
    else:
        return gcd(n, mod(m, n))
print(gcd(18, 45))

在惯用 Python 中,我会使用%运算符而不是重新发明mod,并且使用while循环而不是递归会更有效率。但我想展示两个函数定义,并尽可能使示例相似,以帮助您阅读 Scheme 代码。

Scheme 没有像 Python 中那样的迭代控制流命令,如whilefor。迭代是通过递归完成的。请注意,在 Scheme 和 Python 示例中没有赋值。广泛使用递归和最小使用赋值是函数式编程的标志。⁷

现在让我们回顾一下 Python 3.10 版本的lis.py代码。完整的源代码和测试位于 GitHub 存储库fluentpython/example-code-2e的*18-with-match/lispy/py3.10/*目录中。

导入和类型

[示例 18-12 显示了lis.py的前几行。TypeAlias|类型联合运算符的使用需要 Python 3.10。

示例 18-12. lis.py:文件顶部
import math
import operator as op
from collections import ChainMap
from itertools import chain
from typing import Any, TypeAlias, NoReturn
Symbol: TypeAlias = str
Atom: TypeAlias = float | int | Symbol
Expression: TypeAlias = Atom | list

定义的类型有:

Symbol

str的别名。在lis.py中,Symbol用于标识符;没有带有切片、分割等操作的字符串数据类型。⁸

Atom

一个简单的句法元素,如数字或 Symbol—与由不同部分组成的复合结构相对,如列表。

表达式

Scheme 程序的构建块是由原子和列表组成的表达式,可能是嵌套的。

解析器

Norvig 的解析器是 36 行代码,展示了 Python 的强大之处,应用于处理 S-表达式的简单递归语法,没有字符串数据、注释、宏和标准 Scheme 的其他特性,这些特性使解析变得更加复杂(示例 18-13)。

示例 18-13. lis.py:主要解析函数
def parse(program: str) -> Expression:
    "Read a Scheme expression from a string."
    return read_from_tokens(tokenize(program))
def tokenize(s: str) -> list[str]:
    "Convert a string into a list of tokens."
    return s.replace('(', ' ( ').replace(')', ' ) ').split()
def read_from_tokens(tokens: list[str]) -> Expression:
    "Read an expression from a sequence of tokens."
    # more parsing code omitted in book listing

该组的主要函数是 parse,它接受一个 S-表达式作为 str 并返回一个 Expression 对象,如 示例 18-12 中定义的:一个 Atom 或一个可能包含更多原子和嵌套列表的 list

Norvig 在 tokenize 中使用了一个聪明的技巧:他在输入的每个括号前后添加空格,然后拆分它,结果是一个包含 '('')' 作为单独标记的句法标记列表。这个快捷方式有效,因为在 lis.py 的小 Scheme 中没有字符串类型,所以每个 '('')' 都是表达式分隔符。递归解析代码在 read_from_tokens 中,这是一个 14 行的函数,你可以在 fluentpython/example-code-2e 仓库中阅读。我会跳过它,因为我想专注于解释器的其他部分。

这里是从 lispy/py3.10/examples_test.py 中提取的一些 doctest:

>>> from lis import parse
>>> parse('1.5')
1.5
>>> parse('ni!')
'ni!'
>>> parse('(gcd 18 45)')
['gcd', 18, 45]
>>> parse('''
... (define double
...     (lambda (n)
...         (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]

这个 Scheme 子集的解析规则很简单:

  1. 看起来像数字的标记被解析为 floatint
  2. 其他任何不是 '('')' 的内容都被解析为 Symbol—一个可用作标识符的 str。这包括 Scheme 中有效但不在 Python 中的标识符的源文本,如 +set!make-counter
  3. '('')' 内的表达式被递归解析为包含原子的列表或可能包含原子和更多嵌套列表的嵌套列表。

使用 Python 解释器的术语,parse 的输出是一个 AST(抽象语法树):一个方便的表示 Scheme 程序的嵌套列表形成树状结构,其中最外层列表是主干,内部列表是分支,原子是叶子(图 18-1)。

图 18-1. 一个 Scheme lambda 表达式,表示为源代码(具体语法)、作为树和作为 Python 对象序列(抽象语法)。

流畅的 Python 第二版(GPT 重译)(十)(2)https://developer.aliyun.com/article/1484733

相关文章
|
6天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
44 0
|
6天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
21 0
|
6天前
|
人工智能 JSON 机器人
【Chat GPT】用 ChatGPT 运行 Python
【Chat GPT】用 ChatGPT 运行 Python
|
6天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
6天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
29 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
39 0
|
6天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
6天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
69 0
|
6天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
126 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
6天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
68 4