PEP 572 (Expressões de atribuição no python 3.8)

Olá, Habr. Desta vez, veremos o PEP 572, que fala sobre expressões de atribuição. Se você ainda é cético em relação ao operador ": =" ou não entende completamente as regras de uso, este artigo é para você. Aqui você encontrará muitos exemplos e respostas para a pergunta: "Por que isso acontece?" Este artigo acabou sendo o mais completo possível e, se você tiver pouco tempo, observe a seção que escrevi. No início, as principais “teses” são coletadas para um trabalho confortável com expressões de atribuição. Perdoe-me com antecedência se você encontrar erros (escreva sobre eles para mim, eu vou consertar). Vamos começar:

PEP 572 - Expressões de atribuição

Pep572
Título:Expressões de atribuição
Autores:Chris Angelico <rosuav em gmail.com>, Tim Peters <tim.peters em gmail.com>, Guido van Rossum <guido em python.org>
Discussão:doc-sig em python.org
Estado:Aceitaram
Um tipo:Padrão
Criada:28 de fevereiro de 2018
Versão Python:3.8
Publicar história:28-fev-2018, 02-mar-2018, 23-mar-2018, 04-abr-2018, 17-abr-2018, 25-abr-2018, 09-jul-2018, 05-ago-2019
Permissão para adotar o padrão:mail.python.org/pipermail/python-dev/2018-July/154601.html (com VPN por um longo tempo, mas é carregado)
Conteúdo


anotação


Esta convenção abordará a possibilidade de atribuição dentro de expressões, usando a nova notação NAME: = expr.

Como parte das inovações, o procedimento para calcular geradores de dicionário (compreensão de dicionário) foi atualizado. Isso garante que a expressão da chave seja avaliada antes da expressão do valor (isso permite vincular a chave a uma variável e, em seguida, reutilizar a variável criada no cálculo do valor correspondente à chave).

Durante uma discussão sobre esse PEP, esse operador ficou oficialmente conhecido como operador de morsa. O nome formal da construção é "Expressão de designação" (de acordo com o cabeçalho PEP: Expressões de designação), mas pode ser chamado de "Expressões nomeadas". Por exemplo, a implementação de referência no CPython usa esse mesmo nome.

Justificação


A nomeação é uma parte importante da programação que permite o uso de um nome "descritivo" em vez de uma expressão mais longa, além de facilitar a reutilização de valores. Atualmente, isso só pode ser feito na forma de instruções, o que torna esta operação indisponível ao gerar listas (compreensão da lista), bem como em outras expressões.

Além disso, nomear partes de uma expressão grande pode ajudar na depuração interativa, fornecendo ferramentas para exibir prompts e resultados intermediários. Sem a capacidade de capturar os resultados de expressões aninhadas, você precisará alterar o código-fonte, mas, usando as expressões de atribuição, basta inserir alguns "marcadores" no formulário "nome: = expressão". Isso elimina refatoração desnecessária e, portanto, reduz a probabilidade de alterações não intencionais do código durante a depuração (uma causa comum de Heisenbugs são erros que alteram as propriedades do código durante a depuração e podem aparecer inesperadamente na produção]), e esse código será mais compreensível para outro para o programador.

A Importância do Código Real


Durante o desenvolvimento deste PEP, muitas pessoas (tanto defensores quanto críticos) estavam muito focadas em exemplos de brinquedos, por um lado, e exemplos excessivamente complexos, por outro.

O perigo dos exemplos de brinquedos é duplo: eles geralmente são muito abstratos para fazer alguém dizer "ah, isso é irresistível" e também são facilmente rejeitados com as palavras "eu nunca escreveria isso". O perigo de exemplos excessivamente complexos é que eles fornecem um ambiente conveniente para os críticos, sugerindo que essa funcionalidade seja removida ("Isso é muito confuso", dizem essas pessoas).

No entanto, há um bom uso para tais exemplos: eles ajudam a esclarecer a semântica pretendida. Portanto, daremos alguns deles abaixo. No entanto, para ser convincente , os exemplos devem se basear emcódigo real que foi escrito sem pensar neste PEP. Ou seja, o código que faz parte de um aplicativo realmente útil (sem diferença: seja grande ou pequeno). Tim Peters nos ajudou muito olhando seus repositórios pessoais e escolhendo exemplos do código que ele escreveu, o que (em sua opinião) seria mais compreensível se eles fossem reescritos (sem fanatismo) usando expressões de atribuição. Sua conclusão é a seguinte: as mudanças atuais trariam uma melhoria modesta, mas óbvia, em alguns bits de seu código.

Outro exemplo de código real é a observação indireta de como os programadores valorizam a compactação. Guido van Rossum verificou a base de código do Dropbox e encontrou algumas evidências de que os programadores preferem escrever menos linhas de código do que usar algumas expressões pequenas.

Caso em questão: Guido encontrou vários pontos ilustrativos quando um programador repete uma subexpressão (diminuindo a velocidade do programa), mas salva uma linha extra de código. Por exemplo, em vez de escrever:

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

Os programadores preferiram esta opção:

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

Aqui está outro exemplo, mostrando que os programadores às vezes estão dispostos a trabalhar mais para manter o "nível anterior" de indentação:

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

Esse código calcula o padrão2, mesmo que o padrão1 já corresponda (nesse caso, a segunda subcondição nunca será atendida). Portanto, a seguinte solução é mais eficaz, mas menos atraente:

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

Sintaxe e semântica


Na maioria dos casos em que o Python usa expressões arbitrárias, agora você pode usar expressões de atribuição. Eles têm o formato NAME: = expr, em que expr é qualquer expressão válida do Python, exceto a tupla não parênteses, e NAME é o identificador. O valor dessa expressão coincide com o original, mas um efeito adicional é a atribuição de um valor ao objeto de destino:

# 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]

Casos excepcionais


Existem vários locais em que expressões de atribuição não são permitidas para evitar ambiguidade ou confusão entre os usuários:

  • Expressões de atribuição que não estão entre parênteses são proibidas no nível "superior":

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

    Essa regra facilitará ao programador escolher entre um operador de atribuição e uma expressão de atribuição - não haverá situação sintática na qual as duas opções sejam equivalentes.
  • . :

    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- . , .


Uma expressão de atribuição não introduz um novo escopo. Na maioria dos casos, o escopo no qual a variável será criada não requer explicação: será atual. Se a variável usou as palavras-chave não locais ou globais antes, a expressão de atribuição levará isso em consideração. Somente lambda (sendo uma definição anônima de uma função) é considerado um escopo separado para esses fins.

Há um caso especial: uma expressão de atribuição que ocorre em geradores de listas, conjuntos, dicionários ou nas próprias "expressões de geradores" (doravante denominadas coletivamente como "geradores" (compreensões)) vincula a variável ao escopo que o gerador contém, observando o modificador globab ou não global, se existir.

A lógica desse caso especial é dupla. Em primeiro lugar, permite capturar convenientemente o "membro" nas expressões any () e all (), por exemplo:

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)

Em segundo lugar, fornece uma maneira compacta de atualizar uma variável de um gerador, por exemplo:

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

No entanto, o nome da variável da expressão de atribuição não pode corresponder ao nome já usado nos geradores pelo loop for para iterar. Os sobrenomes são locais para o gerador em que aparecem. Seria inconsistente se as expressões de atribuição também se referissem ao escopo dentro do gerador.

Por exemplo, [i: = i + 1 para i no intervalo (5)] não é válido: o loop for determina que i é local para o gerador, mas a parte “i: = i + 1” insiste que i é uma variável do externo escopo Pelo mesmo motivo, os seguintes exemplos não funcionarão:


[[(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)]                      # 

Embora seja tecnicamente possível atribuir semântica consistente para esses casos, é difícil determinar se a maneira como entendemos essa semântica funcionará em seu código real. É por isso que a implementação de referência garante que esses casos aumentem o SyntaxError em vez de serem executados com um comportamento indefinido, dependendo da implementação específica do hardware. Esta restrição se aplica mesmo que uma expressão de atribuição nunca seja executada:

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

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

Para o corpo do gerador (a parte antes da primeira palavra-chave “for”) e a expressão do filtro (a parte após o “if” e antes de qualquer “for” aninhado), essa restrição se aplica exclusivamente a nomes de variáveis ​​que são simultaneamente usados ​​como variáveis ​​iterativas. Como já dissemos, as expressões Lambda introduzem um novo escopo explícito da função e, portanto, podem ser usadas em expressões de geradores sem restrições adicionais. [Aproximadamente. novamente, exceto nesses casos: [i para i no intervalo (2, (lambda: (s: = 2) ()))]]

Devido a limitações de design na implementação de referência (o analisador de tabela de símbolos não pode reconhecer se os nomes da parte esquerda do gerador são usados ​​na parte restante em que a expressão iterável está localizada), portanto, as expressões de atribuição são completamente proibidas como parte da iterável (na parte após cada "entrada" e antes de qualquer palavra-chave subsequente "se" ou "para"). Ou seja, todos esses casos são inaceitáveis:

[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))()]        # 

Outra exceção ocorre quando uma expressão de atribuição é usada em geradores que estão no escopo de uma classe. Se, ao usar as regras acima, ocorrer a criação de uma classe medida novamente no escopo, essa expressão de atribuição será inválida e resultará em um SyntaxError:

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

(O motivo da última exceção é o escopo implícito da função criada pelo gerador - atualmente não há mecanismo de tempo de execução para as funções se referirem a uma variável localizada no escopo da classe e não queremos adicionar esse mecanismo. Se esse problema for resolvido, esse caso especial (possivelmente) será removido da especificação das expressões de atribuição.Por favor, observe que esse problema ocorrerá mesmo que você tenha criado uma variável anteriormente no escopo da classe e tente alterá-la com uma expressão de atribuição do gerador.)

Consulte o Apêndice B para obter exemplos de como expressões de atribuição encontradas em geradores são convertidas em código equivalente.

Prioridade relativa: =


O operador: = é agrupado mais forte que a vírgula em todas as posições sintáticas, sempre que possível, mas mais fraco que todos os outros operadores, incluindo ou, e, e não, e expressões condicionais (A se C mais B). Como segue na seção "Casos excepcionais" acima, as expressões de atribuição nunca funcionam no mesmo "nível" que a atribuição clássica =. Se uma ordem diferente de operações for necessária, use parênteses.

O operador: = pode ser usado diretamente ao chamar o argumento posicional de uma função. No entanto, isso não funcionará diretamente no argumento. Alguns exemplos que esclarecem o que é tecnicamente permitido e o que não é possível:

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')) #  

A maioria dos exemplos "válidos" acima não é recomendada para uso na prática, pois as pessoas que escanearam rapidamente o seu código-fonte podem não entender corretamente seu significado. Mas, em casos simples, isso é permitido:

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

Este PEP recomenda que você sempre coloque espaços em torno de: =, semelhante à recomendação do PEP 8 para = para tarefas clássicas. (A diferença da última recomendação é que ela proíbe espaços em torno de =, que é usado para passar argumentos principais para a função.)

Mude a ordem dos cálculos.


Para ter uma semântica bem definida, este acordo exige que o procedimento de avaliação seja claramente definido. Tecnicamente, esse não é um requisito novo. O Python já possui uma regra de que subexpressões geralmente são avaliadas da esquerda para a direita. No entanto, as expressões de atribuição tornam esses "efeitos colaterais" mais visíveis, e propomos uma alteração na ordem de cálculo atual:

  • Nos geradores de dicionário {X: Y para ...}, Y é atualmente avaliado antes de X. Sugerimos alterar isso para que X seja calculado antes de Y. (Em um ditado clássico como {X: Y} e também em dict ((X, Y) para ...) isso já foi implementado. Portanto, os geradores de dicionário devem cumprir esse mecanismo)


Diferenças entre expressões de atribuição e instruções de atribuição.


Mais importante, ": =" é uma expressão , o que significa que pode ser usada nos casos em que as instruções não são válidas, incluindo funções e geradores lambda. Por outro lado, as expressões de atribuição não suportam a funcionalidade estendida que pode ser usada nas instruções de atribuição:

  • A atribuição em cascata não é suportada diretamente

    x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
  • "Destinos" separados, exceto o nome da variável simples NAME, não são suportados:

    # No equivalent
    a[i] = x
    self.rest = []
  • A funcionalidade e as vírgulas prioritárias diferem:

    x = 1, 2  # Sets x to (1, 2)
    (x := 1, 2)  # Sets x to 1
  • Os valores de desempacotamento e empacotamento não têm equivalência "pura" ou não são totalmente suportados

    # 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
  • As anotações do tipo embutido não são suportadas:

    # Closest equivalent is "p: Optional[int]" as a separate declaration
    p: Optional[int] = None
  • Não há forma abreviada de operações:

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

Alterações nas especificações durante a implementação


As seguintes alterações foram feitas com base em nossa experiência e análises adicionais após a primeira gravação deste PEP e antes do lançamento do Python 3.8:

  • Para garantir consistência com outras exceções semelhantes e não introduzir um novo nome que pode não ser conveniente para os usuários finais, a subclasse originalmente proposta de TargetScopeError para SyntaxError foi removida e reduzida ao SyntaxError usual. [3]
  • Devido a limitações na análise da tabela de caracteres CPython, a implementação de referência da expressão de atribuição gera um SyntaxError para todos os usos nos iteradores. Anteriormente, essa exceção ocorria apenas se o nome da variável que estava sendo criada coincidisse com o já usado na expressão iterativa. Isso pode ser revisado se houver exemplos suficientemente convincentes, mas a complexidade adicional parece inadequada para casos de uso puramente "hipotéticos".

Exemplos


Exemplos de biblioteca padrão do Python


site.py


env_base é usado apenas em uma condição, portanto a atribuição pode ser colocada em if, como o "cabeçalho" de um bloco lógico.

  • Código atual:
    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base
  • Código aprimorado:
    if env_base := os.environ.get("PYTHONUSERBASE", None):
        return env_base

_pydecimal.py


Você pode evitar ifs aninhados, removendo um nível de recuo.

  • Código atual:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • Código aprimorado:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

copy.py


O código parece mais clássico e também evita o aninhamento múltiplo de instruções condicionais. (Consulte o Apêndice A para saber mais sobre a origem deste exemplo.)

  • Código atual:
    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)
  • Código aprimorado:

    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 é usado apenas para s + = tz. Mover para dentro se ajuda a mostrar sua área lógica de uso.

  • Código atual:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    tz = self._tzstr()
    if tz:
        s += tz
    return s
  • Código aprimorado:

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

sysconfig.py


Chamar fp.readline () como uma “condição” no loop while (assim como chamar o método .match ()) na condição if torna o código mais compacto sem complicar sua compreensão.

  • Código atual:

    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
  • Código aprimorado:

    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

Simplificar geradores de listas


Agora, o gerador de lista pode ser filtrado efetivamente "capturando" a condição:

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

Depois disso, a variável pode ser reutilizada em outra expressão:

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

Observe novamente que, nos dois casos, a variável y está no mesmo escopo que as variáveis ​​resultam e outras coisas.

Capturar valores em condições


As expressões de atribuição podem ser efetivamente usadas nas condições de uma instrução if ou 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)

Em particular, essa abordagem pode eliminar a necessidade de criar um loop infinito, atribuição e verificação de condições. Também permite desenhar um paralelo suave entre um ciclo que usa uma chamada de função como sua condição, bem como um ciclo que não apenas verifica a condição, mas também usa o valor real retornado pela função no futuro.

Garfo


Um exemplo do mundo de baixo nível do UNIX: [aprox. Fork () é uma chamada de sistema em sistemas operacionais semelhantes ao Unix que cria um novo subprocesso em relação ao pai.]

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

Alternativas rejeitadas


Em geral, sugestões semelhantes são bastante comuns na comunidade python. Abaixo estão algumas sintaxes alternativas para expressões de atribuição que são específicas demais para entender e foram rejeitadas em favor do acima.

Alterando o escopo para geradores


Em uma versão anterior deste PEP, foi proposto fazer alterações sutis nas regras de escopo dos geradores para torná-los mais adequados para uso no escopo das classes. No entanto, essas propostas levariam a incompatibilidade com versões anteriores e, portanto, foram rejeitadas. Portanto, esse PEP foi capaz de se concentrar totalmente apenas nas expressões de atribuição.

Ortografia alternativa


Em geral, as expressões de atribuição propostas têm a mesma semântica, mas são escritas de maneira diferente.

  1. EXPR como NAME:

    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)]

    É mais provável que essa sintaxe entre em conflito com os outros do que como (a menos que você conte o aumento de Exc das construções Exc), mas, caso contrário, será comparável a eles. Em vez de um paralelo com expr como target: (o que pode ser útil, mas também pode ser confuso), essa opção não tem paralelos com nada, mas é surpreendentemente melhor lembrada.


Casos especiais em declarações condicionais


Um dos casos de uso mais comuns para expressões de atribuição são as instruções if e while. Em vez de uma solução mais geral, o uso de as melhora a sintaxe dessas duas instruções adicionando um meio de capturar o valor a ser comparado:

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

Isso funciona bem, mas SOMENTE quando a condição desejada é baseada na "correção" do valor de retorno. Portanto, esse método é eficaz para casos específicos (verificação de expressões regulares, leitura de soquetes, retorno de uma string vazia quando a execução termina) e é completamente inútil em casos mais complexos (por exemplo, quando a condição é f (x) <0 e você deseja salve o valor de f (x)). Além disso, isso não faz sentido nos geradores de lista.

Vantagens : Sem ambigüidades sintáticas. Desvantagens : mesmo se você o usar apenas nas declarações if / while, ele só funcionará bem em alguns casos.

Casos especiais em geradores


Outro caso de uso comum para expressões de atribuição são geradores (lista / conjunto / dict e genexps). Como acima, foram feitas sugestões para soluções específicas.

  1. onde, let ou dado:

    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)]

    Esse método resulta em uma subexpressão entre o loop for e a expressão principal. Ele também apresenta uma palavra-chave de idioma adicional, que pode criar conflitos. Das três opções, onde é a mais limpa e a mais legível, mas ainda existem conflitos em potencial (por exemplo, SQLAlchemy e numpy têm seus métodos where, além de tkinter.dnd.Icon na biblioteca padrão).
  2. com 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

Independentemente do método escolhido, uma diferença semântica acentuada será introduzida entre geradores e suas versões implementadas por meio de um loop for. Seria impossível envolver o ciclo em um gerador sem processar o estágio de criação das variáveis. A única palavra-chave que pode ser reorientada para esta tarefa é a palavra with . Mas isso fornecerá semânticas diferentes em diferentes partes do código, o que significa que você precisa criar uma nova palavra-chave, mas isso envolve muitos custos.

Menor prioridade do operador


O operador: = tem duas prioridades lógicas. Ou deve ter a menor prioridade possível (a par do operador de atribuição). Ou deve ter precedência maior que os operadores de comparação. Colocar sua prioridade entre operadores de comparação e operações aritméticas (para ser preciso: um pouco menor que o OR bit a bit) permitirá que você fique sem parênteses na maioria dos casos, quando e se você o usar, pois é mais provável que você deseje manter o valor de algo antes como a comparação será realizada:

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

Assim que find () retorna -1, o loop termina. Se: = liga os operandos tão livremente quanto =, então o resultado de find () será “capturado” primeiro no operador de comparação e geralmente retornará True ou False, o que é menos útil.

Embora esse comportamento seja conveniente na prática em muitas situações, seria mais difícil de explicar. E, portanto, podemos dizer que "o operador: = se comporta da mesma forma que o operador de atribuição usual". Ou seja, a prioridade para: = foi escolhida o mais próximo possível do operador = (exceto que: = tem prioridade maior que a vírgula).

Você dá vírgulas à direita


Alguns críticos argumentam que as expressões de atribuição devem reconhecer tuplas sem a adição de colchetes, para que as duas entradas sejam equivalentes:

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

(Na versão atual do padrão, o último registro será equivalente à expressão ((ponto: = x), y).)

Mas é lógico que nesse cenário, ao usar a expressão de atribuição na chamada de função, ele também tenha uma prioridade mais baixa que a vírgula, portanto, obtivemos seria a seguinte equivalência confusa:

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

E temos a única saída menos confusa: torne o operador: = uma prioridade mais baixa que a vírgula.

Sempre exigindo suportes


Sempre foi proposto agrupar as expressões de atribuição. Isso nos salvaria muitas ambiguidades. De fato, muitas vezes serão necessários parênteses para extrair o valor desejado. Mas nos seguintes casos, a presença de colchetes nos pareceu claramente supérflua:

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

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

Objeções frequentes


Por que não transformar as declarações de atribuição em expressões?


C e linguagens similares definem o operador = como uma expressão, não uma instrução, como o Python. Isso permite a atribuição em muitas situações, incluindo locais onde as variáveis ​​são comparadas. As semelhanças sintáticas entre if (x == y) e if (x = y) contradizem sua semântica nitidamente diferente. Assim, este PEP apresenta o operador: = para esclarecer suas diferenças.

Por que se preocupar com expressões de atribuição se existem instruções de atribuição?


Essas duas formas têm flexibilidades diferentes. O operador: = pode ser usado dentro de uma expressão maior e no operador = pode ser usado pela "família de minioperadores" do tipo "+ =". Também = permite atribuir valores por atributos e índices.

Por que não usar o escopo local e evitar a poluição do espaço para nome?


As versões anteriores deste padrão incluíam um escopo local real (limitado a uma declaração) para expressões de atribuição, impedindo o vazamento e a poluição do espaço para nome. Apesar do fato de que em algumas situações isso deu uma certa vantagem, em muitas outras isso complica a tarefa, e os benefícios não são justificados pelas vantagens da abordagem existente. Isso é feito no interesse da simplicidade da linguagem. Você não precisa mais dessa variável? Existe uma solução: exclua a variável usando a palavra-chave del ou adicione um sublinhado menor ao seu nome.

(O autor gostaria de agradecer a Guido van Rossum e Christophe Groth por suas sugestões para avançar o padrão PEP nessa direção. [2])

Recomendações de estilo


Como as expressões de atribuição às vezes podem ser usadas em pé de igualdade com um operador de atribuição, surge a pergunta: o que ainda é preferido? .. De acordo com outras convenções de estilo (como o PEP 8), há duas recomendações:

  1. Se você pode usar as duas opções de atribuição, dê preferência aos operadores. Eles expressam mais claramente suas intenções.
  2. Se o uso de expressões de atribuição levar a ambiguidade na ordem de execução, reescreva o código usando o operador clássico.

obrigado


Os autores deste padrão gostariam de agradecer a Nick Coghlan e Steven D'Aprano por suas contribuições significativas a este PEP, bem como aos membros do Python Core Mentorship por sua ajuda na implementação disso.

Apêndice A: Conclusões de Tim Peters


Aqui está um pequeno ensaio que Tim Peters escreveu sobre esse tópico.

Não gosto do código "confuso" e também não gosto de colocar lógica conceitualmente não relacionada em uma linha. Então, por exemplo, em vez de:

i = j = count = nerrors = 0

Eu prefiro escrever:

i = j = 0
count = 0
nerrors = 0

Portanto, acho que vou encontrar vários lugares onde quero usar expressões de atribuição. Eu nem quero falar sobre o uso deles em expressões que já estão esticadas para metade da tela. Em outros casos, comportamentos como:

mylast = mylast[1]
yield mylast[0]

Significativamente melhor que isso:

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

Esses dois códigos têm conceitos completamente diferentes e misturá-los seria uma loucura. Em outros casos, a combinação de expressões lógicas complica a compreensão do código. Por exemplo, reescrevendo:

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

Em uma forma mais curta, perdemos a "lógica". Você precisa entender como esse código funciona. Meu cérebro não quer fazer isso:

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

Mas esses casos são raros. A tarefa de preservar o resultado é muito comum e “escasso é melhor que denso” não significa que “quase vazio é melhor que escasso” [aprox. uma referência ao Zen Python]. Por exemplo, tenho muitas funções que retornam None ou 0 para dizer "Não tenho nada útil, mas como isso acontece com frequência, não quero incomodá-lo com exceções". De fato, esse mecanismo também é usado em expressões regulares que retornam None quando não há correspondências. Portanto, neste exemplo, muito código:

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

Acho a seguinte opção mais compreensível e, claro, mais legível:

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

No começo, não dei muita importância a isso, mas uma construção tão curta apareceu com tanta frequência que logo começou a me irritar que eu não podia usá-la. Isso me surpreendeu! [Aproximadamente. aparentemente, isso foi escrito antes do lançamento oficial do Python 3.8.]

Há outros casos em que as expressões de atribuição realmente "disparam". Em vez de vasculhar meu código novamente, Kirill Balunov deu um bom exemplo da função copy () da biblioteca copy.py padrão:

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)

A crescente indentação é enganosa: afinal, a lógica é plana: o primeiro teste bem-sucedido “vence”:

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)

O simples uso de expressões de atribuição permite que a estrutura visual do código enfatize o "plano" da lógica. Mas o recuo sempre crescente o torna implícito.

Aqui está outro pequeno exemplo do meu código, que me deixou muito feliz porque me permitiu colocar a lógica relacionada internamente em uma linha e remover o irritante nível de indentação "artificial". É exatamente isso que eu quero da declaração if e facilita a leitura. O código a seguir:

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

Se tornou:

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

Portanto, na maioria das linhas em que a atribuição de variáveis ​​ocorre, eu não usaria expressões de atribuição. Mas esse design é tão frequente que ainda há muitos lugares onde eu aproveitaria essa oportunidade. Nos casos mais recentes, ganhei um pouco, como costumavam aparecer. Na subparte restante, isso levou a melhorias médias ou grandes. Assim, eu usaria expressões de atribuição com muito mais frequência do que um triplo se, mas com muito menos frequência do que com a atribuição aumentada [aprox. opções curtas: * =, / =, + =, etc.].

Exemplo numérico


Eu tenho outro exemplo que me impressionou mais cedo.

Se todas as variáveis ​​forem números inteiros positivos e a variável a for maior que a enésima raiz de x, esse algoritmo retornará o arredondamento "inferior" da enésima raiz de x (e aproximadamente dobra aproximadamente o número de bits exatos por iteração):

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

Não está claro por que, mas essa variante do algoritmo é menos óbvia do que um loop infinito com uma quebra de ramificação condicional (loop e meio). Também é difícil provar a correção dessa implementação sem depender de uma afirmação matemática (“média aritmética - desigualdade média geométrica”) e sem saber algumas coisas não triviais sobre como o arredondamento aninhado se comporta de maneira descendente. Mas aqui o problema já está em matemática, e não em programação.

E se você sabe tudo isso, a opção que usa expressões de atribuição é lida com muita facilidade, como uma frase simples: “Verifique a“ estimativa ”atual e se ela for muito grande, reduza-a” e a condição permite salvar imediatamente o valor intermediário da condição do loop. Na minha opinião, a forma clássica é mais difícil de entender:

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

Apêndice B: Um Intérprete de Código Bruto para Geradores


Este apêndice tenta esclarecer (embora não especifique) as regras pelas quais uma variável deve ser criada nas expressões geradoras. Para vários exemplos ilustrativos, mostramos o código-fonte onde o gerador é substituído por uma função equivalente em combinação com alguns “andaimes”.

Como [x para ...] é equivalente à lista (x para ...), os exemplos não perdem sua generalidade. E, como esses exemplos se destinam apenas a esclarecer as regras gerais, eles não afirmam ser realistas.

Nota: os geradores agora são implementados através da criação de funções de gerador aninhadas (semelhantes às fornecidas neste apêndice). Os exemplos mostram a nova parte, que adiciona a funcionalidade apropriada para trabalhar com o escopo das expressões de atribuição (como se a atribuição tivesse sido executada em um bloco contendo o gerador mais externo). Para simplificar a “inferência de tipo”, esses exemplos ilustrativos não levam em consideração que as expressões de atribuição são opcionais (mas levam em consideração o escopo da variável criada dentro do gerador).

Vamos primeiro lembrar qual código é gerado "sob o capô" para geradores sem expressões de atribuição:

  • Código fonte (o EXPR costuma usar a variável VAR):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • O código convertido (não vamos nos preocupar com conflitos de nome):

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


Vamos adicionar uma expressão de atribuição simples.

  • Fonte:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

    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)))

Agora vamos adicionar a instrução TARGET global à declaração da função f ().

  • Fonte:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

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

Ou vice-versa, vamos adicionar TARGET não-local à declaração da função f ().

  • Fonte:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

    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)))

Finalmente, vamos colocar dois geradores.

  • Fonte:

    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
    
  • Código convertido:

    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)

Apêndice C: Nenhuma alteração na semântica do escopo


Observe que no Python a semântica do escopo não mudou. O escopo das funções locais ainda é determinado no tempo de compilação e tem uma extensão de tempo indefinida no tempo de execução (fechamento). Exemplo:

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")

Então:

>>> 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

Referências


  1. Prova de implementação de conceito
  2. Discussão da semântica das expressões de atribuição (a VPN é rígida, mas carregada)
  3. Discussão do TargetScopeError no PEP 572 (carregado de forma semelhante à anterior)

direito autoral


Este documento foi disponibilizado ao público.

Fonte: github.com/python/peps/blob/master/pep-0572.rst

Minha parte


Para começar, vamos resumir:
  • Para que as pessoas não tentem remover a dualidade semântica, em muitos lugares "clássicos", onde se pode usar "=" e ": =", existem restrições; portanto, o operador :: = geralmente deve estar entre colchetes. Esses casos deverão ser revistos na seção que descreve o uso básico.
  • A prioridade das expressões de atribuição é um pouco maior que a de uma vírgula. Devido a isso, as tuplas não são formadas durante a atribuição. Também possibilita o uso do operador: = ao passar argumentos para uma função.
  • , , , . . lambda , «» .
  • : ,
  • , .
  • / .
  • , .

No final, quero dizer que gostei do novo operador. Ele permite que você escreva códigos mais simples em condições, listas de "filtros" e também (finalmente) remova a linha "mesma" e solitária antes de se. Se as pessoas usarem expressões de atribuição para a finalidade pretendida, será uma ferramenta muito conveniente que aumentará a legibilidade e a beleza do código (embora isso possa ser dito sobre qualquer linguagem funcional ...)

All Articles