Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 3150 – 语句局部命名空间(也称为“given”子句)

作者:
Alyssa Coghlan <ncoghlan at gmail.com>
状态:
已延期
类型:
标准轨迹
创建时间:
2010-07-09
Python 版本:
3.4
更新历史:
2010-07-14, 2011-04-21, 2011-06-13

目录

摘要

本 PEP 提案在一些目前没有关联代码块的 Python 语句中添加一个可选的 given 子句。此子句将为关联语句创建语句局部命名空间,其中包含可以在该语句中访问的其他名称,但不会成为包含命名空间的一部分。

建议采用新符号 ? 来表示对由关联代码块运行创建的命名空间的向前引用。它将是 types.SimpleNamespace 对象的引用。

主要动机是使编程风格更加声明式,其中首先向读者展示要执行的操作,然后在以下缩进块中展示必要的子计算的详细信息。作为关键示例,这将提升普通赋值语句与 classdef 语句的同等地位,在这些语句中,要定义的项目的名称将在计算该项目值的详细信息之前展示给读者。它还允许命名函数以“多行 lambda”的方式使用,其中该名称仅用作当前表达式中的占位符,并在以下块中定义。

另一个动机是在模块和类级代码中简化临时计算,而不污染结果命名空间。

意图是,given 子句和执行指定操作的单独函数定义之间的关系将类似于现有显式 while 循环和生成器之间的关系,该生成器生成与该 while 循环相同的操作序列。

本 PEP 中的具体提案参考了多年来对这种概念及其相关概念的各种探索(例如 [1][2][3][6][8]),并受到 Haskell 中 wherelet 子句的一定程度的启发。它避免了以往提案中发现的一些问题,但尚未经过实现测试。

提案

本 PEP 提案在可能包含表达式的简单语句的语法中添加一个可选的 given 子句,或者可能出于纯粹的语法目的替代此类语句。以下是在此添加中会受到影响的简单语句列表:

  • 表达式语句
  • 赋值语句
  • 增强赋值语句
  • del 语句
  • return 语句
  • yield 语句
  • raise 语句
  • assert 语句
  • pass 语句

given 子句允许在标题行中使用名称引用子表达式,实际定义在缩进子句中。举个简单的例子

sorted_data = sorted(data, key=?.sort_key) given:
    def sort_key(item):
        return item.attr1, item.attr2

新符号 ? 用于引用 given 命名空间。它将是一个 types.SimpleNamespace 实例,因此 ?.sort_key 充当对 given 子句中定义的名称的向前引用。

given 子句中允许使用文档字符串,并将其作为 __doc__ 属性附加到结果命名空间。

包含 pass 语句是为了提供一种一致的方式来跳过在标题行中包含有意义的表达式。虽然这不是预期的用例,但它不是可以阻止的,因为即使不允许 pass 本身,仍然存在多个替代方案(如 ...())。

given 子句的主体将在新作用域中执行,使用正常的函数闭包语义。为了支持循环变量和全局引用的早期绑定,以及允许访问类作用域中定义的其他名称,given 子句还允许在标题行中进行显式绑定操作

# Explicit early binding via given clause
seq = []
for i in range(10):
    seq.append(?.f) given i=i in:
        def f():
            return i
assert [f() for f in seq] == list(range(10))

语义

以下语句

op(?.f, ?.g) given bound_a=a, bound_b=b in:
    def f():
        return bound_a + bound_b
    def g():
        return bound_a - bound_b

大致等效于以下代码(__var 表示隐藏的编译器变量或解释器堆栈中的条目)

__arg1 = a
__arg2 = b
def __scope(bound_a, bound_b):
    def f():
        return bound_a + bound_b
    def g():
        return bound_a - bound_b
   return types.SimpleNamespace(**locals())
__ref = __scope(__arg1, __arg2)
__ref.__doc__ = __scope.__doc__
op(__ref.f, __ref.g)

given 子句本质上是一个嵌套函数,它被创建然后立即执行。除非明确传入,否则名称使用正常的范围规则查找,因此类作用域中定义的名称将不可见。声明为向前引用的名称将被返回并在标题语句中使用,而不会在周围命名空间中局部绑定。

语法变更

当前

expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
             ('=' (yield_expr|testlist_star_expr))*)
del_stmt: 'del' exprlist
pass_stmt: 'pass'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
assert_stmt: 'assert' test [',' test]

新的

expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
             ('=' (yield_expr|testlist_star_expr))*) [given_clause]
del_stmt: 'del' exprlist [given_clause]
pass_stmt: 'pass' [given_clause]
return_stmt: 'return' [testlist] [given_clause]
yield_stmt: yield_expr [given_clause]
raise_stmt: 'raise' [test ['from' test]] [given_clause]
assert_stmt: 'assert' test [',' test] [given_clause]
given_clause: "given" [(NAME '=' test)+ "in"]":" suite

(请注意,语法中的 expr_stmt 有点误称,因为它除了简单的表达式语句外,还涵盖赋值和增强赋值)

注意

这些建议的语法更改尚未涵盖访问语句局部命名空间中定义的名称的向前引用表达式语法。

新子句作为现有语句的可选元素添加,而不是作为一种新的复合语句,以避免在语法中创建歧义。它只应用于列出的特定元素,因此以下语句将不被允许

break given:
    a = b = 1

import sys given:
    a = b = 1

然而,上面描述的精确语法更改是不够的,因为它为 simple_stmt 的定义带来了问题(simple_stmt 允许使用“;”而不是“\n”连接多个单行语句)。

因此,上述语法更改应被视为意图的说明。任何实际的提案都需要在可以认真考虑之前解决 simple_stmt 解析问题。这可能需要对语法进行非平凡的重构,将 small_stmt 和 flow_stmt 分开,将可能包含任意子表达式的语句分开,然后允许一个这些语句在 simple_stmt 级别包含 given 子句。类似于以下内容

stmt: simple_stmt | given_stmt | compound_stmt
simple_stmt: small_stmt (';' (small_stmt | subexpr_stmt))* [';'] NEWLINE
small_stmt: (pass_stmt | flow_stmt | import_stmt |
             global_stmt | nonlocal_stmt)
flow_stmt: break_stmt | continue_stmt
given_stmt: subexpr_stmt (given_clause |
              (';' (small_stmt | subexpr_stmt))* [';']) NEWLINE
subexpr_stmt: expr_stmt | del_stmt | flow_subexpr_stmt | assert_stmt
flow_subexpr_stmt: return_stmt | raise_stmt | yield_stmt
given_clause: "given" (NAME '=' test)* ":" suite

作为参考,以下是该级别当前的定义

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt

除了上述更改外,atom 的定义也将被更改,使其也允许 ?。此使用限制在具有关联 given 子句的语句中,这将由编译过程的后期阶段处理(可能是 AST 构建,该构建已经强制执行了其他限制,而语法过于宽松以简化初始解析步骤)。

新的 PEP 8 指南

正如在 python-ideas 上讨论的([7][9]),还需要制定新的 PEP 8 指南,为何时使用 given 子句而不是普通变量赋值提供适当的指导。

基于针对 try 语句已经存在的类似指南,本 PEP 提案在 PEP 8 的“编程约定”部分中为 given 语句添加以下内容

  • 对于可以合理地分解为单独函数的代码,但当前未在任何地方重用,请考虑使用 given 子句。这清楚地表明哪些变量仅用于定义另一个语句的子组件,而不是用于保存算法或应用程序状态。当将多行函数传递给接受可调用参数的操作时,这是一种特别有用的技术。
  • 保持 given 子句简洁。如果它们变得笨拙,要么将它们分解为多个步骤,要么将详细信息移动到单独的函数中。

理由

Python 中的函数和类语句相对于普通赋值语句具有独特的属性:在某种程度上,它们是声明式的。它们在继续定义函数或类主体中的实际定义的详细信息之前,向代码的读者提供了一些关于要定义的名称的关键信息。

要声明的对象的名称是关键字之后声明的第一件事。其他重要信息也有资格优先于实现细节

  • 装饰器(它们可以极大地影响创建的对象的行为,并且出于实用性而非美观的考虑,被放置在关键字和名称之前)
  • 文档字符串(位于标题行后的第一行)
  • 函数定义的参数、默认值和注释
  • 类定义的父类、元类和可选的其他细节(取决于元类)

本 PEP 提案通过允许在任何简单赋值语句之后包含“given”块,使类似的声明式风格可用于任意赋值操作

TARGET = [TARGET2 = ... TARGETN =] EXPR given:
    SUITE

按照惯例,块主体中的代码应该完全面向正确地定义标题行中执行的赋值操作。标题行操作也应该有足够的描述性(例如,通过适当的变量名选择),以便读者在不阅读块主体的情况下就能合理地了解操作的目的。

然而,虽然它们是最初的驱动力用例,但将此功能仅限于简单的赋值将过于严格。一旦该功能被定义,阻止它用于增强赋值、返回语句、yield 表达式、推导式以及可能修改应用程序状态的任意表达式将是相当武断的。

当将一次性函数传递给诸如 sorted() 之类的操作或在基于回调的事件驱动编程中时,given 子句也可以用作某些 lambda 表达式和类似结构的更易读的替代方案。

在模块和类级别代码中,given 子句将作为 del 语句的明确且可靠的替代方案,以防止临时工作变量污染结果命名空间。

将提议的子句视为传统内联代码和将操作分离到专用函数之间的中间地带的一种潜在有用方法,就像内联 while 循环最终可能被分解为专用生成器一样。

设计讨论

关键字选择

此提案最初使用 where,基于 Haskell 中类似构造的名称。但是,已经指出,存在现有的 Python 库(例如 Numpy [4])已经在 SQL 查询条件意义上使用 where,这使得该关键字选择可能令人困惑。

虽然 given 也可以用作变量名(因此将使用通常的 __future__ 方式来引入新关键字而被弃用),但它与新的子句的期望的“这里有一些额外变量此表达式可以使用”语义的关联要强得多。

还建议重用 with 关键字。这样做的好处是可以避免添加新关键字,但也存在很大的混淆可能性,因为 with 子句和 with 语句看起来很相似,但做的事情完全不同。那条路通向 C++ 和 Perl :)

与 PEP 403 的关系

PEP 403(通用装饰器子句)尝试使用受现有装饰器语法启发的、不太激进的语言更改来实现本 PEP 的主要目标。

尽管这两份 PEP 有相同的作者,但它们却相互直接竞争。PEP 403 代表了一种极简主义方法,试图通过对现状进行最小的改变来实现有用的功能。本 PEP 反而旨在设计一个更灵活的独立语句,这需要对语言进行更大程度的改变。

请注意,PEP 403 更适合解释生成器表达式的行为,本 PEP 更适合解释装饰器子句的行为。这两份 PEP 都支持对容器推导式语义的充分解释。

解释容器推导式和生成器表达式

提议的构造的一个有趣的特征是,它可以用作解释容器推导式的作用域和执行顺序语义的基元。

seq2 = [x for x in y if q(x) for y in seq if p(y)]

# would be equivalent to

seq2 = ?.result given seq=seq:
    result = []
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    result.append(x)

这种扩展中的重点是,它解释了为什么推导式在类作用域中似乎表现不正常:只有最外层的迭代器在类作用域中进行评估,而所有谓词、嵌套迭代器和值表达式都在嵌套作用域中进行评估。

注意,与 PEP 403 不同,本 PEP 的当前版本 *不能* 为生成器表达式提供完全等效的扩展。它能做到的最接近的是定义额外的作用域级别。

seq2 = ?.g(seq) given:
    def g(seq):
        for y in seq:
            if p(y):
                for x in y:
                    if q(x):
                        yield x

此限制可以通过允许 given 子句成为生成器函数来弥补,在这种情况下,?将引用生成器迭代器对象而不是简单的命名空间。

seq2 = ? given seq=seq in:
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    yield x

然而,这将使“?”的含义非常模糊,甚至比 def 语句的含义(通常会有一个文档字符串指示函数定义是否实际上是一个生成器)更为模糊。

解释装饰器子句的评估和应用

装饰器子句评估和应用的标准解释必须处理隐藏的编译器变量的概念,以便显示执行步骤的顺序。given 语句允许像

@classmethod
def classname(cls):
    return cls.__name__

这样的装饰函数定义被解释为大致等效于

classname = .d1(classname) given:
    d1 = classmethod
    def classname(cls):
        return cls.__name__

预期的反对意见

两种方法

现在,许多代码可能使用在表达式使用之前定义的值或在 given 子句中定义的值,从而创建两种方式来执行此操作,可能没有明显的选择方式。

反思后,我觉得这是对“一个明显的途径”格言的误用。Python 已经提供了 *很多* 编写代码的方式。我们可以使用 for 循环或 while 循环,使用函数式风格或命令式风格或面向对象风格。一般来说,该语言旨在让人们编写符合他们思维方式的代码。由于不同的人思维方式不同,他们编写代码的方式也会相应改变。

代码库中的这种风格问题应该正确地留给负责该代码的开发小组。表达式什么时候变得非常复杂,以至于应该将子表达式取出并赋值给变量,即使这些变量只会被使用一次?什么时候应该用实现相同逻辑的生成器来替换内联 while 循环?人们的意见不同,这没关系。

但是,CPython 和标准库需要明确的 PEP 8 指导,这一点在上面的提案中进行了讨论。

乱序执行

given 子句使执行有点奇怪地跳跃,因为 given 子句的主体在子句标题中的简单语句之前执行。Python 中其他任何部分最接近这种情况的是列表推导式、生成器表达式和条件表达式中的乱序评估以及装饰器函数对它们装饰的函数的延迟应用(装饰器表达式本身按它们编写的顺序执行)。

虽然这是真的,但语法旨在用于人们自己 *思考* 问题时按不同顺序进行思考的情况(至少就语言而言)。作为例子,请考虑 Python 用户心中以下想法。

我想根据每个项目上 attr1 和 attr2 的值对该序列中的项目进行排序。

如果他们熟悉 Python 的 lambda 表达式,那么他们可能会选择这样写。

sorted_list = sorted(original, key=(lambda v: v.attr1, v.attr2))

这完成了工作,但它几乎没有达到符合 Python 声誉的 可执行伪代码 的标准。

如果他们不喜欢 lambda 本身,operator 模块提供了一种替代方法,它仍然允许内联定义关键函数。

sorted_list = sorted(original,
                     key=operator.attrgetter(v. 'attr1', 'attr2'))

同样,它完成了工作,但即使是最慷慨的读者也不会认为它是“可执行伪代码”。

如果他们认为上述两种选择都很丑陋且令人困惑,或者他们需要在关键函数中使用无法用表达式表达的逻辑(例如捕获异常),那么 Python 目前迫使他们颠倒他们最初的想法的顺序,首先定义排序标准。

def sort_key(item):
    return item.attr1, item.attr2

sorted_list = sorted(original, key=sort_key)

“只需定义一个函数”多年来一直是人们对多行 lambda 支持请求的机械式回复。与上述选项一样,它完成了工作,但它确实代表了用户的想法和语言允许他们表达的内容之间的断裂。

我相信本 PEP 中的提案将最终使 Python 接近对上面表达的那种想法的“可执行伪代码”标准。

sorted_list = sorted(original, key=?.key) given:
    def key(item):
        return item.attr1, item.attr2

一切都按照用户最初的想法的顺序排列,他们甚至不需要为排序标准想出一个名字:可以直接重用关键字参数名。

对这些提议的可能增强是提供一种方便的简写语法来表示“将 given 子句内容用作关键字参数”。即使没有专门的语法,也可以简单地编写为 **vars(?)

对内省有害

在模块和类内部进行探索是白盒测试和交互式调试的宝贵工具。 given 子句将非常有效地防止访问计算过程中使用的临时状态(尽管在这方面不比目前使用 del 语句更有效)。

虽然这是一个合理的担忧,但可测试性设计是一个跨越许多编程方面的议题。如果需要独立测试一个组件,那么应该将 given 语句重构为单独的语句,以便将信息公开给测试套件。这与将隐藏在函数或生成器内部的操作重构为它自己的函数以允许它被孤立地测试没有太大区别。

缺乏现实世界影响评估

当前 PEP 中的例子几乎都是相对较小的“玩具”例子。本 PEP 中的提案需要经受将大型代码库(例如标准库或大型 Twisted 应用程序)应用于测试的考验,以寻找可以真正提高真实世界代码可读性的例子。

这更多的是 PEP 本身的不足,而不是这个想法的不足。如果这不是一个真实世界的问题,我们不会收到这么多关于缺乏多行 lambda 支持的抱怨,Ruby 的块构造可能也不会那么受欢迎。

开放问题

向前引用的语法

? 符号被提议用于向前引用 given 命名空间,因为它简短、目前未使用并且暗示“这里缺少一些东西,将在以后填补”。

PEP 中的提案并没有与 Python 中的任何现有功能完全平行,因此有意避免重用已经使用的符号。

处理 nonlocalglobal

nonlocalglobalgiven 子句块中明确禁止使用,如果出现将是语法错误。如果它们出现在该块中的 def 语句中,它们将正常工作。

或者,它们可以被定义为与上面展开中的匿名函数定义相同。

处理 breakcontinue

breakcontinue 的行为将与上面展开中的匿名函数定义相同。如果它们出现在 given 子句块中,将是语法错误,但如果它们出现在该块中的 forwhile 循环中,则会正常工作。

处理 returnyield

returnyieldgiven 子句块中明确禁止使用,如果出现将是语法错误。如果它们出现在该块中的 def 语句中,它们将正常工作。

示例

为事件驱动编程定义回调函数

# Current Python (definition before use)
def cb(sock):
    # Do something with socket
def eb(exc):
    logging.exception(
        "Failed connecting to %s:%s", host, port)
loop.create_connection((host, port), cb, eb) given:

# Becomes:
loop.create_connection((host, port), ?.cb, ?.eb) given:
    def cb(sock):
        # Do something with socket
    def eb(exc):
        logging.exception(
            "Failed connecting to %s:%s", host, port)

定义通常只有一个实例的“一次性”类

# Current Python (instantiation after definition)
class public_name():
  ... # However many lines
public_name = public_name(*params)

# Current Python (custom decorator)
def singleton(*args, **kwds):
    def decorator(cls):
        return cls(*args, **kwds)
    return decorator

@singleton(*params)
class public_name():
  ... # However many lines

# Becomes:
public_name = ?.MeaningfulClassName(*params) given:
  class MeaningfulClassName():
    ... # Should trawl the stdlib for an example of doing this

计算属性而不污染局部命名空间(来自 os.py)

# Current Python (manual namespace cleanup)
def _createenviron():
  ... # 27 line function

environ = _createenviron()
del _createenviron

# Becomes:
environ = ?._createenviron() given:
    def _createenviron():
      ... # 27 line function

替换默认参数黑客(来自 functools.lru_cache)

# Current Python (default argument hack)
def decorating_function(user_function,
               tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):
  ... # 60 line function
return decorating_function

# Becomes:
return ?.decorating_function given:
  # Cell variables rather than locals, but should give similar speedup
  tuple, sorted, len, KeyError = tuple, sorted, len, KeyError
  def decorating_function(user_function):
    ... # 60 line function

# This example also nicely makes it clear that there is nothing in the
# function after the nested function definition. Due to additional
# nested functions, that isn't entirely clear in the current code.

可能的添加

  • 当前提案只允许在简单语句中添加 given 子句。将此想法扩展到允许使用复合语句是完全可能的(通过在末尾附加一个独立的 given 子句),但这样做会引发严重的易读性问题(因为在 given 子句中定义的值可能在定义之前就被使用,这正是其他功能(如装饰器和 with 语句)旨在消除的可读性陷阱)。
  • “显式早期绑定”变体可能适用于 python-ideas 上关于如何消除默认参数黑客的讨论。函数标题行中的 given 子句(在返回值类型标注之后)可能是这个问题的答案。

被拒绝的替代方案

  • 此 PEP 的早期版本允许隐式地向前引用尾随块中的名称,并使用隐式早期绑定语义。这两个想法在没有提供足够的表达能力提高的情况下,极大地使提案复杂化。当前提出的显式向前引用和早期绑定,使新结构符合现有的作用域语义,极大地提高了该想法实际实现的可能性。
  • 除了这里提出的建议之外,还有一些关于两个块“按顺序”变体的建议,它们提供了有限的名称作用域,而不支持乱序执行。我认为这些建议很大程度上错过了人们在要求支持多行 lambda 时抱怨的要点 - 并不是说为子表达式想出一个名字特别困难,而是说在使用该语句之前命名函数意味着代码不再匹配开发人员对问题的思考方式。
  • 我尝试过一些未发布的方法,允许直接引用 given 子句隐式创建的闭包,同时保留本 PEP 中定义的语法的总体结构(例如,允许使用类似 ?given:given 的子表达式在表达式中表示对隐式闭包的直接引用,从而防止它自动被调用以创建局部命名空间)。所有这些尝试与 PEP 403 中更简单的装饰器启发式提案相比,都显得不美观且令人困惑。

参考实现

目前还没有。如果您想快速学习 Python 命名空间语义和代码编译,请随意尝试;)

待办事项

  • 提及 PEP 359 以及 given 子句中 locals() 的可能用途。
  • 确定这是否可以用于内部,使零参数 super() 调用的实现不那么糟糕。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-3150.rst

最后修改时间:2023-10-11 12:05:51 GMT