PEP 572(Python 3.8中的赋值表达式)

哈Ha 这次,我们将讨论PEP 572,它讨论赋值表达式。如果您仍然对“:=”运算符表示怀疑,或者不完全了解其使用规则,那么本文适合您。在这里,您会找到许多示例和问题答案:“为什么会这样?” 事实证明,本文尽可能完整,如果您没有时间,请看一下我撰写的部分。在开始时,主要的“主题”是通过分配表达式来进行舒适的工作的。如果您发现错误,请事先原谅我(将它们写给我,我会解决)。开始吧:

PEP 572-赋值表达式

p572
标题:作业表达
作者:Chris Angelico <gmail.com上的rosuav>,蒂姆·彼得斯<gmail.com上的tim.peters>,Guido van Rossum <python.org上的guido>
讨论:python.org上的doc-sig
状态:公认
一种:标准
创建时间:2018年2月28日
Python版本:3.8
发表故事:2018年2月28日,2018年3月2日,2018年3月23日,2018年4月4日,2018年4月17日,2018年4月25日,2018年7月9日,2019年8月5日
准采用标准:mail.python.org/pipermail/python-dev/2018-July/154601.html(使用VPN的时间很长,但是会加载)
内容


注解


该约定将使用新的符号NAME:= expr讨论表达式内部赋值的可能性。

作为创新的一部分,已更新了计算字典生成器(词典理解)的过程。这样可以确保在值表达式之前先评估键表达式(这使您可以将键绑定到变量,然后在计算对应于键的值时重用创建的变量)。

在讨论此PEP期间,该运算符被非正式地称为“海象运算符”。构造的正式名称为“赋值表达式”(根据PEP:赋值表达式标题),但可以称为“命名表达式”。例如,CPython中的引用实现使用此名称。

理由


命名是编程的重要组成部分,它使您可以使用“描述性”名称代替较长的表达式,还可以轻松重用值。当前,这只能以指令的形式完成,这使得在生成列表(列表理解)以及其他表达式时此操作不可用。

此外,通过提供用于显示提示和中间结果的工具,对大型表达式的各个部分进行命名可以帮助进行交互式调试。由于无法捕获嵌套表达式的结果,您将需要更改源代码,但是使用赋值表达式,您只需要插入一些“名称:=表达式”形式的“标记”。这消除了不必要的重构,因此减少了调试期间意外代码更改的可能性(Heisenbugs的常见原因是在调试期间更改代码属性并在生产中意外出现的错误),并且该代码对于其他代码更易理解。给程序员。

实代码的重要性


在此PEP的开发过程中,许多人(支持者和批评家)一方面过于关注玩具示例,另一方面过于关注玩具示例。

玩具示例的危险有两个方面:它们过于抽象,以至于不能让人说“哦,这是不可抗拒的”,并且它们也很容易被拒绝,并带有“我永远不会写那个”的字眼。过于复杂的示例的危险在于,它们为批评家建议删除此功能提供了便利的环境(此类人士说:“这太令人困惑了”)。

但是,此类示例很有用:它们有助于阐明预期的语义。因此,我们将在下面给出其中一些。但是,令人信服的是,示例必须基于无需考虑此PEP而编写的真实代码。也就是说,该代码是真正有用的应用程序的一部分(没有什么区别:无论大小)。蒂姆·彼得斯(Tim Peters)通过查看他的个人存储库并选择他编写的代码示例为我们提供了很多帮助(如果认为)(使用狂热主义)使用赋值表达式对其进行了重写,那么这样做(在他看来)会更容易理解。他的结论是:当前的更改将对其代码的某些部分进行适度但明显的改进。

真实代码的另一个示例是对程序员如何重视紧凑性的间接观察。 Guido van Rossum检查了Dropbox代码库,发现一些证据表明程序员比使用一些小表达式更喜欢编写更少的代码行。

例子:Guido在程序员重复一个子表达式(从而减慢了程序速度)时发现了几个说明性要点,但是节省了额外的代码行。例如,代替编写:

match = re.match(data)
group = match.group(1) if match else None

程序员首选此选项:

group = re.match(data).group(1) if re.match(data) else None

这是另一个示例,表明程序员有时愿意做更多的工作来保持缩进的“先前级别”:

match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
    result = match1.group(1)
elif match2:
    result = match2.group(2)
else:
    result = None

即使pattern1已经匹配,此代码也会计算pattern2(在这种情况下,第二个子条件将永远不会被满足)。因此,以下解决方案更有效,但吸引力较小:

match1 = pattern1.match(data)
if match1:
    result = match1.group(1)
else:
    match2 = pattern2.match(data)
    if match2:
        result = match2.group(2)
    else:
        result = None

语法和语义


在大多数情况下,Python使用任意表达式,您现在可以使用赋值表达式。它们的形式为NAME:= expr,其中expr是任何有效的Python表达式,但未加括号的元组除外,而NAME是标识符。这样的表达式的值与原始值一致,但另外的效果是将值分配给目标对象:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
   process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]

特殊情况


为了避免用户之间的歧义或混淆,在很多地方都不允许使用赋值表达式:

  • 在“上”级别禁止未用括号括起来的赋值表达式:

    y := f(x)  # 
    (y := f(x))  # ,   

    该规则将使程序员更容易在赋值运算符和赋值表达式之间进行选择-不会出现两个选项都相等的句法情况。
  • . :

    y0 = y1 := f(x)  # 
    y0 = (y1 := f(x))  # ,   

    . :

    foo(x = y := f(x))  # 
    foo(x=(y := f(x)))  # ,     

    , .
  • . :

    def foo(answer = p := 42):  # 
        ...
    def foo(answer=(p := 42)):  # Valid, though not great style
        ...

    , (. , «» ).
  • , . :

    def foo(answer: p := 42 = 5):  # 
        ...
    def foo(answer: (p := 42) = 5):  # ,  
        ...

    : , "=" ":=" .
  • -. :

    (lambda: x := 1) # 
    lambda: (x := 1) # ,  
    (x := lambda: 1) # 
    lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid

    - , ":=". . , , () , .
  • f- . :

    >>> f'{(x:=10)}'  # ,  
    '10'
    >>> x = 10
    >>> f'{x:=10}'    # ,  ,  '=10'
    '        10'

    , , f-, . f- ":" . , f- . , .


赋值表达式不会引入新的作用域。在大多数情况下,创建变量的范围不需要解释:它将是最新的。如果变量以前使用过非本地或全局关键字,则赋值表达式将对此加以考虑。为此,只有lambda(作为函数的匿名定义)被认为是单独的范围。

有一种特殊情况:在列表,集合,字典的生成器中或在“生成器的表达式”本身中发生的赋值表达式(以下统称为“生成器”(理解))将变量绑定到生成器包含的范围,并观察globab修饰符或非全局(如果存在)。

这种特殊情况的理由是双重的。首先,它使我们可以方便地捕获any()和all()表达式中的“成员”,例如:

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

if all((nonblank := line).strip() == '' for line in lines):
    print("All lines are blank")
else:
    print("First non-blank line:", nonblank)

其次,它提供了一种紧凑的方式来更新生成器中的变量,例如:

# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)

但是,赋值表达式中的变量名称不能与for循环中已在生成器中使用的名称匹配。姓氏对于出现它们的生成器来说是本地的。如果赋值表达式也引用生成器中的范围,那将是不一致的。

例如,[i:= i + 1 for range in 5(5)]是无效的:for循环确定i对生成器而言是本地的,但“ i:= i + 1”部分坚持认为i是来自外部的变量范围 出于相同的原因,以下示例将不起作用:


[[(j := j) for i in range(5)] for j in range(5)] # 
[i := 0 for i, j in stuff]                       # 
[i+1 for i in (i := stuff)]                      # 

尽管在技术上可以为此类情况指定一致的语义,但是很难确定我们对这种语义的理解方式是否可以在您的实际代码中使用。这就是为什么参考实现可确保此类情况引发SyntaxError而不是根据未定义的行为执行,具体取决于特定的硬件实现。即使从未执行赋值表达式,此限制也适用:

[False and (i := 0) for i, j in stuff]     # 
[i for i, j in stuff if True or (j := 1)]  # 

# [.  . - ""   
# ,       
# ,    ,   ]

对于生成器主体(第一个关键字“ for”之前的部分)和过滤器表达式(“ if”之后以及任何嵌套的“ for”之前的部分),此限制仅适用于同时用作迭代变量的变量名称。正如我们已经说过的,Lambda表达式引入了函数的新的显式范围,因此可以在生成器的表达式中使用而没有其他限制。 [大约再次,除非在这种情况下:[i代表i在(2,(lambda:(s:= 2)()))的范围内]]

由于参考实现中的设计限制(符号表分析器无法识别生成器左侧的名称是否在可迭代表达式所在的其余部分中使用),因此,完全禁止将赋值表达式作为可迭代表达式的一部分(在每个“ in”和“ in”之后的部分中)在任何后续关键字“ if”或“ for”之前)。也就是说,所有这些情况都是不可接受的:

[i+1 for i in (j := stuff)]                    # 
[i+1 for i in range(2) for j in (k := stuff)]  # 
[i+1 for i in [j for j in (k := stuff)]]       # 
[i+1 for i in (lambda: (j := stuff))()]        # 

当在类范围内的生成器中使用赋值表达式时,会发生另一个异常。如果在使用上述规则时,应该创建在范围内重新测量的类,则这样的赋值表达式无效并且将导致SyntaxError:

class Example:
    [(j := i) for i in range(5)]  # 

(最后一个例外的原因是生成器创建的函数的隐式范围-当前没有函数的运行时机制来引用位于类范围内的变量,因此我们不想添加这种机制。如果解决了此问题,则该特殊情况(可能)将从赋值表达式的规范中删除。请注意,即使您在类的作用域中较早地创建了变量,并尝试使用生成器中的赋值表达式对其进行更改,也会出现此问题。)

有关示例的示例,请参见附录B。生成器中找到的赋值表达式将转换为等效代码。

相对优先级:=


在可能的所有句法位置,将= =运算符分组为比逗号强,但比所有其他运算符(包括or,和,非条件条件表达式,A和C,否则为B)弱。如上文“例外情况”部分所述,赋值表达式永远无法在与经典赋值=相同的“级别”下工作。如果需要不同的操作顺序,请使用括号。

运算符:=可以在调用函数的位置参数时直接使用。但是,这不能直接在参数中使用。一些示例阐明了技术上允许的和不可能的:

x := 0 # 

(x := 0) #  

x = y := 0 # 

x = (y := 0) #  

len(lines := f.readlines()) # 

foo(x := 3, cat='vector') # 

foo(cat=category := 'vector') # 

foo(cat=(category := 'vector')) #  

不建议在实践中使用以上大多数“有效”示例,因为快速扫描您的源代码的人可能无法正确理解其含义。但是在简单的情况下,这是允许的:

# Valid
if any(len(longline := line) >= 100 for line in lines):
    print("Extremely long line:", longline)

该PEP建议您绝对在以下位置加上空格:=,类似于PEP 8对于=的经典分配建议。(最后一个建议的区别是它禁止在=周围使用空格,该空格用于将键参数传递给函数。)

更改计算顺序。


为了具有明确定义的语义,此协议要求明确定义评估过程。从技术上讲,这不是新要求。Python已经有一个规则,即通常从左到右评估子表达式。但是,赋值表达式使这些“副作用”更加明显,我们建议对当前的计算顺序进行一次更改:

  • 在字典生成器{X:Y for ...}中,当前会在X之前评估Y。我们建议更改此值,以便在Y之前计算X。(在经典dict中,例如{X:Y}以及dict ((……的(X,Y))已经实现。因此,字典生成器必须遵守此机制。)


赋值表达式和赋值指令之间的差异。


最重要的是,“:=”是一个表达式,这意味着它可以在指令无效的情况下使用,包括lambda函数和生成器。相反,赋值表达式不支持可在赋值指令中使用的扩展功能

  • 不直接支持级联分配

    x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
  • 除简单变量名称NAME之外,不支持单独的“目标”:

    # No equivalent
    a[i] = x
    self.rest = []
  • 功能和围绕逗号的优先级有所不同:

    x = 1, 2  # Sets x to (1, 2)
    (x := 1, 2)  # Sets x to 1
  • 开箱价和装箱价没有``纯''等价物或根本不被支持

    # Equivalent needs extra parentheses
    loc = x, y  # Use (loc := (x, y))
    info = name, phone, *rest  # Use (info := (name, phone, *rest))
    
    # No equivalent
    px, py, pz = position
    name, phone, email, *other_info = contact
  • 不支持内联类型注释:

    # Closest equivalent is "p: Optional[int]" as a separate declaration
    p: Optional[int] = None
  • 没有简化的操作形式:

    total += tax  # Equivalent: (total := total + tax)

实施过程中的规格变更


在本PEP首次编写之后以及Python 3.8发行之前,根据我们的经验和其他分析进行了以下更改:

  • 为了确保与其他类似异常的一致性,并且不引入可能对最终用户不方便的新名称,最初建议的SyntaxError的TargetScopeError子类已被删除,并减少为通常的SyntaxError。[3]
  • 由于解析CPython字符表的限制,赋值表达式的引用实现为迭代器内的所有使用引发SyntaxError。以前,仅当要创建的变量的名称与迭代表达式中已使用的名称一致时,才会发生此异常。如果有足够有说服力的示例,可以对此进行修订,但是对于纯粹的“假设”用例而言,额外的复杂性似乎不合适。

例子


Python标准库示例


site.py


env_base仅在条件中使用,因此可以将分配作为逻辑块的“头”放置在其中。

  • 当前代码:
    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base
  • 改进的代码:
    if env_base := os.environ.get("PYTHONUSERBASE", None):
        return env_base

_pydecimal.py


您可以避免嵌套的ifs,从而消除一级缩进。

  • 当前代码:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • 改进的代码:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

复制


该代码看起来更经典,并且避免了条件语句的多重嵌套。(请参阅附录A,以了解有关此示例来源的更多信息。)

  • 当前代码:
    reductor = dispatch_table.get(cls)
    if reductor:
        rv = reductor(x)
    else:
        reductor = getattr(x, "__reduce_ex__", None)
        if reductor:
            rv = reductor(4)
        else:
            reductor = getattr(x, "__reduce__", None)
            if reductor:
                rv = reductor()
            else:
                raise Error(
                    "un(deep)copyable object of type %s" % cls)
  • 改进的代码:

    if reductor := dispatch_table.get(cls):
        rv = reductor(x)
    elif reductor := getattr(x, "__reduce_ex__", None):
        rv = reductor(4)
    elif reductor := getattr(x, "__reduce__", None):
        rv = reductor()
    else:
        raise Error("un(deep)copyable object of type %s" % cls)

datetime.py


tz仅用于s + = tz。向内移动它有助于显示其合理的使用范围。

  • 当前代码:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    tz = self._tzstr()
    if tz:
        s += tz
    return s
  • 改进的代码:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    if tz := self._tzstr():
        s += tz
    return s

sysconfig.py


在if循环中,在while循环中将fp.readline()作为“条件”来调用(以及调用.match()方法),使代码更紧凑,而不会增加对其的理解。

  • 当前代码:

    while True:
        line = fp.readline()
        if not line:
            break
        m = define_rx.match(line)
        if m:
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        else:
            m = undef_rx.match(line)
            if m:
                vars[m.group(1)] = 0
  • 改进的代码:

    while line := fp.readline():
        if m := define_rx.match(line):
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        elif m := undef_rx.match(line):
            vars[m.group(1)] = 0

简化列表生成器


现在,可以通过“捕获”条件来有效地过滤列表生成器:

results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]

之后,该变量可以在另一个表达式中重用:

stuff = [[y := f(x), x/y] for x in range(5)]

请再次注意,在两种情况下,变量y与变量result和stuff的作用域相同。

在条件下获取价值


赋值表达式可以在if或while语句的条件下有效使用:

# Loop-and-a-half
while (command := input("> ")) != "quit":
    print("You entered:", command)

# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
    print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

# Reading socket data until an empty string is returned
while data := sock.recv(8192):
    print("Received data:", data)

尤其是,这种方法可以消除创建无限循环,分配和条件检查的需要。它还允许您在使用函数调用作为其条件的循环以及不仅检查条件,还使用将来函数返回的实际值的循环之间绘制平滑的并行关系。

叉子


UNIX底层世界的一个示例:Fork()是类Unix操作系统上的系统调用,它相对于父级创建一个新的子进程。

if pid := os.fork():
    # Parent code
else:
    # Child code

拒绝的选择


通常,类似的建议在python社区中非常普遍。下面是赋值表达式的许多替代语法,这些语法太过具体而难以理解,因此已被拒绝使用。

更改生成器的范围


在此PEP的先前版本中,建议对生成器的作用域规则进行细微更改,以使其更适合在类的作用域中使用。但是,这些建议将导致向后不兼容,因此被拒绝。因此,该PEP仅能够完全专注于赋值表达式。

替代拼写


通常,建议的赋值表达式具有相同的语义,但编写方式不同。

  1. 以名称的身份浏览:

    stuff = [[f(x) as y, x/y] for x in range(5)]

    EXPR as NAME import, except with, (, ).

    ( , «with EXPR as VAR» EXPR VAR, EXPR.__enter__() VAR.)

    , ":=" :
    • , if f(x) as y , ​​ if f x blah-blah, if f(x) and y.
    • , as , , :
      • import foo as bar
      • except Exc as var
      • with ctxmgr() as var

      , as if while , as « » .
    • «»
      • NAME = EXPR
      • if NAME := EXPR

      .
  2. EXPR -> NAME

    stuff = [[f(x) -> y, x/y] for x in range(5)]

    , R Haskell, . ( , - y < — f (x) Python, - .) «as» , import, except with, . Python ( ), ":=" ( Algol-58) .
  3. «»

    stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as"
    stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="

    . Python, , .
  4. where: :

    value = x**2 + 2*x where:
        x = spam(1, 4, 7, q)

    ( , «»). , «» ( with:). . PEP 3150, ( given: ).
  5. TARGET from EXPR:

    stuff = [[y from f(x), x/y] for x in range(5)]

    这句法不太可能与他人发生冲突比(除非你从EXC结构加薪EXC),但在其他方面可以媲美他们。该选项不是将expr作为目标的并行:(这可能有用,但也可能造成混淆),该选项根本没有与任何并行的内容,但是令人惊讶的是,它记忆犹新。


条件语句中的特殊情况


赋值表达式最常见的用例之一是if和while语句。代替使用更通用的解决方案,使用as可以通过添加一种捕获要比较的值的方式来改进这两个语句的语法:

if re.search(pat, text) as match:
    print("Found:", match.group(0))

这可以正常工作,但是当所需条件基于返回值的“正确性”时才有效。因此,此方法在特定情况下有效(检查正则表达式,读取套接字,在执行结束时返回空字符串),在更复杂的情况下(例如,当条件为f(x)<0且您想要保存f(x)的值)。同样,这在列表生成器中没有意义。

优点:没有句法歧义。缺点:即使仅在if / while语句中使用它,它也仅在某些情况下有效。

发电机的特殊情况


赋值表达式的另一个常见用例是生成器(列表/集合/字典和genexps)。如上所述,针对具体解决方案提出了建议。

  1. 在哪里,让,或给定:

    stuff = [(y, x/y) where y = f(x) for x in range(5)]
    stuff = [(y, x/y) let y = f(x) for x in range(5)]
    stuff = [(y, x/y) given y = f(x) for x in range(5)]

    此方法在for循环和主表达式之间产生子表达式。它还引入了一个附加的语言关键字,该关键字可能会产生冲突。在这三个选项中,where是最干净,最易读的,但是仍然存在潜在的冲突(例如,SQLAlchemy和numpy拥有其where方法,以及标准库中的tkinter.dnd.Icon)。
  2. NAME = EXPR:

    stuff = [(y, x/y) with y = f(x) for x in range(5)]

    , , with. . , «» for. C, , . : « «with NAME = EXPR:» , ?»
  3. with EXPR as NAME:

    stuff = [(y, x/y) with f(x) as y for x in range(5)]

    , as, . , for. with

无论选择哪种方法,都会通过for循环在生成器及其部署版本之间引入明显的语义差异。如果不处理创建变量的阶段,就不可能将循环包装在生成器中。可以为此任务重定向的唯一关键字是with但这将在代码的不同部分赋予它不同的语义,这意味着您需要创建一个新的关键字,但这会涉及很多成本。

降低操作员优先级


:=运算符具有两个逻辑优先级。或者它应该具有尽可能低的优先级(与赋值运算符相当)。或者它应该比比较运算符具有更高的优先级。将其优先级放在比较运算符和算术运算之间(准确地说:略低于按位OR)将使您在大多数情况下(如果使用)不使用方括号,因为您更有可能希望保留某对象的值之前如何对其进行比较:

pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
    ...

一旦find()返回-1,循环就结束了。如果:=与=一样自由地绑定操作数,则find()的结果将首先被“捕获”到比较运算符中,并且通常将返回True或False,这用处不大。

尽管此行为在许多情况下在实践中会很方便,但将更难以解释。因此,我们可以说“运算符:=的行为与通常的赋值运算符相同”。也就是说,为:=设置的优先级应尽可能接近运算符=(除了:=的优先级高于逗号)。

您在右边加上逗号


一些批评家认为,赋值表达式应在不添加方括号的情况下识别元组,以便使两个条目等效:

(point := (x, y))
(point := x, y)

(在标准的当前版本中,最后一条记录将与表达式((point:= x),y)等效。)

但是在这种情况下,当在函数调用中使用赋值表达式时,它的优先级也比逗号低,这是合乎逻辑的以下是令人困惑的等效项:

foo (x: = 1, y)
foo (x: = (1, y))

而且,我们得到了唯一不那么令人困惑的出路:将:=运算符的优先级设置为比逗号低的优先级。

总是需要括号


一直建议将赋值表达式括起来。这将为我们节省许多歧义。实际上,通常需要使用括号来提取所需的值。但是在以下情况下,括号在我们看来显然是多余的:

# Top level in if
if match := pattern.match(line):
    return match.group(1)

# Short call
len(lines := f.readlines())

经常异议


为什么不只将赋值语句转换为表达式?


C和类似语言将=运算符定义为表达式,而不是指令,就像Python一样。这允许在许多情况下进行分配,包括比较变量的地方。if(x == y)和if(x = y)之间的句法相似之处与它们截然不同的语义相矛盾。因此,此PEP引入了运算符:=来阐明它们之间的差异。

如果存在分配指令,为什么还要麻烦分配表达式


这两种形式具有不同的灵活性。运算符:=可以在较大的表达式内使用,而在=运算符中,可以由类型“ + =”的“小型运算符族”使用。Also =允许您通过属性和索引分配值。

为什么不使用本地范围并防止名称空间污染?


该标准的先前版本包括一个用于赋值表达式的实际局部范围(仅限于一条语句),可防止名称泄漏和名称空间污染。尽管在某些情况下这提供了一定的优势,但在其他情况下却使任务变得复杂,并且现有方法的优势并不能证明其优势。这样做是为了简化语言。您不再需要此变量?有一个解决方案:使用del关键字删除变量,或在其名称下添加一个下划线。

(作者要感谢Guido van Rossum和Christophe Groth提出的朝这个方向发展PEP标准的建议。[2])

风格建议


由于赋值表达式有时可以与赋值运算符同等使用,因此出现了一个问题,哪个仍然是首选?..根据其他样式约定(例如PEP 8),有两个建议:

  1. 如果您可以同时使用两个分配选项,请优先选择运算符。他们最清楚地表达了您的意图。
  2. 如果使用赋值表达式导致执行顺序不明确,请使用经典运算符重写代码。

谢谢


该标准的作者要感谢Nick Coghlan和Steven D'Aprano对本PEP所做的重要贡献,并感谢Python Core Mentorship成员为实现这一目标所提供的帮助。

附录A:Tim Peters的结论


这是蒂姆·彼得斯(Tim Peters)撰写的有关该主题的简短文章。

我不喜欢“混淆”的代码,也不喜欢将概念上不相关的逻辑放在一行上。因此,例如,代替:

i = j = count = nerrors = 0

我更喜欢这样写:

i = j = 0
count = 0
nerrors = 0

因此,我想我会找到几个要使用赋值表达式的地方。我什至不想谈论它们在已经拉伸到一半屏幕的表达式中的用法。在其他情况下,这种行为如下:

mylast = mylast[1]
yield mylast[0]

明显优于此:

yield (mylast := mylast[1])[0]

这两个代码具有完全不同的概念,将它们混合会很疯狂。在其他情况下,组合逻辑表达式会使代码的理解复杂化。例如,重写:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

简而言之,我们失去了“逻辑”。您需要了解此代码的工作方式。我的大脑不想这样做:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

但是这种情况很少见。保存结果的任务非常普遍,“稀疏胜于稠密”并不意味着“几乎空着胜于稀疏”。Zen Python的参考]。例如,我有许多函数返回None或0表示“我没有任何用处,但是由于这种情况经常发生,因此我不想因异常而困扰您”。实际上,这种机制也用在没有匹配项时返回None的正则表达式中。因此,在此示例中,有很多代码:

result = solution(xs, n)
if result:
    # use result

我发现以下选项更容易理解,当然也更具可读性:

if result := solution(xs, n):
    # use result

刚开始我对此并不十分重视,但是如此短的结构经常出现,以至于很快就使我烦恼,以至于我无法使用它。这让我感到惊讶![大约 显然,这是在Python 3.8正式发布之前编写的。]

在其他情况下,赋值表达式实际上是“错误的”。Kirill Balunov不再在我的代码中反复翻腾,而是给出了标准copy.py库中copy()函数的一个很好的示例:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error("un(shallow)copyable object of type %s" % cls)

不断增加的缩进具有误导性:毕竟,逻辑是平坦的:第一个成功的测试“获胜”:

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(shallow)copyable object of type %s" % cls)

赋值表达式的简单使用使代码的视觉结构可以强调逻辑的“平面”。但是,不断增加的缩进使其隐含。

这是代码中的另一个小示例,这使我非常高兴,因为它使我可以将内部相关的逻辑放在一行上,并消除了烦人的“人造”缩进级别。这正是我从if语句中获得的结果,它使阅读更加轻松。如下代码:

diff = x - x_base
if diff:
    g = gcd(diff, n)
    if g > 1:
        return g

转换成:

if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
    return g

因此,在大多数发生变量赋值的行中,我不会使用赋值表达式。但是这种设计是如此频繁,以至于我仍然有很多地方可以利用这个机会。在最近的情况下,我经常赢了一点。在其余的子部分中,这导致了中等或较大的改进。因此,如果使用赋值表达式,则比使用三重表达式要多得多,但比扩展赋值[大约。简短选项:* =,/ =,+ =等)。

数值例子


我还有另一个使我印象深刻的例子。

如果所有变量都是正整数,并且变量a大于x的第n个根,则此算法返回x的第n个根的“较低”舍入(并使每次迭代的精确位数大约加倍):

while a > (d := x // a**(n-1)):
    a = ((n-1)*a + d) // n
return a

原因尚不清楚,但是这种算法的变体要比带有条件分支中断的无限循环(循环半)不那么明显。在不依靠数学陈述(“算术平均值-几何平均值不等式”)并且不了解嵌套舍入函数如何向下行为的一些重要问题上,也很难证明这种实现的正确性。但是这里的问题已经在数学中,而不是编程中。

而且,如果您了解所有这些,那么使用赋值表达式的选项就很容易阅读,就像一个简单的句子:“检查当前的”猜测”,如果它太大,则减小它”,该条件允许您立即从循环条件中保存中间值。我认为经典形式更难理解:

while True:
    d = x // a**(n-1)
    if a <= d:
        break
    a = ((n-1)*a + d) // n
return a

附录B:生成器的粗糙代码解释器


本附录试图阐明(尽管未指定)在生成器表达式中创建变量的规则。对于许多说明性示例,我们显示了源代码,其中将生成器替换为等效函数并结合了一些“脚手架”。

由于[x for ...]等同于list(x for ...),因此这些示例不会失去通用性。而且,由于这些示例仅旨在阐明一般规则,因此它们并不声称是现实的。

注意:现在,通过创建嵌套的生成器函数(类似于本附录中给出的函数)来实现生成器。这些示例显示了新的部分,该部分添加了适当的功能来处理赋值表达式的范围(这种范围就好像赋值是在包含最外部生成器的块中执行的一样)。为了简化“类型推断”,这些说明性示例未考虑赋值表达式是可选的(但它们考虑了在生成器内部创建的变量的范围)。

让我们首先回想一下没有赋值表达式的生成器“在幕后”生成的代码:

  • 源代码(EXPR最常使用VAR变量):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • 转换后的代码(不用担心名称冲突):

    def f():
        def genexpr(iterator):
            for VAR in iterator:
                yield EXPR
        a = list(genexpr(iter(ITERABLE)))


让我们添加一个简单的赋值表达式。

  • 资源:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • 转换后的代码:

    def f():
        if False:
            TARGET = None  # Dead code to ensure TARGET is a local variable
        def genexpr(iterator):
            nonlocal TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

现在让我们将全局TARGET语句添加到f()函数的声明中。

  • 资源:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • 转换后的代码:

    def f():
        global TARGET
        def genexpr(iterator):
            global TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

反之亦然,让我们将非本地TARGET添加到f()函数的声明中。

  • 资源:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • 转换后的代码:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            def genexpr(iterator):
                nonlocal TARGET
                for VAR in iterator:
                    TARGET = EXPR
                    yield TARGET
            a = list(genexpr(iter(ITERABLE)))

最后,让我们放入两个生成器。

  • 资源:

    def f():
        a = [[TARGET := i for i in range(3)] for j in range(2)]
        # I.e., a = [[0, 1, 2], [0, 1, 2]]
        print(TARGET)  # prints 2
    
  • 转换后的代码:

    def f():
        if False:
            TARGET = None
        def outer_genexpr(outer_iterator):
            nonlocal TARGET
            def inner_generator(inner_iterator):
                nonlocal TARGET
                for i in inner_iterator:
                    TARGET = i
                    yield i
            for j in outer_iterator:
                yield list(inner_generator(range(3)))
        a = list(outer_genexpr(range(2)))
        print(TARGET)

附录C:范围语义未更改


请注意,在Python中,作用域语义没有改变。局部函数的范围仍在编译时确定,并且在运行时(关闭)具有不确定的时间范围。例:

a = 42
def f():
    # `a` is local to `f`, but remains unbound
    # until the caller executes this genexp:
    yield ((a := i) for i in range(3))
    yield lambda: a + 100
    print("done")
    try:
        print(f"`a` is bound to {a}")
        assert False
    except UnboundLocalError:
        print("`a` is not yet bound")

然后:

>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42

参考文献


  1. 概念验证的实施
  2. 讨论赋值表达式的语义(VPN严格但已加载)
  3. 对PEP 572中TargetScopeError的讨论(与上一个类似地加载)

版权


该文档已公开发布。

资料来源:github.com/python/peps/blob/master/pep-0572.rst

我这边


首先,让我们总结一下:
  • 为了避免人们试图消除语义对偶性,在很多可以同时使用“ =”和“:=”的“经典”地方,都有一些限制,因此运算符:: =必须经常放在方括号中。这些情况将必须在描述基本用法的部分中进行审查
  • 赋值表达式的优先级略高于逗号。因此,在分配过程中不会形成元组。将参数传递给函数时,还可以使用:=运算符。
  • , , , . . lambda , «» .
  • : ,
  • , .
  • / .
  • , .

最后,我想说我喜欢新的运算符。它使您可以在条件,“过滤器”列表中编写更扁平的代码,并(最终)在if之前删除“相同”的孤独行。如果人们将赋值表达式用于其预期目的,那么这将是一个非常方便的工具,它将增加代码的可读性和美观性(尽管对于任何功能语言来说都可以这么说……)。

All Articles