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

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

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

环境

Environment 类扩展了 collections.ChainMap,添加了一个 change 方法来更新链式字典中的值,ChainMap 实例将这些值保存在映射列表中:self.maps 属性。change 方法用于支持后面描述的 Scheme (set! …) 形式;参见 示例 18-14。

示例 18-14. lis.pyEnvironment
class Environment(ChainMap[Symbol, Any]):
    "A ChainMap that allows changing an item in-place."
    def change(self, key: Symbol, value: Any) -> None:
        "Find where key is defined and change the value there."
        for map in self.maps:
            if key in map:
                map[key] = value  # type: ignore[index]
                return
        raise KeyError(key)

注意,change 方法只更新现有键。⁹ 尝试更改未找到的键会引发 KeyError

这个 doctest 展示了 Environment 的工作原理:

>>> from lis import Environment
>>> inner_env = {'a': 2}
>>> outer_env = {'a': 0, 'b': 1}
>>> env = Environment(inner_env, outer_env)
>>> env['a']  # ①
2 >>> env['a'] = 111  # ②
>>> env['c'] = 222
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) >>> env.change('b', 333)  # ③
>>> env
Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333})

在读取值时,Environment 的工作方式类似于 ChainMap:键从左到右在嵌套映射中搜索。这就是为什么 outer_enva 的值被 inner_env 中的值遮蔽。

使用 [] 赋值会覆盖或插入新项,但始终在第一个映射 inner_env 中进行,本例中。

env.change('b', 333) 寻找 'b' 键并在 outer_env 中就地分配一个新值。

接下来是 standard_env() 函数,它构建并返回一个加载了预定义函数的 Environment,类似于 Python 的 __builtins__ 模块,始终可用(示例 18-15)。

示例 18-15. lis.py:standard_env()构建并返回全局环境
def standard_env() -> Environment:
    "An environment with some Scheme standard procedures."
    env = Environment()
    env.update(vars(math))   # sin, cos, sqrt, pi, ...
    env.update({
            '+': op.add,
            '-': op.sub,
            '*': op.mul,
            '/': op.truediv,
            # omitted here: more operator definitions
            'abs': abs,
            'append': lambda *args: list(chain(*args)),
            'apply': lambda proc, args: proc(*args),
            'begin': lambda *x: x[-1],
            'car': lambda x: x[0],
            'cdr': lambda x: x[1:],
            # omitted here: more function definitions
            'number?': lambda x: isinstance(x, (int, float)),
            'procedure?': callable,
            'round': round,
            'symbol?': lambda x: isinstance(x, Symbol),
    })
    return env

总结一下,env映射加载了:

  • Python 的math模块中的所有函数
  • 从 Python 的op模块中选择的运算符
  • 使用 Python 的lambda构建的简单但强大的函数
  • Python 内置函数重命名,比如callable改为procedure?,或者直接映射,比如round

REPL

Norvig 的 REPL(读取-求值-打印-循环)易于理解但不用户友好(参见 Example 18-16)。如果没有向lis.py提供命令行参数,则main()会调用repl()函数—在模块末尾定义。在lis.py>提示符下,我们必须输入正确和完整的表达式;如果忘记关闭一个括号,lis.py会崩溃。¹⁰

示例 18-16. REPL 函数
def repl(prompt: str = 'lis.py> ') -> NoReturn:
    "A prompt-read-eval-print loop."
    global_env = Environment({}, standard_env())
    while True:
        ast = parse(input(prompt))
        val = evaluate(ast, global_env)
        if val is not None:
            print(lispstr(val))
def lispstr(exp: object) -> str:
    "Convert a Python object back into a Lisp-readable string."
    if isinstance(exp, list):
        return '(' + ' '.join(map(lispstr, exp)) + ')'
    else:
        return str(exp)

这里是关于这两个函数的简要解释:

repl(prompt: str = 'lis.py> ') -> NoReturn

调用standard_env()为全局环境提供内置函数,然后进入一个无限循环,读取和解析每个输入行,在全局环境中评估它,并显示结果—除非它是Noneglobal_env可能会被evaluate修改。例如,当用户定义新的全局变量或命名函数时,它会存储在环境的第一个映射中—在repl的第一行中的Environment构造函数调用中的空dict中。

lispstr(exp: object) -> str

parse的反函数:给定表示表达式的 Python 对象,parse返回其 Scheme 源代码。例如,给定['+', 2, 3],结果是'(+ 2 3)'

评估器

现在我们可以欣赏 Norvig 的表达式求值器的美丽之处—通过match/case稍微美化了一下。Example 18-17 中的evaluate函数接受由parse构建的Expression和一个Environment

evaluate的主体是一个带有表达式exp的单个match语句。case模式以惊人的清晰度表达了 Scheme 的语法和语义。

示例 18-17. evaluate接受一个表达式并计算其值
KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']
def evaluate(exp: Expression, env: Environment) -> Any:
    "Evaluate an expression in an environment."
    match exp:
        case int(x) | float(x):
            return x
        case Symbol(var):
            return env[var]
        case ['quote', x]:
            return x
        case ['if', test, consequence, alternative]:
            if evaluate(test, env):
                return evaluate(consequence, env)
            else:
                return evaluate(alternative, env)
        case ['lambda', [*parms], *body] if body:
            return Procedure(parms, body, env)
        case ['define', Symbol(name), value_exp]:
            env[name] = evaluate(value_exp, env)
        case ['define', [Symbol(name), *parms], *body] if body:
            env[name] = Procedure(parms, body, env)
        case ['set!', Symbol(name), value_exp]:
            env.change(name, evaluate(value_exp, env))
        case [func_exp, *args] if func_exp not in KEYWORDS:
            proc = evaluate(func_exp, env)
            values = [evaluate(arg, env) for arg in args]
            return proc(*values)
        case _:
            raise SyntaxError(lispstr(exp))

让我们研究每个case子句及其作用。在某些情况下,我添加了注释,显示出一个 S 表达式,当解析为 Python 列表时会匹配该模式。从examples_test.py提取的 Doctests 展示了每个case

评估数字

case int(x) | float(x):
        return x

主题:

intfloat的实例。

操作:

原样返回值。

示例:

>>> from lis import parse, evaluate, standard_env
>>> evaluate(parse('1.5'), {})
1.5

评估符号

case Symbol(var):
        return env[var]

主题:

Symbol的实例,即作为标识符使用的str

操作:

env中查找var并返回其值。

示例:

>>> evaluate(parse('+'), standard_env())
<built-in function add>
>>> evaluate(parse('ni!'), standard_env())
Traceback (most recent call last):
    ...
KeyError: 'ni!'

(quote …)

quote特殊形式将原子和列表视为数据而不是要评估的表达式。

# (quote (99 bottles of beer))
    case ['quote', x]:
        return x

主题:

以符号'quote'开头的列表,后跟一个表达式x

操作:

返回x而不对其进行评估。

示例:

>>> evaluate(parse('(quote no-such-name)'), standard_env())
'no-such-name'
>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env())
[99, 'bottles', 'of', 'beer']
>>> evaluate(parse('(quote (/ 10 0))'), standard_env())
['/', 10, 0]

没有quote,测试中的每个表达式都会引发错误:

  • 在环境中查找no-such-name,引发KeyError
  • (99 bottles of beer)无法评估,因为数字 99 不是命名特殊形式、运算符或函数的Symbol
  • (/ 10 0)会引发ZeroDivisionError

(if …)

# (if (< x 0) 0 x)
    case ['if', test, consequence, alternative]:
        if evaluate(test, env):
            return evaluate(consequence, env)
        else:
            return evaluate(alternative, env)

主题:

'if'开头的列表,后跟三个表达式:testconsequencealternative

操作:

评估test

  • 如果为真,则评估consequence并返回其值。
  • 否则,评估alternative并返回其值。

示例:

>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env())
1
>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env())
0

consequencealternative分支必须是单个表达式。如果分支中需要多个表达式,可以使用(begin exp1 exp2…)将它们组合起来,作为lis.py中的一个函数提供—参见 Example 18-15。

(lambda …)

Scheme 的lambda形式定义了匿名函数。它不受 Python 的lambda的限制:任何可以用 Scheme 编写的函数都可以使用(lambda ...)语法编写。

# (lambda (a b) (/ (+ a b) 2))
    case ['lambda' [*parms], *body] if body:
        return Procedure(parms, body, env)

主题:

'lambda'开头的列表,后跟:

  • 零个或多个参数名的列表。
  • 一个或多个收集在body中的表达式(保证body不为空)。

操作:

创建并返回一个新的Procedure实例,其中包含参数名称、作为主体的表达式列表和当前环境。

例子:

>>> expr = '(lambda (a b) (* (/ a b) 100))'
>>> f = evaluate(parse(expr), standard_env())
>>> f  # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> f(15, 20)
75.0

Procedure类实现了闭包的概念:一个可调用对象,包含参数名称、函数体和函数定义所在环境的引用。我们将很快研究Procedure的代码。

(define …)

define关键字以两种不同的语法形式使用。最简单的是:

# (define half (/ 1 2))
    case ['define', Symbol(name), value_exp]:
        env[name] = evaluate(value_exp, env)

主题:

'define'开头的列表,后跟一个Symbol和一个表达式。

操作:

评估表达式并将其值放入env中,使用name作为键。

例子:

>>> global_env = standard_env()
>>> evaluate(parse('(define answer (* 7 6))'), global_env)
>>> global_env['answer']
42

case的 doctest 创建一个global_env,以便我们可以验证evaluateanswer放入该Environment中。

我们可以使用简单的define形式来创建变量或将名称绑定到匿名函数,使用(lambda …)作为value_exp

标准 Scheme 提供了一种快捷方式来定义命名函数。这是第二种define形式:

# (define (average a b) (/ (+ a b) 2))
    case ['define', [Symbol(name), *parms], *body] if body:
        env[name] = Procedure(parms, body, env)

主题:

'define'开头的列表,后跟:

  • Symbol(name)开头的列表,后跟零个或多个收集到名为parms的列表中的项目。
  • 一个或多个收集在body中的表达式(保证body不为空)。

操作:

  • 创建一个新的Procedure实例,其中包含参数名称、作为主体的表达式列表和当前环境。
  • Procedure放入env中,使用name作为键。

Example 18-18 中的 doctest 定义了一个名为%的计算百分比的函数,并将其添加到global_env中。

例 18-18. 定义一个名为%的计算百分比的函数
>>> global_env = standard_env()
>>> percent = '(define (% a b) (* (/ a b) 100))'
>>> evaluate(parse(percent), global_env)
>>> global_env['%']  # doctest: +ELLIPSIS
<lis.Procedure object at 0x...>
>>> global_env'%'
85.0

调用evaluate后,我们检查%是否绑定到一个接受两个数值参数并返回百分比的Procedure

第二个define case的模式不强制要求parms中的项目都是Symbol实例。我必须在构建Procedure之前检查这一点,但我没有这样做——为了使代码像 Norvig 的那样易于理解。

(set! …)

set!形式更改先前定义的变量的值。¹¹

# (set! n (+ n 1))
    case ['set!', Symbol(name), value_exp]:
        env.change(name, evaluate(value_exp, env))

主题:

'set!'开头的列表,后跟一个Symbol和一个表达式。

操作:

使用评估表达式的结果更新env中的name的值。

Environment.change方法从本地到全局遍历链接的环境,并用新值更新第一次出现的name。如果我们不实现'set!'关键字,我们可以在解释器中的任何地方都使用 Python 的ChainMap作为Environment类型。

现在我们来到一个函数调用。

函数调用

# (gcd (* 2 105) 84)
    case [func_exp, *args] if func_exp not in KEYWORDS:
        proc = evaluate(func_exp, env)
        values = [evaluate(arg, env) for arg in args]
        return proc(*values)

主题:

一个或多个项目的列表。

保护确保func_exp不是['quote', 'if', 'define', 'lambda', 'set!']中的一个——在 Example 18-17 中的evaluate之前列出。

模式匹配任何具有一个或多个表达式的列表,将第一个表达式绑定到func_exp,其余的绑定到args作为列表,可能为空。

操作:

  • 评估func_exp以获得函数proc
  • 评估args中的每个项目以构建参数值列表。
  • 用值作为单独参数调用proc,返回结果。

例子:

>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env)
42.0

此 doctest 继续自 Example 18-18:假设global_env有一个名为%的函数。给定给%的参数是算术表达式,以强调在调用函数之前对参数进行评估。

case中的保护是必需的,因为[func_exp, *args]匹配任何具有一个或多个项目的主题序列。但是,如果func_exp是一个关键字,并且主题没有匹配任何先前的情况,那么这实际上是一个语法错误。

捕获语法错误

如果主题exp不匹配任何先前的情况,通用case会引发SyntaxError

case _:
        raise SyntaxError(lispstr(exp))

这是一个作为SyntaxError报告的格式不正确的(lambda …)的示例:

>>> evaluate(parse('(lambda is not like this)'), standard_env())
Traceback (most recent call last):
    ...
SyntaxError: (lambda is not like this)

如果函数调用的 case 没有拒绝关键字的保护,(lambda is not like this) 表达式将被处理为函数调用,这将引发 KeyError,因为 'lambda' 不是环境的一部分——就像 lambda 不是 Python 内置函数一样。

Procedure: 实现闭包的类

Procedure 类实际上可以被命名为 Closure,因为它代表的就是一个函数定义和一个环境。函数定义包括参数的名称和构成函数主体的表达式。当函数被调用时,环境用于提供自由变量的值:出现在函数主体中但不是参数、局部变量或全局变量的变量。我们在“闭包”中看到了闭包自由变量的概念。

我们学会了如何在 Python 中使用闭包,但现在我们可以更深入地了解闭包是如何在 lis.py 中实现的:

class Procedure:
    "A user-defined Scheme procedure."
    def __init__(  # ①
        self, parms: list[Symbol], body: list[Expression], env: Environment
    ):
        self.parms = parms  # ②
        self.body = body
        self.env = env
    def __call__(self, *args: Expression) -> Any:  # ③
        local_env = dict(zip(self.parms, args))  # ④
        env = Environment(local_env, self.env)  # ⑤
        for exp in self.body:  # ⑥
            result = evaluate(exp, env)
        return result  # ⑦

当函数由 lambdadefine 形式定义时调用。

保存参数名称、主体表达式和环境以备后用。

case [func_exp, *args] 子句的最后一行由 proc(*values) 调用。

构建 local_env,将 self.parms 映射为局部变量名称,将给定的 args 映射为值。

构建一个新的合并 env,将 local_env 放在首位,然后是 self.env—即在函数定义时保存的环境。

迭代 self.body 中的每个表达式,在合并的 env 中对其进行评估。

返回最后一个表达式评估的结果。

lis.py中的 evaluate 后有几个简单的函数:run 读取一个完整的 Scheme 程序并执行它,main 调用 runrepl,取决于命令行——类似于 Python 的做法。我不会描述这些函数,因为它们没有什么新内容。我的目标是与你分享 Norvig 的小解释器的美丽之处,更深入地了解闭包的工作原理,并展示 match/case 如何成为 Python 的一个很好的补充。

结束这一关于模式匹配的扩展部分,让我们正式定义一下OR-pattern的概念。

使用 OR-patterns

一系列由 | 分隔的模式是一个OR-pattern:如果任何子模式成功,则它成功。“评估数字”中的模式就是一个 OR-pattern:

case int(x) | float(x):
        return x

OR-pattern 中的所有子模式必须使用相同的变量。这种限制是必要的,以确保变量在匹配的子模式中都是可用的,无论匹配的是哪个子模式。

警告

case 子句的上下文中,| 运算符有特殊的含义。它不会触发 __or__ 特殊方法,该方法处理其他上下文中的表达式,如 a | b,在这些上下文中,它被重载以执行诸如集合并集或整数按位或等操作,具体取决于操作数。

OR-pattern 不限于出现在模式的顶层。你也可以在子模式中使用 |。例如,如果我们希望 lis.py 接受希腊字母 λ (lambda)¹² 以及 lambda 关键字,我们可以像这样重写模式:

# (λ (a b) (/ (+ a b) 2) )
    case ['lambda' | 'λ', [*parms], *body] if body:
        return Procedure(parms, body, env)

现在我们可以转向本章的第三个也是最后一个主题:Python 中出现 else 子句的不寻常位置。

这样做,然后那样:else 块超出 if

这并不是什么秘密,但是这是一个被低估的语言特性:else子句不仅可以用在if语句中,还可以用在forwhiletry语句中。

for/elsewhile/elsetry/else的语义密切相关,但与if/else非常不同。最初,else这个词实际上阻碍了我对这些特性的理解,但最终我习惯了它。

这里是规则:

for

for循环完成时,else块将运行一次(即,如果forbreak中止,则不会运行)。

while

while循环退出时,else块只会运行一次,因为条件变为(即,如果whilebreak中止,则不会运行)。

try

try块中没有引发异常时,else块将运行。官方文档还指出:“else子句中的异常不会被前面的except子句处理。”

在所有情况下,如果异常或returnbreakcontinue语句导致控制跳出复合语句的主块,则else子句也会被跳过。

注意

我认为else在除了if之外的所有情况下都是一个非常糟糕的关键字选择。它暗示了一个排除性的替代方案,比如,“运行这个循环,否则做那个”。但是在循环中else的语义是相反的:“运行这个循环,然后做那个”。这表明then是一个更好的关键字选择——这在try的上下文中也是有意义的:“尝试这个,然后做那个”。然而,添加一个新关键字会对语言造成破坏性的改变——这不是一个容易做出的决定。

在这些语句中使用else通常会使代码更易读,避免设置控制标志或编写额外的if语句。

在循环中使用else通常遵循以下代码段的模式:

for item in my_list:
    if item.flavor == 'banana':
        break
else:
    raise ValueError('No banana flavor found!')

try/except块的情况下,else起初可能看起来是多余的。毕竟,在以下代码段中,只有当dangerous_call()不引发异常时,after_call()才会运行,对吗?

try:
    dangerous_call()
    after_call()
except OSError:
    log('OSError...')

然而,这样做将after_call()放在try块内没有任何好处。为了清晰和正确,try块的主体应该只包含可能引发预期异常的语句。这样更好:

try:
    dangerous_call()
except OSError:
    log('OSError...')
else:
    after_call()

现在清楚了try块是在保护dangerous_call()可能出现的错误,而不是在after_call()中。明确的是,只有在try块中没有引发异常时,after_call()才会执行。

在 Python 中,try/except通常用于控制流,而不仅仅用于错误处理。甚至在官方 Python 术语表中都有一个缩略语/口号来记录这一点:

EAFP

宽恕比许可更容易。这种常见的 Python 编码风格假设存在有效的键或属性,并在假设被证明为假时捕获异常。这种干净且快速的风格的特点是存在许多 try 和 except 语句。这种技术与许多其他语言(如 C)常见的LBYL风格形成对比。

然后,术语表定义了 LBYL:

LBYL

三思而后行。这种编码风格在进行调用或查找之前明确测试前提条件。这种风格与EAFP方法形成对比,其特点是存在许多 if 语句。在多线程环境中,LBYL 方法可能会在“观察”和“跳转”之间引入竞争条件。例如,如果代码if key in mapping: return mapping[key]在测试后,但查找前,另一个线程从映射中删除了键,那么就会失败。这个问题可以通过使用锁或使用 EAFP 方法来解决。

鉴于 EAFP 风格,了解并善于使用try/except语句中的else块更有意义。

注意

当讨论match语句时,一些人(包括我在内)认为它也应该有一个else子句。最终决定不需要,因为case _:可以完成相同的工作。¹³

现在让我们总结本章。

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

相关文章
|
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 重译)(六)
127 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
6天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
68 4