PEP 572 (expressions d'affectation en python 3.8)

Bonjour, Habr. Cette fois, nous examinerons le PEP 572, qui parle d'expressions d'affectation. Si vous êtes toujours sceptique vis-à-vis de l'opérateur ": =" ou si vous ne comprenez pas parfaitement les règles de son utilisation, cet article est pour vous. Vous trouverez ici de nombreux exemples et réponses à la question: «Pourquoi en est-il ainsi?» Cet article s'est avéré être aussi complet que possible, et si vous avez peu de temps, alors regardez la section que j'ai écrite. À ses débuts, les principales «thèses» sont rassemblées pour un travail confortable avec des expressions d'affectation. Pardonnez-moi à l'avance si vous trouvez des erreurs (écrivez-moi, je vais les corriger). Commençons:

PEP 572 - Expressions d'affectation

Dynamisme572
Titre:Expressions d'affectation
Auteurs:Chris Angelico <rosuav at gmail.com>, Tim Peters <tim.peters at gmail.com>, Guido van Rossum <guido at python.org>
Discussion:doc-sig sur python.org
Statut:Accepté
Un type:la norme
Créé:28-févr.-2018
Version Python:3.8
Histoire de poste:28-févr.-2018, 02-mars-2018, 23-mars-2018, 04-avr-2018, 17-avr-2018, 25-avr-2018, 09-juil-2018, 05-août-2019
Autorisation d'adopter la norme:mail.python.org/pipermail/python-dev/2018-July/154601.html (avec VPN depuis longtemps, mais il se charge)
Contenu


annotation


Cette convention parlera de la possibilité d'affectation à l'intérieur des expressions, en utilisant la nouvelle notation NAME: = expr.

Dans le cadre des innovations, la procédure de calcul des générateurs de dictionnaire (compréhension de dictionnaire) a été mise à jour. Cela garantit que l'expression de clé est évaluée avant l'expression de valeur (cela vous permet de lier la clé à une variable, puis de réutiliser la variable créée dans le calcul de la valeur correspondant à la clé).

Au cours d'une discussion sur ce PEP, cet opérateur est devenu officieusement connu sous le nom d'opérateur de morse. Le nom officiel de la construction est «Expression d'affectation» (selon le titre PEP: Expressions d'affectation), mais il peut être appelé «Expressions nommées». Par exemple, l'implémentation de référence dans CPython utilise ce même nom.

Justification


Le nommage est une partie importante de la programmation qui vous permet d'utiliser un nom «descriptif» au lieu d'une expression plus longue, et facilite également la réutilisation des valeurs. Actuellement, cela ne peut se faire que sous forme d'instructions, ce qui rend cette opération indisponible lors de la génération de listes (compréhension de liste), ainsi que dans d'autres expressions.

De plus, le fait de nommer des parties d'une grande expression peut aider au débogage interactif en fournissant des outils pour afficher des invites et des résultats intermédiaires. Sans la possibilité de capturer les résultats d'expressions imbriquées, vous devrez modifier le code source, mais en utilisant les expressions d'affectation, il vous suffit d'insérer quelques "marqueurs" du formulaire "nom: = expression". Cela élimine le refactoring inutile et réduit donc la probabilité de modifications de code involontaires pendant le débogage (une cause courante des Heisenbugs est des erreurs qui modifient les propriétés du code pendant le débogage et peuvent apparaître de manière inattendue en production]), et ce code sera plus compréhensible pour un autre au programmeur.

L'importance du vrai code


Au cours de l'élaboration de ce PEP, de nombreuses personnes (partisans et critiques) se sont trop concentrées sur les exemples de jouets d'une part et les exemples trop complexes de l'autre.

Le danger des exemples de jouets est double: ils sont souvent trop abstraits pour faire dire à quelqu'un "oh, c'est irrésistible", et ils sont aussi facilement rejetés avec les mots "je n'écrirais jamais ça". Le danger d'exemples trop complexes est qu'ils fournissent un environnement pratique pour les critiques suggérant que cette fonctionnalité soit supprimée («C'est trop déroutant», disent de telles personnes).

Cependant, ces exemples sont utiles: ils aident à clarifier la sémantique voulue. Par conséquent, nous en donnerons certains ci-dessous. Cependant, pour être convaincant , les exemples doivent être basés surun vrai code qui a été écrit sans penser à ce PEP. Autrement dit, le code qui fait partie d'une application vraiment utile (aucune différence: qu'il soit grand ou petit). Tim Peters nous a beaucoup aidés en consultant ses répertoires personnels et en choisissant des exemples de code qu'il a écrit, qui (à son avis) seraient plus compréhensibles s'ils étaient réécrits (sans fanatisme) à l'aide d'expressions d'affectation. Sa conclusion est la suivante: les changements actuels apporteraient une amélioration modeste mais évidente dans quelques morceaux de son code.

Un autre exemple de code réel est l'observation indirecte de la façon dont les programmeurs apprécient la compacité. Guido van Rossum a vérifié la base de code Dropbox et a trouvé des preuves que les programmeurs préfèrent écrire moins de lignes de code que d'utiliser quelques petites expressions.

Exemple: Guido a trouvé plusieurs points illustratifs lorsqu'un programmeur répète une sous-expression (ralentissant ainsi le programme), mais enregistre une ligne de code supplémentaire. Par exemple, au lieu d'écrire:

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

Les programmeurs ont préféré cette option:

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

Voici un autre exemple montrant que les programmeurs sont parfois prêts à faire plus de travail pour maintenir le «niveau précédent» d'indentation:

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

Ce code calcule pattern2, même si pattern1 correspond déjà (dans ce cas, la deuxième sous-condition ne sera jamais remplie). Par conséquent, la solution suivante est plus efficace, mais moins attrayante:

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

Syntaxe et sémantique


Dans la plupart des cas où Python utilise des expressions arbitraires, vous pouvez désormais utiliser des expressions d'affectation. Ils ont la forme NAME: = expr, où expr est n'importe quelle expression Python valide, à l'exception du tuple non parenthésé, et NAME est l'identifiant. La valeur d'une telle expression coïncide avec l'original, mais un effet supplémentaire est l'attribution d'une valeur à l'objet cible:

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

Cas exceptionnels


Il existe plusieurs endroits où les expressions d'affectation ne sont pas autorisées afin d'éviter toute ambiguïté ou confusion parmi les utilisateurs:

  • Les expressions d'affectation non incluses entre parenthèses sont interdites au niveau «supérieur»:

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

    Cette règle permettra au programmeur de choisir plus facilement entre un opérateur d'affectation et une expression d'affectation - il n'y aura pas de situation syntaxique dans laquelle les deux options sont équivalentes.
  • . :

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


Une expression d'affectation n'introduit pas de nouvelle étendue. Dans la plupart des cas, l'étendue dans laquelle la variable sera créée ne nécessite aucune explication: elle sera à jour. Si la variable a utilisé les mots clés non locaux ou globaux auparavant, l'expression d'affectation en tiendra compte. Seul lambda (étant une définition anonyme d'une fonction) est considéré comme une portée distincte à ces fins.

Il existe un cas particulier: une expression d'affectation qui se produit dans les générateurs de listes, d'ensembles, de dictionnaires ou dans les «expressions des générateurs» eux-mêmes (ci-après collectivement appelés «générateurs» (compréhensions)) lie la variable à la portée que contient le générateur, en observant le modificateur globab ou non global, s'il en existe un.

La justification de ce cas spécial est double. Premièrement, cela nous permet de capturer facilement le «membre» dans les expressions any () et all (), par exemple:

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)

Deuxièmement, il fournit un moyen compact de mettre à jour une variable à partir d'un générateur, par exemple:

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

Cependant, le nom de la variable de l'expression d'affectation ne peut pas correspondre au nom déjà utilisé dans les générateurs par la boucle for pour itérer. Les noms de famille sont locaux au générateur dans lequel ils apparaissent. Il serait incohérent si les expressions d'affectation faisaient également référence à la portée dans le générateur.

Par exemple, [i: = i + 1 pour i dans la plage (5)] n'est pas valide: la boucle for détermine que i est local au générateur, mais la partie «i: = i + 1» insiste sur le fait que i est une variable de l'extérieur portée Pour la même raison, les exemples suivants ne fonctionneront pas:


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

Bien qu'il soit techniquement possible d'attribuer une sémantique cohérente à de tels cas, il est difficile de déterminer si la façon dont nous comprenons cette sémantique fonctionnera dans votre code réel. C'est pourquoi l'implémentation de référence garantit que de tels cas déclenchent SyntaxError plutôt que d'être exécutés avec un comportement non défini, selon l'implémentation matérielle particulière. Cette restriction s'applique même si une expression d'affectation n'est jamais exécutée:

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

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

Pour le corps du générateur (la partie avant le premier mot-clé «pour») et l'expression de filtre (la partie après le «si» et avant tout «pour» imbriqué), cette restriction s'applique exclusivement aux noms de variable qui sont simultanément utilisés comme variables itératives. Comme nous l'avons déjà dit, les expressions Lambda introduisent une nouvelle portée explicite de la fonction et peuvent donc être utilisées dans les expressions des générateurs sans restrictions supplémentaires. [environ. à nouveau, sauf dans de tels cas: [i pour i dans la plage (2, (lambda: (s: = 2) ()))]]]

En raison des limites de conception dans l'implémentation de référence (l'analyseur de table de symboles ne peut pas reconnaître si les noms de la partie gauche du générateur sont utilisés dans la partie restante où se trouve l'expression itérable), par conséquent, les expressions d'affectation sont complètement interdites dans le cadre de l'itérable (dans la partie après chaque «in» et avant tout mot clé "si" ou "pour"). Autrement dit, tous ces cas sont inacceptables:

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

Une autre exception se produit lorsqu'une expression d'affectation est utilisée dans des générateurs qui se trouvent dans la portée d'une classe. Si, lors de l'utilisation des règles ci-dessus, la création d'une classe remesurée dans la portée devait se produire, alors une telle expression d'affectation n'est pas valide et entraînera une SyntaxError:

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

(La raison de la dernière exception est la portée implicite de la fonction créée par le générateur - il n'y a actuellement aucun mécanisme d'exécution pour que les fonctions se réfèrent à une variable située dans la portée de la classe, et nous ne voulons pas ajouter un tel mécanisme. Si ce problème est résolu, ce cas spécial (éventuellement) sera supprimé de la spécification des expressions d'affectation. Notez que ce problème se produira même si vous avez créé une variable plus tôt dans la portée de la classe et essayez de la changer avec une expression d'affectation du générateur.)

Voir l'annexe B pour des exemples de la façon dont les expressions d'affectation trouvées dans les générateurs sont converties en code équivalent.

Priorité relative: =


L'opérateur: = est groupé plus fort que la virgule dans toutes les positions syntaxiques lorsque cela est possible, mais plus faible que tous les autres opérateurs, y compris ou, et, non, et les expressions conditionnelles (A si C sinon B). Comme indiqué dans la section «Cas exceptionnels» ci-dessus, les expressions d'affectation ne fonctionnent jamais au même «niveau» que l'affectation classique =. Si un ordre d'opérations différent est requis, utilisez des parenthèses.

L'opérateur: = peut être utilisé directement lors de l'appel de l'argument positionnel d'une fonction. Cependant, cela ne fonctionnera pas directement dans l'argument. Quelques exemples clarifiant ce qui est techniquement autorisé et ce qui n'est pas possible:

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

La plupart des exemples «valides» ci-dessus ne sont pas recommandés dans la pratique, car les personnes qui analysent rapidement votre code source peuvent ne pas comprendre correctement sa signification. Mais dans des cas simples, cela est autorisé:

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

Ce PEP vous recommande de toujours mettre des espaces autour: =, similaire à la recommandation du PEP 8 pour = pour une affectation classique. (La différence de la dernière recommandation est qu'elle interdit les espaces autour de =, qui est utilisé pour passer des arguments clés à la fonction.)

Modifiez l'ordre des calculs.


Afin d'avoir une sémantique bien définie, cet accord nécessite que la procédure d'évaluation soit clairement définie. Techniquement, ce n'est pas une nouvelle exigence. Python a déjà une règle selon laquelle les sous-expressions sont généralement évaluées de gauche à droite. Cependant, les expressions d'affectation rendent ces «effets secondaires» plus visibles, et nous proposons un changement dans l'ordre de calcul actuel:

  • Dans les générateurs de dictionnaire {X: Y pour ...}, Y est actuellement évalué avant X. Nous suggérons de le changer pour que X soit calculé avant Y. (Dans un dict classique tel que {X: Y}, ainsi que dans dict ((X, Y) pour ...) ceci a déjà été implémenté. Par conséquent, les générateurs de dictionnaire doivent respecter ce mécanisme)


Différences entre les expressions d'affectation et les instructions d'affectation.


Plus important encore, ": =" est une expression , ce qui signifie qu'elle peut être utilisée dans les cas où les instructions ne sont pas valides, y compris les fonctions lambda et les générateurs. Inversement, les expressions d' affectation ne prennent pas en charge la fonctionnalité étendue qui peut être utilisée dans les instructions d' affectation:

  • L'affectation en cascade n'est pas prise en charge directement

    x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
  • Les "cibles" séparées, à l'exception du nom de variable simple NAME, ne sont pas prises en charge:

    # No equivalent
    a[i] = x
    self.rest = []
  • La fonctionnalité et la priorité des virgules «autour» diffèrent:

    x = 1, 2  # Sets x to (1, 2)
    (x := 1, 2)  # Sets x to 1
  • Les valeurs de déballage et d'emballage n'ont pas d'équivalence «pure» ou ne sont pas du tout prises en charge

    # 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
  • Les annotations de type en ligne ne sont pas prises en charge:

    # Closest equivalent is "p: Optional[int]" as a separate declaration
    p: Optional[int] = None
  • Il n'y a pas d'opération abrégée:

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

Modifications des spécifications lors de la mise en œuvre


Les modifications suivantes ont été apportées sur la base de notre expérience et d'une analyse supplémentaire après la première rédaction de ce PEP et avant la sortie de Python 3.8:

  • Pour garantir la cohérence avec d'autres exceptions similaires et pour ne pas introduire un nouveau nom qui pourrait ne pas convenir aux utilisateurs finaux, la sous-classe initialement proposée de TargetScopeError pour SyntaxError a été supprimée et réduite à la SyntaxError habituelle. [3]
  • En raison des limitations de l'analyse de la table de caractères CPython, l'implémentation de référence de l'expression d'affectation déclenche une SyntaxError pour toutes les utilisations au sein des itérateurs. Auparavant, cette exception ne se produisait que si le nom de la variable en cours de création coïncidait avec celui déjà utilisé dans l'expression itérative. Ceci peut être révisé s'il existe des exemples suffisamment convaincants, mais la complexité supplémentaire semble inappropriée pour des cas d'utilisation purement «hypothétiques».

Exemples


Exemples de bibliothèques standard Python


site.py


env_base est utilisé uniquement dans une condition, de sorte que l'affectation peut être placée dans if, comme "en-tête" d'un bloc logique.

  • Code actuel:
    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base
  • Code amélioré:
    if env_base := os.environ.get("PYTHONUSERBASE", None):
        return env_base

_pydecimal.py


Vous pouvez éviter les if imbriqués, supprimant ainsi un niveau d'indentation.

  • Code actuel:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • Code amélioré:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

copy.py


Le code semble plus classique et évite également l'imbrication multiple des instructions conditionnelles. (Voir l'annexe A pour en savoir plus sur l'origine de cet exemple.)

  • Code actuel:
    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)
  • Code amélioré:

    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 est utilisé uniquement pour s + = tz. Le déplacer vers l'intérieur permet de montrer sa zone logique d'utilisation.

  • Code actuel:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    tz = self._tzstr()
    if tz:
        s += tz
    return s
  • Code amélioré:

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

sysconfig.py


L'appel de fp.readline () en tant que «condition» dans la boucle while (ainsi que l'appel de la méthode .match ()) dans la condition if rend le code plus compact sans compliquer sa compréhension.

  • Code actuel:

    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
  • Code amélioré:

    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

Simplifier les générateurs de listes


Maintenant, le générateur de liste peut être efficacement filtré en "capturant" la condition:

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

Après cela, la variable peut être réutilisée dans une autre expression:

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

Veuillez noter à nouveau que dans les deux cas, la variable y a la même portée que les variables result et stuff.

Capturer des valeurs dans des conditions


Les expressions d'affectation peuvent être utilisées efficacement dans les conditions d'une instruction 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)

En particulier, cette approche peut éliminer la nécessité de créer une boucle infinie, l'affectation et la vérification des conditions. Il vous permet également de tracer un parallèle harmonieux entre un cycle qui utilise un appel de fonction comme condition, ainsi qu'un cycle qui vérifie non seulement la condition, mais utilise également la valeur réelle renvoyée par la fonction à l'avenir.

Fourchette


Un exemple du monde bas niveau d'UNIX: [env. Fork () est un appel système sur les systèmes d'exploitation de type Unix qui crée un nouveau sous-processus par rapport au parent.]

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

Alternatives rejetées


En général, des suggestions similaires sont assez courantes dans la communauté python. Vous trouverez ci-dessous un certain nombre de syntaxes alternatives pour les expressions d'affectation qui sont trop spécifiques pour être comprises et ont été rejetées en faveur de ce qui précède.

Changer la portée des générateurs


Dans une version précédente de ce PEP, des modifications subtiles des règles de portée pour les générateurs ont été proposées pour les rendre plus utilisables dans la portée de classe. Cependant, ces propositions entraîneraient une incompatibilité en amont et ont donc été rejetées. Par conséquent, ce PEP a pu se concentrer entièrement uniquement sur les expressions d'affectation.

Orthographes alternatives


En général, les expressions d'affectation proposées ont la même sémantique, mais sont écrites différemment.

  1. EXPR comme NOM:

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

    Cette syntaxe est moins en conflit avec les autres qu'en tant que (sauf si vous comptez l'augmentation des constructions Exc), mais sinon elle est comparable à elles. Au lieu d'un parallèle avec avec expr comme cible: (ce qui peut être utile, mais cela peut aussi prêter à confusion), cette option n'a aucun parallèle avec quoi que ce soit, mais elle est étonnamment mieux mémorisée.


Cas particuliers dans les déclarations conditionnelles


L'un des cas d'utilisation les plus courants pour les expressions d'affectation est les instructions if et while. Au lieu d'une solution plus générale, l'utilisation de as améliore la syntaxe de ces deux instructions en ajoutant un moyen de capturer la valeur à comparer:

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

Cela fonctionne bien, mais UNIQUEMENT lorsque la condition souhaitée est basée sur la «justesse» de la valeur de retour. Ainsi, cette méthode est efficace pour des cas spécifiques (recherche d'expressions régulières, lecture de sockets, retour d'une chaîne vide à la fin de l'exécution), et est complètement inutile dans des cas plus complexes (par exemple, lorsque la condition est f (x) <0, et que vous voulez enregistrer la valeur de f (x)). En outre, cela n'a aucun sens dans les générateurs de listes.

Avantages : Aucune ambiguïté syntaxique. Inconvénients : même si vous ne l'utilisez que dans les instructions if / while, cela ne fonctionne bien que dans certains cas.

Cas particuliers dans les générateurs


Un autre cas d'utilisation courant pour les expressions d'affectation est les générateurs (list / set / dict et genexps). Comme ci-dessus, des suggestions ont été faites pour des solutions spécifiques.

  1. où, loué ou donné:

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

    Cette méthode entraîne une sous-expression entre la boucle for et l'expression principale. Il introduit également un mot-clé de langue supplémentaire, qui peut créer des conflits. Des trois options, est la plus propre et la plus lisible, mais des conflits potentiels existent toujours (par exemple, SQLAlchemy et numpy ont leurs méthodes where, ainsi que tkinter.dnd.Icon dans la bibliothèque standard).
  2. avec 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

Quelle que soit la méthode choisie, une nette différence sémantique sera introduite entre les générateurs et leurs versions déployées via une boucle for. Il deviendrait impossible d'envelopper un cycle dans un générateur sans traiter l'étape de création des variables. Le seul mot-clé qui pourrait être réorienté pour cette tâche est le mot avec . Mais cela lui donnera une sémantique différente dans différentes parties du code, ce qui signifie que vous devez créer un nouveau mot-clé, mais cela implique beaucoup de coûts.

Priorité inférieure de l'opérateur


L'opérateur: = a deux priorités logiques. Ou il doit avoir une priorité aussi faible que possible (au même niveau que l'opérateur d'affectation). Ou il doit avoir une priorité supérieure aux opérateurs de comparaison. Placer sa priorité entre les opérateurs de comparaison et les opérations arithmétiques (pour être précis: légèrement inférieur à OR au niveau du bit) vous permettra de vous passer de crochets dans la plupart des cas quand et si vous l'utilisez, car il est plus probable que vous souhaitiez conserver la valeur de quelque chose avant comment la comparaison sera effectuée dessus:

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

Dès que find () renvoie -1, la boucle se termine. Si: = lie les opérandes aussi librement que =, alors le résultat de find () sera d'abord «capturé» dans l'opérateur de comparaison et retournera généralement True ou False, ce qui est moins utile.

Bien que ce comportement soit pratique en pratique dans de nombreuses situations, il serait plus difficile à expliquer. Et nous pouvons donc dire que "l'opérateur: = se comporte de la même manière que l'opérateur d'affectation habituel." Autrement dit, la priorité pour: = a été sélectionnée le plus près possible de l'opérateur = (sauf que: = a une priorité supérieure à la virgule).

Vous donnez des virgules à droite


Certains critiques soutiennent que les expressions d'affectation devraient reconnaître les tuples sans ajouter de crochets afin que les deux entrées soient équivalentes:

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

(Dans la version actuelle de la norme, le dernier enregistrement sera équivalent à l'expression ((point: = x), y).)

Mais il est logique que dans ce scénario, lors de l'utilisation de l'expression d'affectation dans l'appel de fonction, il aurait également une priorité inférieure à la virgule, nous avons donc obtenu serait l'équivalence déroutante suivante:

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

Et nous obtenons la seule issue moins confuse: faire de l'opérateur: = une priorité inférieure à la virgule.

Toujours besoin de supports


Il a toujours été proposé de mettre entre crochets les expressions d'affectation. Cela nous éviterait bien des ambiguïtés. En effet, des parenthèses seront souvent nécessaires pour extraire la valeur souhaitée. Mais dans les cas suivants, la présence de parenthèses nous a clairement paru superflue:

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

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

Objections fréquentes


Pourquoi ne pas simplement transformer les instructions d'affectation en expressions?


C et les langages similaires définissent l'opérateur = comme une expression, et non comme une instruction, comme le fait Python. Cela permet une affectation dans de nombreuses situations, y compris aux endroits où les variables sont comparées. Les similitudes syntaxiques entre if (x == y) et if (x = y) contredisent leur sémantique très différente. Ainsi, ce PEP introduit l'opérateur: = pour clarifier leurs différences.

Pourquoi s'embêter avec des expressions d' affectation s'il existe des instructions d' affectation?


Ces deux formes ont des flexibilités différentes. L'opérateur: = peut être utilisé dans une expression plus grande, et dans l'opérateur = il peut être utilisé par la "famille des mini-opérateurs" de type "+ =". Aussi = vous permet d'affecter des valeurs par attributs et index.

Pourquoi ne pas utiliser la portée locale et éviter la pollution de l'espace de noms?


Les versions précédentes de cette norme comprenaient une véritable portée locale (limitée à une instruction) pour les expressions d'affectation, empêchant la fuite de nom et la pollution de l'espace de nom. Malgré le fait que dans certaines situations cela a donné un certain avantage, dans beaucoup d'autres cela complique la tâche, et les avantages ne sont pas justifiés par les avantages de l'approche existante. Cela se fait dans l'intérêt de la simplicité de la langue. Vous n'avez plus besoin de cette variable? Il existe une solution: supprimez la variable à l'aide du mot clé del ou ajoutez un trait de soulignement inférieur à son nom.

(L'auteur tient à remercier Guido van Rossum et Christophe Groth pour leurs suggestions visant à faire avancer la norme PEP dans cette direction. [2])

Recommandations de style


Étant donné que les expressions d'affectation peuvent parfois être utilisées sur un pied d'égalité avec un opérateur d'affectation, la question se pose, qu'est-ce qui est toujours préféré? .. Conformément à d'autres conventions de style (comme PEP 8), il y a deux recommandations:

  1. Si vous pouvez utiliser les deux options d'affectation, privilégiez les opérateurs. Ils expriment le plus clairement vos intentions.
  2. Si l'utilisation d'expressions d'affectation entraîne une ambiguïté dans l'ordre d'exécution, réécrivez le code à l'aide de l'opérateur classique.

Merci


Les auteurs de cette norme tiennent à remercier Nick Coghlan et Steven D'Aprano pour leurs contributions significatives à ce PEP, ainsi que les membres du Python Core Mentorship pour leur aide dans la mise en œuvre de celui-ci.

Annexe A: Conclusions de Tim Peters


Voici un court essai que Tim Peters a écrit sur ce sujet.

Je n'aime pas le code "confus", et je n'aime pas non plus mettre la logique sans rapport conceptuel sur une seule ligne. Ainsi, par exemple, au lieu de:

i = j = count = nerrors = 0

Je préfère écrire:

i = j = 0
count = 0
nerrors = 0

Par conséquent, je pense que je vais trouver plusieurs endroits où je veux utiliser des expressions d'affectation. Je ne veux même pas parler de leur utilisation dans des expressions qui sont déjà étirées sur la moitié de l'écran. Dans d'autres cas, des comportements tels que:

mylast = mylast[1]
yield mylast[0]

Beaucoup mieux que cela:

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

Ces deux codes ont des concepts complètement différents et les mélanger serait fou. Dans d'autres cas, la combinaison d'expressions logiques rend le code plus difficile à comprendre. Par exemple, réécriture:

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

Dans une forme plus courte, nous avons perdu la «logique». Vous devez comprendre comment ce code fonctionne. Mon cerveau ne veut pas faire ça:

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

Mais de tels cas sont rares. La tâche de préserver le résultat est très courante et «clairsemé vaut mieux que dense» ne signifie pas que «presque vide vaut mieux que clairsemé» [env. une référence à Zen Python]. Par exemple, j'ai de nombreuses fonctions qui renvoient None ou 0 pour dire "Je n'ai rien d'utile, mais comme cela se produit souvent, je ne veux pas vous déranger avec des exceptions." En fait, ce mécanisme est également utilisé dans les expressions régulières qui renvoient None lorsqu'il n'y a pas de correspondance. Par conséquent, dans cet exemple, beaucoup de code:

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

Je trouve l'option suivante plus compréhensible et bien sûr plus lisible:

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

Au début, je n'y attachais pas beaucoup d'importance, mais une construction si courte est apparue si souvent qu'elle a rapidement commencé à m'ennuyer que je ne pouvais pas l'utiliser. Ça m'a étonné! [environ. apparemment, cela a été écrit avant la sortie officielle de Python 3.8.]

Il y a d'autres cas où les expressions d'affectation "tirent" vraiment. Au lieu de fouiller à nouveau dans mon code, Kirill Balunov a donné un bel exemple de la fonction copy () de la bibliothèque copy.py standard:

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)

L'indentation toujours croissante est trompeuse: après tout, la logique est plate: le premier test réussi «gagne»:

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)

La simple utilisation des expressions d'affectation permet à la structure visuelle du code de mettre en valeur le «plan» de la logique. Mais l'indentation toujours croissante la rend implicite.

Voici un autre petit exemple de mon code, qui m'a fait très plaisir car il m'a permis de mettre une logique liée en interne sur une seule ligne et de supprimer le niveau d'indentation "artificiel" ennuyeux. C'est exactement ce que je veux de l'instruction if et cela facilite la lecture. Le code suivant:

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

Transformé en:

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

Ainsi, dans la plupart des lignes où l'affectation de variable se produit, je n'utiliserais pas d'expressions d'affectation. Mais cette conception est si fréquente qu'il y a encore de nombreux endroits où je saisirais cette opportunité. Dans les cas les plus récents, j'ai gagné un peu, comme ils apparaissaient souvent. Dans la sous-partie restante, cela a conduit à des améliorations moyennes ou importantes. Ainsi, j'utiliserais des expressions d'affectation beaucoup plus souvent qu'un triple si, mais beaucoup moins souvent qu'une affectation augmentée [env. options courtes: * =, / =, + =, etc.].

Exemple numérique


J'ai un autre exemple qui m'a frappé plus tôt.

Si toutes les variables sont des entiers positifs et que la variable a est supérieure à la nième racine de x, alors cet algorithme renvoie l'arrondi «inférieur» de la nième racine de x (et double approximativement le nombre de bits exacts par itération):

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

On ne sait pas pourquoi, mais une telle variante de l'algorithme est moins évidente qu'une boucle infinie avec une rupture de branche conditionnelle (boucle et demie). Il est également difficile de prouver l'exactitude de cette implémentation sans s'appuyer sur un énoncé mathématique («moyenne arithmétique - inégalité moyenne géométrique») et ne pas savoir certaines choses non triviales sur la façon dont les fonctions d'arrondi imbriquées se comportent vers le bas. Mais ici, le problème est déjà en mathématiques et non en programmation.

Et si vous savez tout cela, alors l'option utilisant des expressions d'affectation est lue très facilement, comme une simple phrase: "Vérifiez la" supposition "actuelle et si elle est trop grande, réduisez-la" et la condition vous permet d'enregistrer immédiatement la valeur intermédiaire de la condition de boucle. À mon avis, la forme classique est plus difficile à comprendre:

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

Annexe B: Un interpréteur de code approximatif pour les générateurs


Cette annexe tente de clarifier (sans préciser) les règles selon lesquelles une variable doit être créée dans les expressions de générateur. Pour un certain nombre d'exemples illustratifs, nous montrons le code source où le générateur est remplacé par une fonction équivalente en combinaison avec certains «échafaudages».

Puisque [x pour ...] est équivalent à list (x pour ...), les exemples ne perdent pas leur généralité. Et comme ces exemples ne visent qu'à clarifier les règles générales, ils ne prétendent pas être réalistes.

Remarque: les générateurs sont désormais implémentés par la création de fonctions génératrices imbriquées (similaires à celles données dans cette annexe). Les exemples montrent la nouvelle partie, qui ajoute la fonctionnalité appropriée pour travailler avec la portée des expressions d'affectation (telle que la portée comme si l'affectation était effectuée dans un bloc contenant le générateur le plus externe). Pour simplifier l '«inférence de type», ces exemples illustratifs ne prennent pas en compte le fait que les expressions d'affectation sont facultatives (mais elles prennent en compte la portée de la variable créée à l'intérieur du générateur).

Rappelons d'abord quel code est généré «sous le capot» pour les générateurs sans expressions d'affectation:

  • Code source (EXPR utilise le plus souvent la variable VAR):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • Le code converti (ne nous inquiétons pas des conflits de noms):

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


Ajoutons une expression d'affectation simple.

  • La source:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Code converti:

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

Ajoutons maintenant l'instruction TARGET globale à la déclaration de la fonction f ().

  • La source:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Code converti:

    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, ajoutons TARGET non local à la déclaration de la fonction f ().

  • La source:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Code converti:

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

Enfin, mettons deux générateurs.

  • La source:

    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
    
  • Code converti:

    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)

Annexe C: Aucun changement dans la sémantique de la portée


Notez qu'en Python, la sémantique des portées n'a pas changé. L'étendue des fonctions locales est toujours déterminée au moment de la compilation et a une durée indéfinie au moment de l'exécution (fermeture). Exemple:

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

Alors:

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

Références


  1. Mise en œuvre de la preuve de concept
  2. Discussion de la sémantique des expressions d'affectation (VPN est serré mais chargé)
  3. Discussion de TargetScopeError dans PEP 572 (chargé de manière similaire à la précédente)

droits d'auteur


Ce document a été rendu public.

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

Ma partie


Pour commencer, résumons:
  • Pour que les gens n'essaient pas de supprimer la dualité sémantique, dans de nombreux endroits «classiques» où vous pouvez utiliser à la fois «=» et «: =», il y a des restrictions, donc l'opérateur :: = doit souvent être placé entre crochets. Ces cas devront être examinés dans la section décrivant l'utilisation de base.
  • La priorité des expressions d'affectation est légèrement supérieure à celle d'une virgule. Pour cette raison, les tuples ne sont pas formés pendant l'affectation. Il permet également d'utiliser l'opérateur: = lors du passage d'arguments à une fonction.
  • , , , . . lambda , «» .
  • : ,
  • , .
  • / .
  • , .

Au final, je tiens à dire que j'ai aimé le nouvel opérateur. Il vous permet d'écrire du code plus plat dans des conditions, de «filtrer» les listes, et aussi (enfin) de supprimer la «même» ligne solitaire avant if. Si les gens utilisent des expressions d'affectation pour leur objectif, ce sera un outil très pratique qui augmentera la lisibilité et la beauté du code (bien que cela puisse être dit à propos de n'importe quel langage fonctionnel ...)

All Articles