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

Python 增强提案

PEP 505 – 支持 None 的运算符

作者:
Mark E. Haase <mehaase at gmail.com>, Steve Dower <steve.dower at python.org>
状态:
延期
类型:
标准轨迹
创建:
2015-09-18
Python 版本:
3.8

目录

摘要

几种现代编程语言有所谓的“null 合并”或“null 感知”运算符,包括 C# [1],Dart [2],Perl,Swift 和 PHP(从版本 7 开始)。 也有将它们添加到 ECMAScript(又名 JavaScript)的第 3 阶段草案提案 [3] [4]。 这些运算符为涉及空引用的常见模式提供了语法糖。

  • null 合并”运算符是一个二元运算符,如果它的左操作数不为 null,则返回该操作数。 否则,它返回其右操作数。
  • null 感知成员访问”运算符仅当该实例非 null 时访问实例成员。 否则,它返回 null。(这也被称为“安全导航”运算符。)
  • null 感知索引访问”运算符仅当该集合非 null 时访问集合中的元素。 否则,它返回 null。(这是另一种类型的“安全导航”运算符。)

这个 PEP 为 Python 提出了三个支持 None 的运算符,它们基于上述定义和其它语言的实现。 具体来说

  • None 合并”二元运算符 ?? 如果其左侧表达式计算结果不为 None,则返回左侧表达式,否则计算并返回右侧表达式。 包含合并 ??= 增强赋值运算符。
  • None 感知属性访问”运算符 ?.(“可能点”)如果左侧表达式计算结果不为 None,则计算整个表达式。
  • None 感知索引”运算符 ?[](“可能下标”)如果左侧表达式计算结果不为 None,则计算整个表达式。

有关所需语法更改的细节和示例,请参阅 语法更改 部分。

有关可以使用新运算符更新的代码的更实际示例,请参阅 示例 部分。

语法和语义

None 的特殊性

None 对象表示缺少值。 出于这些运算符的目的,缺少值表示表达式的其余部分也缺少值,不应计算。

一个被拒绝的提案是将任何在布尔上下文中计算为“假”的值视为没有值。 但是,这些运算符的目的是传播“缺少值”状态,而不是“假”状态。

有些人认为这使得 None 特殊。 我们认为 None 本来就很特殊,用它作为这些运算符的测试和结果不会以任何方式改变现有的语义。

有关替代方法的讨论,请参阅 被拒绝的想法 部分。

语法更改

Python 语法的以下规则已更新为:

augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
            '<<=' | '>>=' | '**=' | '//=' | '??=')

power: coalesce ['**' factor]
coalesce: atom_expr ['??' factor]
atom_expr: ['await'] atom trailer*
trailer: ('(' [arglist] ')' |
          '[' subscriptlist ']' |
          '?[' subscriptlist ']' |
          '.' NAME |
          '?.' NAME)

合并规则

coalesce 规则提供 ?? 二元运算符。 与大多数二元运算符不同,只有当左侧表达式确定为 None 时,才计算右侧表达式。

?? 运算符比其它二元运算符绑定得更紧密,因为这些运算符的大多数现有实现不会传播 None 值(它们通常会引发 TypeError)。 已知可能导致 None 的表达式可以替换为默认值,而无需额外的括号。

以下是一些在存在 ?? 运算符的情况下如何放置隐式括号以评估运算符优先级的示例:

a, b = None, None
def c(): return None
def ex(): raise Exception()

(a ?? 2 ** b ?? 3) == a ?? (2 ** (b ?? 3))
(a * b ?? c // d) == a * (b ?? c) // d
(a ?? True and b ?? False) == (a ?? True) and (b ?? False)
(c() ?? c() ?? True) == True
(True ?? ex()) == True
(c ?? ex)() == c()

特别是对于像 a ?? 2 ** b ?? 3 这样的情况,以任何其他方式对子表达式加括号会导致 TypeError,因为 int.__pow__ 无法使用 None 调用(并且使用 ?? 运算符本身就意味着 ab 可能为 None)。 但是,像往常一样,虽然括号不是必需的,但如果它有助于提高可读性,则应该添加。

还添加了 ?? 运算符的增强赋值运算符。 增强合并赋值运算符仅当其当前值为 None 时才会重新绑定名称。 如果目标名称已经具有值,则不会计算右侧表达式。 例如:

a = None
b = ''
c = 0

a ??= 'value'
b ??= undefined_name
c ??= shutil.rmtree('/')    # don't try this at home, kids

assert a == 'value'
assert b == ''
assert c == 0 and any(os.scandir('/'))

可能点和可能下标运算符

可能点和可能下标运算符被添加为原子的后缀,因此它们可以在与常规运算符相同的所有位置使用,包括作为赋值目标的一部分(下面将详细介绍)。 由于现有的评估规则没有直接嵌入语法中,因此我们在下面指定了所需的更改。

假设 atom 总是成功评估。 然后,每个 trailer 从左到右评估,将自己的参数(要么是它的参数、下标或属性名称)应用于生成下一个 trailer 的值。 最后,如果存在,将应用 await

例如,await a.b(c).d[e] 当前被解析为 ['await', 'a', '.b', '(c)', '.d', '[e]'] 并按以下方式评估:

_v = a
_v = _v.b
_v = _v(c)
_v = _v.d
_v = _v[e]
await _v

当存在 None 感知运算符时,从左到右的评估可能会短路。 例如,await a?.b(c).d?[e] 按以下方式评估:

_v = a
if _v is not None:
    _v = _v.b
    _v = _v(c)
    _v = _v.d
    if _v is not None:
        _v = _v[e]
await _v

注意

await 在这种情况下几乎肯定会失败,就像在代码尝试 await None 的情况下一样。 我们不建议在这里添加支持 Noneawait 关键字,只是为了完整性,将它包含在这个示例中,因为 atom_expr 语法规则包含该关键字。 如果它有自己的规则,我们绝不会提到它。

带括号的表达式由 atom 规则(上面未显示)处理,该规则会隐式地终止上面转换的短路行为。 例如,(a?.b ?? c).d?.e 按以下方式评估:

# a?.b
_v = a
if _v is not None:
    _v = _v.b

# ... ?? c
if _v is None:
    _v = c

# (...).d?.e
_v = _v.d
if _v is not None:
    _v = _v.e

当用作赋值目标时,支持 None 的运算符只能在“加载”上下文中使用。 也就是说,a?.b = 1a?[b] = 1 会引发 SyntaxError。 在表达式中更早的位置(a?.b.c = 1)是允许的,尽管除非与合并运算符组合,否则不太可能有用。

(a?.b ?? d).c = 1

阅读表达式

对于可能点和可能下标运算符,目的是表达式的含义应该与这些运算符的常规版本相同。 在“正常”情况下,像 a?.b?[c]a.b[c] 这样的表达式最终结果将是相同的,就像我们目前不将“a.b”读作“从 a 读取属性 b *如果它具有属性 a,否则它会引发 AttributeError*”一样,也不需要将“a?.b”读作“从 a 读取属性 b *如果 a 不为 None*”(除非在监听者需要了解特定行为的上下文中)。

对于使用 ?? 运算符的合并表达式,表达式应该要么读作“或 … 如果为 None”,要么读作“与 … 合并”。 例如,表达式 a.get_value() ?? 100 可以读作“调用 a 点 get_value 或 100 如果为 None”,或者“调用 a 点 get_value 与 100 合并”。

注意

在口语中阅读代码总是会有信息丢失,因此我们不试图定义一种不含糊的表达这些运算符的方式。 这些建议旨在为添加新语法的影响添加上下文。

示例

本节介绍了一些常见的支持 None 的模式示例,并展示了转换为使用支持 None 的运算符可能是什么样子。

标准库

使用 find-pep505.py 脚本 [5] 对 Python 3.7 标准库进行分析,发现高达 678 个代码片段可以用其中一个 None 敏感运算符来替换。

$ find /usr/lib/python3.7 -name '*.py' | xargs python3.7 find-pep505.py
<snip>
Total None-coalescing `if` blocks: 449
Total [possible] None-coalescing `or`: 120
Total None-coalescing ternaries: 27
Total Safe navigation `and`: 13
Total Safe navigation `if` blocks: 61
Total Safe navigation ternaries: 8

下面展示了一些在转换为使用新运算符之前和之后的示例。

来自 bisect.py

def insort_right(a, x, lo=0, hi=None):
    # ...
    if hi is None:
        hi = len(a)
    # ...

更新为使用 ??= 增量赋值语句后

def insort_right(a, x, lo=0, hi=None):
    # ...
    hi ??= len(a)
    # ...

来自 calendar.py

encoding = options.encoding
if encoding is None:
    encoding = sys.getdefaultencoding()
optdict = dict(encoding=encoding, css=options.css)

更新为使用 ?? 运算符后

optdict = dict(encoding=options.encoding ?? sys.getdefaultencoding(),
               css=options.css)

来自 email/generator.py(重要的是要注意,在这种情况下,无法用 or 代替 ??

mangle_from_ = True if policy is None else policy.mangle_from_

更新后

mangle_from_ = policy?.mangle_from_ ?? True

来自 asyncio/subprocess.py

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    if reader is not None:
        reader.feed_data(data)

更新为使用 ?. 运算符后

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    reader?.feed_data(data)

来自 asyncio/tasks.py

try:
    await waiter
finally:
    if timeout_handle is not None:
        timeout_handle.cancel()

更新为使用 ?. 运算符后

try:
    await waiter
finally:
    timeout_handle?.cancel()

来自 ctypes/_aix.py

if libpaths is None:
    libpaths = []
else:
    libpaths = libpaths.split(":")

更新后

libpaths = libpaths?.split(":") ?? []

来自 os.py

if entry.is_dir():
    dirs.append(name)
    if entries is not None:
        entries.append(entry)
else:
    nondirs.append(name)

更新为使用 ?. 运算符后

if entry.is_dir():
    dirs.append(name)
    entries?.append(entry)
else:
    nondirs.append(name)

来自 importlib/abc.py

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    found = self.find_spec(fullname, path)
    return found.loader if found is not None else None

部分更新后

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    return self.find_spec(fullname, path)?.loader

大量更新后(可以说过度了,但那是风格指南要决定的)

def find_module(self, fullname, path):
    return getattr(self, 'find_spec', None)?.__call__(fullname, path)?.loader

来自 dis.py

def _get_const_info(const_index, const_list):
    argval = const_index
    if const_list is not None:
        argval = const_list[const_index]
    return argval, repr(argval)

更新为使用 ?[]?? 运算符后

def _get_const_info(const_index, const_list):
    argval = const_list?[const_index] ?? const_index
    return argval, repr(argval)

jsonify

这个示例来自一个 Python 网页爬虫,它使用 Flask 框架作为前端。此函数从 SQL 数据库中检索有关网站的信息,并将其格式化为 JSON 以发送到 HTTP 客户端。

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen.isoformat() if site.first_seen is not None else None,
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen.isoformat() if site.last_seen is not None else None,
            url=site.url.rstrip('/')
        )

在数据库中,first_seenlast_seen 都允许为 null,在 JSON 响应中它们也允许为 null。JSON 没有原生方式来表示 datetime,因此服务器的协议规定,任何非 null 日期都用 ISO-8601 字符串表示。

在不知道 first_seenlast_seen 属性的确切语义的情况下,无法知道是否可以安全或高效地多次访问该属性。

修复此代码的一种方法是用显式值赋值和完整的 if/else 块替换每个条件表达式。

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        first_seen_dt = site.first_seen
        if first_seen_dt is None:
            first_seen = None
        else:
            first_seen = first_seen_dt.isoformat()

        last_seen_dt = site.last_seen
        if last_seen_dt is None:
            last_seen = None
        else:
            last_seen = last_seen_dt.isoformat()

        return jsonify(
            first_seen=first_seen,
            id=site.id,
            is_active=site.is_active,
            last_seen=last_seen,
            url=site.url.rstrip('/')
        )

这在函数中增加了十行代码和四个新的代码路径,显著增加了表面上的复杂性。使用 None 敏感属性运算符重写后,代码更短,意图更清晰。

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen?.isoformat(),
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen?.isoformat(),
            url=site.url.rstrip('/')
        )

Grab

下一个示例来自一个名为 Grab 的 Python 抓取库。

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        if ctype is None:
            return 'application/octet-stream'
        else:
            return ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        if filename is None:
            self.filename = self.get_random_filename()
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        if filename is None:
            self.filename = os.path.split(path)[1]
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

此示例包含多个需要提供默认值的良好示例。使用条件表达式重写减少了代码总行数,但不一定提高了可读性。

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return 'application/octet-stream' if ctype is None else ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = (self.get_random_filename() if filename
            is None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = (os.path.split(path)[1] if filename is
            None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

第一个三元表达式很整洁,但它反转了操作数的直观顺序:它应该在有值的情况下返回 ctype,并使用字符串字面量作为备用。其他三元表达式不直观,而且太长,必须换行。整体可读性下降,而不是提高。

使用 None 合并运算符重写后

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return ctype ?? 'application/octet-stream'

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = filename ?? self.get_random_filename()
        self.content_type = content_type ?? self.find_content_type(self.filename)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = filename ?? os.path.split(path)[1]
        self.content_type = content_type ?? self.find_content_type(self.filename)

此语法具有操作数的直观顺序。例如,在 find_content_type 中,首选值 ctype 位于备用值之前。语法的简洁性也减少了代码行数和视觉解析的代码量,从左到右和从上到下的阅读更准确地遵循执行流程。

被拒绝的想法

本节中的前三个想法是针对 None 的特殊处理的经常提出的替代方案。有关为何拒绝这些方案的更多背景信息,请参见它们在 PEP 531PEP 532 中的处理以及相关的讨论。

无值协议

可以通过定义一个协议来指示何时一个值表示“无值”来将运算符推广到用户定义的类型。这样的协议可能是一个 dunder 方法 __has_value__(self),如果该值应被视为具有值,则返回 True,如果该值应被视为无值,则返回 False

通过这种泛化,object 将实现一个等效于此的 dunder 方法

def __has_value__(self):
    return True

NoneType 将实现一个等效于此的 dunder 方法

def __has_value__(self):
    return False

在规范部分中,所有使用 x is None 都将被替换为 not x.__has_value__()

这种泛化将允许域特定的“无值”对象像 None 一样合并。例如,pyasn1 包含一个名为 Null 的类型,它表示一个 ASN.1 null

>>> from pyasn1.type import univ
>>> univ.Null() ?? univ.Integer(123)
Integer(123)

类似地,math.nanNotImplemented 等值可以被视为表示无值。

但是,这些值的“无值”性质是特定于域的,这意味着它们 *应该* 被语言视为值。例如,math.nan.imag 是定义良好的(它是 0.0),因此将 math.nan?.imag 短路以返回 math.nan 将是不正确的。

由于 None 已由语言定义为表示“无值”的值,并且当前规范不会阻止将来切换到协议(尽管对内置对象的更改将不兼容),因此此想法目前被拒绝。

布尔感知运算符

此建议本质上与添加无值协议相同,因此上面的讨论也适用。

类似于 ?? 运算符的行为可以用 or 表达式来实现,但是 or 检查其左操作数是否是假值,而不是专门是 None。这种方法很有吸引力,因为它需要对语言进行较少的更改,但最终没有正确地解决根本问题。

假设检查的是真值,而不是 None,那么就不再需要 ?? 运算符。但是,将此检查应用于 ?.?[] 运算符会阻止完全有效的操作应用。

考虑以下示例,其中 get_log_list() 可以返回包含当前日志消息的列表(可能为空),或者如果未启用日志记录,则返回 None

lst = get_log_list()
lst?.append('A log message')

如果 ?. 检查的是真值,而不是专门是 None,并且日志尚未用任何项目初始化,那么将永远不会追加任何项目。这违反了代码的明显意图,即追加项目。 append 方法可用于空列表,所有其他列表方法也是如此,没有理由假设这些成员不应该使用,因为列表目前为空。

此外,没有合理的替换表达式结果。普通的 lst.append 返回 None,但在这种情况下,lst?.append 可能会产生 []None,具体取决于 lst 的值。与上一节中的示例一样,这没有意义。

由于检查真值而不是 None 会导致表面上有效的表达式不再按预期执行,因此此想法被拒绝。

异常感知运算符

可以说,遇到 None 时对表达式进行短路的原因是避免在正常情况下会引发的 AttributeErrorTypeError。作为测试 None 的替代方案,?.?[] 运算符可以改为处理由操作引发的 AttributeErrorTypeError,并跳过表达式剩余部分。

这为 a?.b.c?.d.e 生成了类似于此的转换

_v = a
try:
    _v = _v.b
except AttributeError:
    pass
else:
    _v = _v.c
    try:
        _v = _v.d
    except AttributeError:
        pass
    else:
        _v = _v.e

一个悬而未决的问题是,当处理异常时,应将哪个值作为表达式的结果返回。上面的示例只是保留了部分结果,但这对用默认值替换没有帮助。另一种方法是强制结果为 None,这又引出一个问题,即为什么 None 特别到足以成为结果,但不够特别到足以成为测试。

其次,这种方法会掩盖在表达式中隐式执行的代码中的错误。对于 ?.,任何在属性或 __getattr__ 实现中的 AttributeError 都会被隐藏,对于 ?[]__getitem__ 实现也是如此。

类似地,诸如 {}?.ietms() 的简单类型错误可能无法被发现。

现有的通过 getattr 内置函数和由 dict 建立的 .get(key, default) 方法模式来处理这些类型错误的约定表明,已经可以显式地使用此行为。

由于这种方法会隐藏代码中的错误,因此被拒绝。

None 感知函数调用

None感知语法适用于属性和索引访问,因此似乎自然地会问是否也应该将其应用于函数调用语法。它可以写成 foo?(),其中 foo 仅在不为 None 时才被调用。

由于所提出的运算符旨在帮助遍历部分填充的层次数据结构,而不是遍历任意类层次结构,因此该提议已被推迟。这也反映在以下事实中:所有其他已经提供此语法的主流语言都没有发现为可选函数调用提供类似语法的必要性。

一种类似于 C# 使用的解决方法是编写 maybe_none?.__call__(arguments)。如果可调用对象为 None,则表达式将不会被求值。(C# 等效项在其可调用类型上使用 ?.Invoke()。)

? 一元后缀运算符

为了推广 None感知行为并限制引入的新运算符的数量,有人建议使用一个一元后缀运算符,拼写为 ?。这个想法是 ? 可能返回一个特殊对象,该对象可以覆盖返回 self 的 dunder 方法。例如,foo? 如果不为 None,则将求值为 foo,否则将求值为 NoneQuestion 的实例。

class NoneQuestion():
    def __call__(self, *args, **kwargs):
        return self

    def __getattr__(self, name):
        return self

    def __getitem__(self, key):
        return self

使用这个新运算符和新类型,类似 foo?.bar[baz] 的表达式如果 foo 为 None 则会求值为 NoneQuestion。这是一个巧妙的概括,但实际上很难使用,因为大多数现有代码不知道什么是 NoneQuestion

回到上面提到的动机示例之一,请考虑以下内容

>>> import json
>>> created = None
>>> json.dumps({'created': created?.isoformat()})

JSON 序列化器不知道如何序列化 NoneQuestion,任何其他 API 也不知道。该提议实际上需要在整个标准库和任何第三方库中大量专门的逻辑

同时,? 运算符也可能过于通用,因为它可以与任何其他运算符组合。以下表达式应该是什么意思?

>>> x? + 1
>>> x? -= 1
>>> x? == 1
>>> ~x?

这种程度的泛化没有用。本文实际提出的运算符有意限于几个预计将使编写常见代码模式变得更容易的运算符。

内置 maybe

Haskell 有一个名为 Maybe 的概念,它封装了可选值的概念,而不依赖于任何特殊关键字(例如 null)或任何特殊实例(例如 None)。在 Haskell 中,Maybe 的目的是避免对“某些东西”和“无”进行单独处理。

一个名为 pymaybe 的 Python 包提供了一个粗略的近似值。文档显示了以下示例

>>> maybe('VALUE').lower()
'value'

>>> maybe(None).invalid().method().or_else('unknown')
'unknown'

函数 maybe() 返回 Something 实例或 Nothing 实例。与上一节中描述的一元后缀运算符类似,Nothing 会覆盖 dunder 方法,以允许在丢失值上进行链接。

请注意,最终需要使用 or_else()pymaybe 的包装器中检索基础值。此外,pymaybe 不会短路任何求值。虽然 pymaybe 有一些优点,并且可能本身有用,但它也说明了为什么合并的纯 Python 实现不像语言内置支持那样强大。

添加内置 maybe 类型以启用此场景的想法被拒绝。

直接使用条件表达式

另一种常见的初始化默认值的方法是使用三元运算符。以下是流行的 Requests 包 的摘录

data = [] if data is None else data
files = [] if files is None else files
headers = {} if headers is None else headers
params = {} if params is None else params
hooks = {} if hooks is None else hooks

这种特殊的公式具有不希望有的效果,即以不直观的顺序放置操作数:大脑会认为,“如果可能,使用 data,并使用 [] 作为后备,”但代码将后备放在首选值之前

该包的作者可以改为这样编写

data = data if data is not None else []
files = files if files is not None else []
headers = headers if headers is not None else {}
params = params if params is not None else {}
hooks = hooks if hooks is not None else {}

这种操作数顺序更直观,但需要多 4 个字符(用于“not”)。它还突出显示了标识符的重复:data if datafiles if files 等等。

使用 None 合并运算符编写时,示例读为

data = data ?? []
files = files ?? []
headers = headers ?? {}
params = params ?? {}
hooks = hooks ?? {}

参考文献


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

最后修改时间:2023-09-09 17:39:29 GMT