PEP 3150 – 语句局部命名空间(又称“given”子句)
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 创建日期:
- 2010年7月9日
- Python 版本:
- 3.4
- 发布历史:
- 2010年7月14日,2011年4月21日,2011年6月13日
摘要
本PEP提议为Python中目前没有相关代码套件的几个语句添加一个可选的given
子句。该子句将为附加名称创建一个语句局部命名空间,这些名称可在相关语句中访问,但不会成为包含命名空间的一部分。
提议采用一个新的符号?
来表示对通过运行相关代码套件创建的命名空间的前向引用。它将是对types.SimpleNamespace
对象的引用。
主要动机是启用更具声明性的编程风格,即首先向读者呈现要执行的操作,然后详细介绍必要的子计算。作为一个关键示例,这将使普通赋值语句与class
和def
语句处于同等地位,在这些语句中,要定义项的名称在计算该项值之前就呈现给读者。它还允许以“多行lambda”方式使用命名函数,其中名称仅用作当前表达式中的占位符,然后在随后的套件中定义。
次要动机是在模块和类级别代码中简化中间计算,而不会污染最终的命名空间。
目的是,给定子句与执行指定操作的单独函数定义之间的关系,将类似于显式while循环与生成相同操作序列的生成器之间的现有关系。
本PEP中的具体提议受到了多年来对该概念及相关概念的各种探索(例如[1]、[2]、[3]、[6]、[8])的影响,并在一定程度上受到Haskell中where
和let
子句的启发。它避免了过去提案中发现的一些问题,但其本身尚未经过实现的检验。
提案
本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
新符号?
用于引用给定命名空间。它将是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 的定义带来了问题(它允许用“;”而不是“\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表达式、推导式和可能修改应用程序状态的任意表达式将是相当武断的。
given
子句还可以作为对某些lambda表达式和类似构造的更具可读性的替代,当将一次性函数传递给诸如sorted()
之类的操作或在基于回调的事件驱动编程中使用时。
在模块和类级别代码中,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
一切都与用户最初的想法保持相同的顺序,他们甚至不需要为排序标准想出名称:可以直接重用关键字参数名称。
对此提案的可能增强是提供一种便捷的简写语法,表示“将给定子句内容用作关键字参数”。即使没有专用语法,也可以简单地写成**vars(?)
。
有害于自省
在模块和类内部进行探测是白盒测试和交互式调试的宝贵工具。given
子句将非常有效地阻止访问计算过程中使用的临时状态(尽管在这方面,它不会比当前del
语句的使用更有效)。
虽然这是一个合理的问题,但可测试性设计是一个涉及编程许多方面的问题。如果一个组件需要独立测试,那么given
语句应该重构为单独的语句,以便信息暴露给测试套件。这与将隐藏在函数或生成器内部的操作重构到其自己的函数中,仅仅为了允许其独立测试,并没有显著区别。
缺乏实际影响评估
当前PEP中的例子几乎都是相对较小的“玩具”例子。本PEP中的提案需要通过应用于大型代码库(例如标准库或大型Twisted应用程序)的测试,以寻找实际代码的可读性真正得到增强的例子。
然而,这更多是PEP的不足,而非想法本身的不足。如果这不是一个实际问题,我们就不会收到这么多关于缺乏多行lambda支持的抱怨,Ruby的块构造可能也不会如此受欢迎。
开放问题
前向引用的语法
提议使用?
符号作为给定命名空间的前向引用,因为它简短、目前未使用且暗示“这里缺少一些稍后会填补的东西”。
PEP 中的提案与任何现有 Python 特性都不完全并行,因此故意避免重用已使用的符号。
nonlocal
和 global
的处理
nonlocal
和global
在given
子句套件中被明确禁止,如果出现将导致语法错误。如果它们出现在该套件内的def
语句中,它们将正常工作。
或者,它们可以定义为像上面扩展中定义的匿名函数一样操作。
break
和 continue
的处理
break
和continue
将按照上述扩展中定义的匿名函数那样操作。如果它们出现在given
子句套件中,它们将是语法错误,但如果它们作为该套件的一部分出现在for
或while
循环中,它们将正常工作。
return
和 yield
的处理
return
和yield
在given
子句套件中被明确禁止,如果出现将导致语法错误。如果它们出现在该套件内的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
最后修改时间:2025-02-01 08:59:27 GMT