Expressions d'affectation Python 3.8 – Python réel

By | août 9, 2021

trouver un expert Python

Chaque nouvelle version de Python ajoute de nouvelles fonctionnalités au langage. Pour Python 3.8, le plus gros changement est l'ajout de expressions d'affectation. Plus précisément, le := L'opérateur vous donne une nouvelle syntaxe pour affecter des variables au milieu des expressions. Cet opérateur est familièrement connu sous le nom de opérateur morse.

Ce didacticiel est une introduction approfondie à l'opérateur morse. Vous apprendrez certaines des motivations de la mise à jour de la syntaxe et explorerez quelques exemples où les expressions d'affectation peuvent être utiles.

Notez que tous les exemples d'opérateurs morses de ce didacticiel nécessitent Python 3.8 ou une version ultérieure pour fonctionner.

Principes de base de l'opérateur morse

Commençons par quelques termes différents que les programmeurs utilisent pour faire référence à cette nouvelle syntaxe. Vous en avez déjà vu quelques-uns dans ce tutoriel.

Le := l'opérateur est officiellement connu sous le nom de opérateur d'expression d'affectation. Au cours des premières discussions, il a été surnommé le opérateur morse parce que le := la syntaxe ressemble aux yeux et aux défenses d'un morse de côté. Vous pouvez également voir le := opérateur dénommé opérateur deux points est égal à. Un autre terme utilisé pour les expressions d'affectation est expressions nommées.

Salut Morse !

Pour avoir une première impression de ce que sont les expressions d'affectation, démarrez votre REPL et jouez avec le code suivant :

>>>

    1>>> morse = Faux
    2>>> morse
    3Faux
    4
    5>>> (morse := Vrai)
    6Vrai
    7>>> morse
    8Vrai

La ligne 1 montre une instruction d'affectation traditionnelle où la valeur Faux est attribué à morse. Ensuite, à la ligne 5, vous utilisez une expression d'affectation pour affecter la valeur Vrai à morse. Après les deux lignes 1 et 5, vous pouvez vous référer aux valeurs attribuées en utilisant le nom de la variable morse.

Vous vous demandez peut-être pourquoi vous utilisez des parenthèses à la ligne 5, et vous apprendrez pourquoi les parenthèses sont nécessaires plus tard dans ce didacticiel.

Il existe une différence subtile, mais importante, entre les deux types d'affectations vues précédemment avec le morse variable. Une expression d'affectation renvoie la valeur, contrairement à une affectation traditionnelle. Vous pouvez le voir en action lorsque le REPL n'imprime aucune valeur après morse = Faux à la ligne 1, pendant qu'il s'imprime Vrai après l'expression d'affectation à la ligne 5.

Vous pouvez voir un autre aspect important des opérateurs morses dans cet exemple. Bien qu'il puisse sembler neuf, le := l'opérateur fait ne pas faire tout ce qui n'est pas possible sans cela. Cela ne fait que rendre certaines constructions plus pratiques et peut parfois communiquer plus clairement l'intention de votre code.

Vous avez maintenant une idée de base de ce que := l'opérateur est et ce qu'il peut faire. C'est un opérateur utilisé dans les expressions d'affectation, qui peut renvoyer la valeur affectée, contrairement aux instructions d'affectation traditionnelles. Pour approfondir et vraiment en savoir plus sur l'opérateur morse, continuez à lire pour voir où vous devriez et ne devriez pas l'utiliser.

Mise en œuvre

Comme la plupart des nouvelles fonctionnalités de Python, les expressions d'affectation ont été introduites via un Proposition d'amélioration Python (DYNAMISME). PEP 572 décrit la motivation pour introduire l'opérateur morse, les détails de la syntaxe, ainsi que des exemples où le := peut être utilisé pour améliorer votre code.

Ce PEP a été initialement écrit par Chris Angelico en février 2018. Après de vives discussions, le PEP 572 a été accepté par Guido van Rossum en juillet 2018. Depuis lors, Guido a annoncé qu'il se retirait de son rôle de dictateur bienveillant à vie (BDFL). À partir du début de 2019, Python est plutôt gouverné par un conseil de direction élu.

L'opérateur morse a été implémenté par Emily Morehouse et disponible dans la première version alpha de Python 3.8.

Motivation

Dans de nombreux langages, y compris C et ses dérivés, les instructions d'affectation fonctionnent comme des expressions. Cela peut être à la fois très puissant et aussi une source de bugs déroutants. Par exemple, le code suivant est du C valide mais ne s'exécute pas comme prévu :

entier X = 3, oui = 8;
si (X = oui) 
    imprimer("x et y sont égaux (x = %d, y = %d)", X, oui);

Ici, si (x = y) évaluera à vrai et l'extrait de code s'imprimera x et y sont égaux (x = 8, y = 8). Est-ce le résultat que vous attendiez ? Tu essayais de comparer X et oui. Comment la valeur de X changer de 3 à 8?

Le problème est que vous utilisez l'opérateur d'affectation (=) au lieu de l'opérateur de comparaison d'égalité (==). En C, x = y est une expression qui évalue à la valeur de oui. Dans cet exemple, x = y est évalué comme 8, ce qui est considéré comme véridique dans le contexte de la si déclaration.

Jetez un œil à un exemple correspondant en Python. Ce code soulève un Erreur de syntaxe:

X, oui = 3, 8
si X = oui:
    imprimer(F"x et y sont égaux (X = , oui = )")

Contrairement à l'exemple C, ce code Python vous donne une erreur explicite au lieu d'un bogue.

La distinction entre les instructions d'affectation et les expressions d'affectation en Python est utile afin d'éviter ce genre de bogues difficiles à trouver. Le PEP 572 soutient que Python est mieux adapté pour avoir une syntaxe différente pour les instructions d'affectation et les expressions au lieu de transformer les instructions d'affectation existantes en expressions.

Un principe de conception qui sous-tend l'opérateur morse est qu'il n'y a pas de contextes de code identiques où à la fois une instruction d'affectation utilisant le = opérateur et une expression d'affectation à l'aide de l'opérateur := l'opérateur serait valide. Par exemple, vous ne pouvez pas faire une affectation simple avec l'opérateur morse :

>>>

>>> morse := Vrai
  Fichier "", ligne 1
    morse := Vrai
           ^
Erreur de syntaxe: Syntaxe invalide

Dans de nombreux cas, vous pouvez ajouter des parenthèses (()) autour de l'expression d'affectation pour la rendre valide Python :

>>>

>>> (morse := Vrai)  # Valide, mais les déclarations régulières sont préférées
Vrai

Rédaction d'une déclaration de mission traditionnelle avec = n'est pas autorisé à l'intérieur de ces parenthèses. Cela vous aide à détecter les bogues potentiels.

Plus loin dans ce didacticiel, vous en apprendrez plus sur les situations où l'opérateur morse n'est pas autorisé, mais vous découvrirez d'abord les situations dans lesquelles vous pourriez vouloir les utiliser.

Cas d'utilisation de l'opérateur Walrus

Dans cette section, vous verrez plusieurs exemples où l'opérateur morse peut simplifier votre code. Un thème général dans tous ces exemples est que vous éviterez différents types de répétition :

  • Appels de fonction répétés peut rendre votre code plus lent que nécessaire.
  • Déclarations répétées peut rendre votre code difficile à maintenir.
  • Appels répétés qui épuisent les itérateurs peut rendre votre code trop complexe.

Vous verrez comment l'opérateur morse peut vous aider dans chacune de ces situations.

Débogage

L'un des meilleurs cas d'utilisation de l'opérateur morse est sans doute le débogage d'expressions complexes. Supposons que vous souhaitiez trouver la distance entre deux emplacements le long de la surface de la Terre. Une façon de le faire est d'utiliser la formule haversine :

La formule haversine

φ représente la latitude et λ représente la longitude de chaque emplacement. Pour illustrer cette formule, vous pouvez calculer la distance entre Oslo (59,9°N 10,8°E) et Vancouver (49,3°N 123,1°W) comme suit :

>>>

>>> de math importer un péché, car, radians, péché, carré

>>> # Rayon approximatif de la Terre en kilomètres
>>> rad = 6371

>>> # Emplacements d'Oslo et de Vancouver
>>> 1, 1 = radians(59,9), radians(10.8)
>>> 2, 2 = radians(49,3), radians(-123,1)

>>> # Distance entre Oslo et Vancouver
>>> 2 * rad * un péché(
...     carré(
...         péché((2 - 1) / 2) ** 2
...         + car(1) * car(2) * péché((2 - 1) / 2) ** 2
...     )
... )
...
7181.7841229421165

Comme vous pouvez le voir, la distance d'Oslo à Vancouver est d'un peu moins de 7200 kilomètres.

Maintenant, disons que vous avez besoin de revérifier votre implémentation et que vous voulez voir dans quelle mesure les termes haversine contribuent au résultat final. Vous pouvez copier et coller le terme de votre code principal pour l'évaluer séparément. Cependant, vous pouvez également utiliser le := pour donner un nom à la sous-expression qui vous intéresse :

>>>

>>> 2 * rad * un péché(
...     carré(
...         (_hav := péché((2 - 1) / 2) ** 2)
...         + car(1) * car(2) * péché((2 - 1) / 2) ** 2
...     )
... )
...
7181.7841229421165

>>> _hav
0,008532325425222883

L'avantage d'utiliser l'opérateur morse ici est que vous calculez la valeur de l'expression complète et gardez une trace de la valeur de _hav en même temps. Cela vous permet de confirmer que vous n'avez introduit aucune erreur lors du débogage.

Listes et dictionnaires

Les listes sont de puissantes structures de données en Python qui représentent souvent une série d'attributs liés. De même, les dictionnaires sont utilisés partout dans Python et sont parfaits pour structurer les informations.

Parfois, lors de la configuration de ces structures de données, vous finissez par effectuer plusieurs fois la même opération. Comme premier exemple, calculez quelques statistiques descriptives de base d'une liste de nombres et stockez-les dans un dictionnaire :

>>>

>>> Nombres = [[[[2, 8, 0, 1, 1, 9, 7, 7]

>>> la description = 
...     "longueur": longueur(Nombres),
...     "somme": somme(Nombres),
...     "signifier": somme(Nombres) / longueur(Nombres),
... 

>>> la description
'longueur' : 8, 'somme' : 35, 'moyenne' : 4,375

Notez que la somme et la longueur des Nombres liste sont calculés deux fois. Les conséquences ne sont pas trop graves dans cet exemple simple, mais si la liste était plus longue ou si les calculs étaient plus compliqués, vous voudrez peut-être optimiser le code. Pour ce faire, vous pouvez d'abord déplacer les appels de fonction hors de la définition du dictionnaire :

>>>

>>> Nombres = [[[[2, 8, 0, 1, 1, 9, 7, 7]

>>> nombre_longueur = longueur(Nombres)
>>> somme_num = somme(Nombres)

>>> la description = 
...     "longueur": nombre_longueur,
...     "somme": somme_num,
...     "signifier": somme_num / nombre_longueur,
... 

>>> la description
'longueur' : 8, 'somme' : 35, 'moyenne' : 4,375

Les variables nombre_longueur et somme_num ne sont utilisées que pour optimiser les calculs à l'intérieur du dictionnaire. En utilisant l'opérateur morse, ce rôle peut être rendu plus clair :

>>>

>>> Nombres = [[[[2, 8, 0, 1, 1, 9, 7, 7]

>>> la description = 
...     "longueur": (nombre_longueur := longueur(Nombres)),
...     "somme": (somme_num := somme(Nombres)),
...     "signifier": somme_num / nombre_longueur,
... 

>>> la description
'longueur' : 8, 'somme' : 35, 'moyenne' : 4,375

nombre_longueur et somme_num sont maintenant définis à l'intérieur de la définition de la description. C'est un indice clair pour quiconque lit ce code que ces variables sont juste utilisées pour optimiser ces calculs et ne sont pas réutilisées plus tard.

Dans l'exemple suivant, vous travaillerez avec une implémentation simple du toilettes utilitaire pour compter les lignes, les mots et les caractères dans un fichier texte :

    1# wc.py
    2
    3importer pathlib
    4importer système
    5
    6pour nom de fichier dans système.argv[[[[1:]:
    7    chemin = pathlib.Chemin(nom de fichier)
    8    compte = (
    9        chemin.read_text().compter("n"),  # Nombre de lignes
dix        longueur(chemin.read_text().diviser()),  # Nombre de mots
11        longueur(chemin.read_text()),  # Nombre de caractères
12    )
13    imprimer(*compte, chemin)

Ce script peut lire un ou plusieurs fichiers texte et indiquer le nombre de lignes, de mots et de caractères que chacun d'eux contient. Voici une ventilation de ce qui se passe dans le code :

  • Ligne 6 boucle sur chaque nom de fichier fourni par l'utilisateur. sys.argv est une liste contenant chaque argument donné sur la ligne de commande, commençant par le nom de votre script. Pour plus d'informations sur sys.argv, vous pouvez consulter les arguments de ligne de commande Python.
  • Ligne 7 traduit chaque chaîne de nom de fichier en un pathlib.Chemin objet. Stockage d'un nom de fichier dans un Chemin object vous permet de lire facilement le fichier texte dans les lignes suivantes.
  • Lignes 8 à 12 construire un tuple de comptes pour représenter le nombre de lignes, de mots et de caractères dans un fichier texte.
  • Ligne 9 lit un fichier texte et calcule le nombre de lignes en comptant les retours à la ligne.
  • Ligne 10 lit un fichier texte et calcule le nombre de mots en les divisant sur des espaces.
  • Ligne 11 lit un fichier texte et calcule le nombre de caractères en trouvant la longueur de la chaîne.
  • Ligne 13 imprime les trois comptes avec le nom du fichier sur la console. Le *compte la syntaxe décompresse le compte tuple. Dans ce cas, le imprimer() l'énoncé équivaut à imprimer (compte[0], compte[1], compte[2], chemin).

Voir wc.py en action, vous pouvez utiliser le script sur lui-même comme suit :

$ python wc.py wc.py
13 34 316 wc.py

En d'autres termes, le wc.py Le fichier se compose de 13 lignes, 34 mots et 316 caractères.

Si vous regardez attentivement cette implémentation, vous remarquerez qu'elle est loin d'être optimale. En particulier, l'appel à chemin.read_text() est répété trois fois. Cela signifie que chaque fichier texte est lu trois fois. Vous pouvez utiliser l'opérateur morse pour éviter la répétition :

# wc.py

importer pathlib
importer système

pour nom de fichier dans système.argv[[[[1:]:
    chemin = pathlib.Chemin(nom de fichier)
    compte = [[[[
        (texte := chemin.read_text()).compter("n"),  # Nombre de lignes
        longueur(texte.diviser()),  # Nombre de mots
        longueur(texte),  # Nombre de caractères
    ]
    imprimer(*compte, chemin)

Le contenu du fichier est affecté à texte, qui est réutilisé dans les deux prochains calculs. Le programme fonctionne toujours de la même manière :

$ python wc.py wc.py
13 36 302 wc.py

Comme dans les exemples précédents, une approche alternative consiste à définir texte avant la définition de compte:

# wc.py

importer pathlib
importer système

pour nom de fichier dans système.argv[[[[1:]:
    chemin = pathlib.Chemin(nom de fichier)
    texte = chemin.read_text()
    compte = [[[[
        texte.compter("n"),  # Nombre de lignes
        longueur(texte.diviser()),  # Nombre de mots
        longueur(texte),  # Nombre de caractères
    ]
    imprimer(*compte, chemin)

Bien qu'il s'agisse d'une ligne de plus que l'implémentation précédente, il offre probablement le meilleur équilibre entre lisibilité et efficacité. Le := L'opérateur d'expression d'affectation n'est pas toujours la solution la plus lisible même lorsqu'il rend votre code plus concis.

Compréhensions de liste

Les compréhensions de liste sont idéales pour construire et filtrer des listes. Ils indiquent clairement l'intention du code et s'exécutent généralement assez rapidement.

Il existe un cas d'utilisation de la compréhension de liste où l'opérateur morse peut être particulièrement utile. Supposons que vous vouliez appliquer une fonction coûteuse en calcul, ralentir(), aux éléments de votre liste et filtrez sur les valeurs résultantes. Vous pouvez faire quelque chose comme ce qui suit :

Nombres = [[[[7, 6, 1, 4, 1, 8, 0, 6]

résultats = [[[[ralentir(nombre) pour nombre dans Nombres si ralentir(nombre) > 0]

Ici, vous filtrez les Nombres liste et laisser les résultats positifs de l'application ralentir(). Le problème avec ce code est que cette fonction coûteuse est appelée deux fois.

Une solution très courante pour ce type de situation consiste à réécrire votre code pour utiliser un pour boucle:

résultats = []
pour nombre dans Nombres:
    valeur = ralentir(nombre)
    si valeur > 0:
        résultats.ajouter(valeur)

Cela n'appellera que ralentir() une fois. Malheureusement, le code est maintenant plus détaillé et l'intention du code est plus difficile à comprendre. La compréhension de la liste avait clairement signalé que vous étiez en train de créer une nouvelle liste, alors que cela est plus caché dans l'explicite pour boucle puisque plusieurs lignes de code séparent la création de la liste et l'utilisation de .ajouter(). De plus, la compréhension de la liste est plus rapide que les appels répétés à .ajouter().

Vous pouvez coder d'autres solutions en utilisant un filtre() expression ou une sorte de compréhension en double liste :

# Utilisation du filtre
résultats = filtre(lambda valeur: valeur > 0, (ralentir(nombre) pour nombre dans Nombres))

# Utiliser une compréhension en double liste
résultats = [[[[valeur pour nombre dans Nombres pour valeur dans [[[[ralentir(nombre)] si valeur > 0]

La bonne nouvelle est qu'il n'y a qu'un seul appel à ralentir() pour chaque numéro. La mauvaise nouvelle est que la lisibilité du code a souffert dans les deux expressions.

Comprendre ce qui se passe réellement dans la compréhension de la double liste prend beaucoup de temps. Essentiellement, le deuxième pour l'instruction est utilisée uniquement pour donner le nom valeur à la valeur de retour de lent(nombre). Heureusement, cela ressemble à quelque chose qui peut être exécuté à la place avec une expression d'affectation !

Vous pouvez réécrire la compréhension de liste à l'aide de l'opérateur morse comme suit :

résultats = [[[[valeur pour nombre dans Nombres si (valeur := ralentir(nombre)) > 0]

Notez que les parenthèses autour value := slow(num) sont requises. Cette version est efficace, lisible et communique bien l'intention du code.

Regardons un exemple un peu plus impliqué et pratique. Dites que vous voulez utiliser le Python réel pour retrouver les titres des derniers épisodes du Real Python Podcast.

Vous pouvez utiliser Real Python Feed Reader pour télécharger des informations sur les dernières Python réel parutions. Pour trouver les titres des épisodes de podcast, vous utiliserez le package Parse. Commencez par les installer dans votre environnement virtuel :

$ python -m pip install realpython-reader parse

Vous pouvez désormais lire les derniers titres publiés par Python réel:

>>>

>>> de lecteur importer alimentation

>>> alimentation.obtenir_titres()
['TheWalrusOperator:Python38AssignmentExpressions'['TheWalrusOperator:Python38AssignmentExpressions'['TheWalrusOperator:Python38AssignmentExpressions'['TheWalrusOperator:Python38AssignmentExpressions'
    'The Real Python Podcast - Episode #63: Créer des applications Web à l'aide d'Anvil',
    « Gestionnaires de contexte et Python avec instruction »,
    ...]

Les titres de podcast commencent par "Le vrai podcast Python", vous pouvez donc créer ici un modèle que Parse peut utiliser pour les identifier :

>>>

>>> importer analyser

>>> modèle = analyser.compiler(
...     "Le vrai podcast Python - Épisode #num:d: Nom"
... )

La compilation préalable du modèle accélère les comparaisons ultérieures, en particulier lorsque vous souhaitez faire correspondre le même modèle encore et encore. Vous pouvez vérifier si une chaîne correspond à votre modèle en utilisant soit pattern.parse() ou alors modèle.search():

>>>

>>> modèle.analyser(
...     « Le vrai podcast Python - Épisode #63 : »
...     "Créer des applications Web à l'aide d'Anvil"
... )
...

Notez que Parse est capable de choisir le numéro de l'épisode du podcast et le nom de l'épisode. Le numéro d'épisode est converti en un type de données entier car vous avez utilisé le :ré spécificateur de format.

Revenons à la tâche à accomplir. Afin de répertorier tous les titres de podcast récents, vous devez vérifier si chaque chaîne correspond à votre modèle, puis analyser le titre de l'épisode. Une première tentative peut ressembler à ceci :

>>>

>>> importer analyser
>>> de lecteur importer alimentation

>>> modèle = analyser.compiler(
...     "Le vrai podcast Python - Épisode #num:d: Nom"
... )

>>> balados = [[[[
...     modèle.analyser(Titre)[[[["Nom"]
...     pour Titre dans alimentation.obtenir_titres()
...     si modèle.analyser(Titre)
... ]

>>> balados[:[:[:[:3]
['CreateWebApplicationsUsingOnlyPythonWithAnvil'['CreateWebApplicationsUsingOnlyPythonWithAnvil'['CreateWebApplicationsUsingOnlyPythonWithAnvil'['CreateWebApplicationsUsingOnlyPythonWithAnvil'
    'Sélectionner la structure de données idéale et démêler les "passes" et "avec" de Python',
    « Mettre à l'échelle une infrastructure de science des données et d'apprentissage automatique comme Netflix »]

Bien que cela fonctionne, vous remarquerez peut-être le même problème que vous avez vu plus tôt. Vous analysez chaque titre deux fois parce que vous filtrez les titres qui correspondent à votre modèle, puis utilisez ce même modèle pour choisir le titre de l'épisode.

Comme vous l'avez fait plus tôt, vous pouvez éviter le double travail en réécrivant la compréhension de la liste en utilisant soit un pour boucle ou une double liste de compréhension. Cependant, l'utilisation de l'opérateur morse est encore plus simple :

>>>

>>> balados = [[[[
...     Podcast[[[["Nom"]
...     pour Titre dans alimentation.obtenir_titres()
...     si (Podcast := modèle.analyser(Titre))
... ]

Les expressions d'affectation fonctionnent bien pour simplifier ces types de compréhension de liste. Ils vous aident à garder votre code lisible tout en évitant de faire deux fois une opération potentiellement coûteuse.

Dans cette section, vous vous êtes concentré sur des exemples où les compréhensions de liste peuvent être réécrites à l'aide de l'opérateur morse. Les mêmes principes s'appliquent également si vous constatez que vous devez répéter une opération dans une compréhension de dictionnaire, une compréhension d'ensemble ou une expression génératrice.

L'exemple suivant utilise une expression génératrice pour calculer la durée moyenne des titres d'épisodes qui sont terminés 50 Longs caractères:

>>>

>>> importer statistiques

>>> statistiques.signifier(
...     title_length
...     pour Titre dans balados
...     si (title_length := longueur(Titre)) > 50
... )
65.425

L'expression du générateur utilise une expression d'affectation pour éviter de calculer deux fois la longueur de chaque titre d'épisode.

Tandis que les boucles

Python a deux constructions de boucle différentes : pour boucles et tandis que boucles. Vous utilisez généralement un pour boucle lorsque vous devez parcourir une séquence connue d'éléments. UNE tandis que boucle, d'autre part, est utilisé lorsque vous ne savez pas à l'avance combien de fois vous aurez besoin de boucler.

Dans tandis que boucles, vous devez définir et vérifier la condition de fin en haut de la boucle. Cela conduit parfois à un code maladroit lorsque vous devez effectuer une configuration avant d'effectuer la vérification. Voici un extrait d'un programme de quiz à choix multiples qui demande à l'utilisateur de répondre à une question avec l'une des nombreuses réponses valides :

question = « Utiliserez-vous l'opérateur morse ? »
réponses_valides = "Oui", "Oui", "ou", "O", "non", "Non", "n", "N"

réponse de l'utilisateur = saisir(F"nquestion    ")
tandis que réponse de l'utilisateur ne pas dans réponses_valides:
    imprimer(F"Veuillez répondre à l'une des ', '.rejoindre(réponses_valides)")
    réponse de l'utilisateur = saisir(F"nquestion    ")

Cela fonctionne mais a une répétition malheureuse d'identiques saisir() lignes. Il est nécessaire d'obtenir au moins une réponse de l'utilisateur avant de vérifier si elle est valide ou non. Vous avez alors un deuxième appel à saisir() à l'intérieur de tandis que boucle pour demander une deuxième réponse au cas où l'original réponse de l'utilisateur n'était pas valide.

Si vous voulez rendre votre code plus maintenable, il est assez courant de réécrire ce genre de logique avec un tandis que vrai boucle. Au lieu de faire du chèque une partie du tandis que instruction, la vérification est effectuée plus tard dans la boucle avec une instruction explicite Pause:

tandis que Vrai:
    réponse de l'utilisateur = saisir(F"nquestion    ")
    si réponse de l'utilisateur dans réponses_valides:
        Pause
    imprimer(F"Veuillez répondre à l'une des ', '.rejoindre(réponses_valides)")

Cela a l'avantage d'éviter la répétition. Cependant, le contrôle réel est maintenant plus difficile à repérer.

Les expressions d'affectation peuvent souvent être utilisées pour simplifier ces types de boucles. Dans cet exemple, vous pouvez maintenant remettre le chèque avec tandis que où cela a plus de sens :

tandis que (réponse de l'utilisateur := saisir(F"nquestion    ")) ne pas dans réponses_valides:
    imprimer(F"Veuillez répondre à l'une des ', '.rejoindre(réponses_valides)")

Le tandis que L'instruction est un peu plus dense, mais le code communique maintenant l'intention plus clairement sans lignes répétées ou boucles apparemment infinies.

Vous pouvez développer la boîte ci-dessous pour voir le code complet du programme de quiz à choix multiples et essayer vous-même quelques questions sur l'opérateur morse.

Ce script exécute un quiz à choix multiples. On vous posera chacune des questions dans l'ordre, mais l'ordre des réponses sera modifié à chaque fois :

# morse_quiz.py

importer Aléatoire
importer chaîne de caractères

DES QUESTIONS = 
    « Quel est le nom du PEP 572 ? »: [[[[
        "Expressions d'affectation",
        « Expressions nommées »,
        "L'Opérateur Morse",
        « L'opérateur du côlon est égal à »,
    ],
    « Lequel de ceux-ci est une utilisation invalide de l'opérateur morse ? »: [[[[
        "[y**2 for x in range(10) if y := f(x) > 0]",
        "imprimer(y := f(x))",
        "(y := f(x))",
        "tout((y := f(x)) pour x dans la plage (10))",
    ],


nombre_correct = 0
pour question, answers dans QUESTIONS.items():
    correct = answers[[[[0]
    random.shuffle(answers)

    coded_answers = dict(zip(chaîne de caractères.ascii_lowercase, answers))
    valid_answers = sorted(coded_answers.keys())

    pour code, answer dans coded_answers.items():
        imprimer(f"  code) answer")

    while (user_answer := input(f"nquestion    ")) ne pas dans valid_answers:
        imprimer(f"Please answer one of ', '.join(valid_answers)")

    if coded_answers[[[[user_answer] == correct:
        imprimer(f"Correct, the answer is user_answer!rn")
        num_correct += 1
    else:
        imprimer(f"No, the answer is correct!rn")

imprimer(f"You got num_correct    correct out of len(QUESTIONS)    questions")

Note that the first answer is assumed to be the correct one. You can add more questions to the quiz yourself. Feel free to share your questions with the community in the comments section below the tutorial!

You can often simplify while loops by using assignment expressions. The original PEP shows an example from the standard library that makes the same point.

Witnesses and Counterexamples

In the examples you’ve seen so far, the := assignment expression operator does essentially the same job as the = assignment operator in your old code. You’ve seen how to simplify code, and now you’ll learn about a different type of use case that’s made possible by this new operator.

In this section, you’ll learn how you can find witnesses when calling any() by using a clever trick that isn’t possible without using the walrus operator. A witness, in this context, is an element that satisfies the check and causes any() to return Vrai.

By applying similar logic, you’ll also learn how you can find counterexamples when working with all(). A counterexample, in this context, is an element that doesn’t satisfy the check and causes all() to return Faux.

In order to have some data to work with, define the following list of city names:

>>>

>>> cities = [[[["Vancouver", "Oslo", "Houston", "Warsaw", "Graz", "Holguín"]

You can use any() et all() to answer questions about your data:

>>>

>>> # Does ANY city name start with "H"?
>>> any(ville.startswith("H") pour ville dans cities)
Vrai

>>> # Does ANY city name have at least 10 characters?
>>> any(len(ville) >= dix pour ville dans cities)
Faux

>>> # Do ALL city names contain "a" or "o"?
>>> tous(set(ville) & set("ao") pour ville dans cities)
Vrai

>>> # Do ALL city names start with "H"?
>>> tous(ville.startswith("H") pour ville dans cities)
Faux

In each of these cases, any() et all() give you plain Vrai ou alors Faux answers. What if you’re also interested in seeing an example or a counterexample of the city names? It could be nice to see what’s causing your Vrai ou alors Faux result:

  • Fait any city name start with "H"?

    Yes, because "Houston" starts with "H".

  • Do tous city names start with "H"?

    No, because "Oslo" doesn’t start with "H".

In other words, you want a witness or a counterexample to justify the answer.

Capturing a witness to an any() expression has not been intuitive in earlier versions of Python. If you were calling any() on a list and then realized you also wanted a witness, you’d typically need to rewrite your code:

>>>

>>> witnesses = [[[[ville pour ville dans cities if ville.startswith("H")]

>>> if witnesses:
...     imprimer(f"witnesses[[[[0]    starts with H")
... else:
...     imprimer("No city name starts with H")
...
Houston starts with H

Here, you first capture all city names that start with "H". Then, if there’s at least one such city name, you print out the first city name starting with "H". Note that here you’re actually not using any() even though you’re doing a similar operation with the list comprehension.

By using the := operator, you can find witnesses directly in your any() expressions:

>>>

>>> if any((witness := ville).startswith("H") pour ville dans cities):
...     imprimer(f"witness    starts with H")
... else:
...     imprimer("No city name starts with H")
...
Houston starts with H

You can capture a witness inside the any() expression. The reason this works is a bit subtle and relies on any() et all() using short-circuit evaluation: they only check as many items as necessary to determine the result.

You can see what’s happening more clearly by wrapping .startswith("H") in a function that also prints out which item is being checked:

>>>

>>> def starts_with_h(Nom):
...     imprimer(f"Checking Nom: Nom.startswith('H')")
...     return Nom.startswith("H")
...

>>> any(starts_with_h(ville) pour ville dans cities)
Checking Vancouver: False
Checking Oslo: False
Checking Houston: True
Vrai

Note that any() doesn’t actually check all items in cities. It only checks items until it finds one that satisfies the condition. Combining the := operator and any() works by iteratively assigning each item that is being checked to witness. However, only the last such item survives and shows which item was last checked by any().

Even when any() returns Faux, a witness is found:

>>>

>>> any(len(witness := ville) >= dix pour ville dans cities)
Faux

>>> witness
'Holguín'

However, in this case, witness doesn’t give any insight. 'Holguín' doesn’t contain 10 or more characters. The witness only shows which item happened to be evaluated last.

Walrus Operator Syntax

One of the main reasons assignments were not expressions in Python from the beginning is that the visual likeness of the assignment operator (=) and the equality comparison operator (==) could potentially lead to bugs. When introducing assignment expressions, a lot of thought was put into how to avoid similar bugs with the walrus operator. As mentioned earlier, one important feature is that the := operator is never allowed as a direct replacement for the = operator, and vice versa.

As you saw at the beginning of this tutorial, you can’t use a plain assignment expression to assign a value:

>>>

>>> walrus := Vrai
  File "", line 1
    walrus := Vrai
           ^
SyntaxError: invalid syntax

It’s syntactically legal to use an assignment expression to only assign a value, but only if you add parentheses:

>>>

>>> (walrus := Vrai)
Vrai

Even though it’s possible, however, this really is a prime example of where you should stay away from the walrus operator and use a traditional assignment statement instead.

PEP 572 shows several other examples where the := operator is either illegal or discouraged. The following examples all raise a SyntaxError:

>>>

>>> lat = lon := 0
SyntaxError: invalid syntax

>>> angle(phi = lat := 59.9)
SyntaxError: invalid syntax

>>> def distance(phi = lat := 0, lam = lon := 0):
SyntaxError: invalid syntax

In all these cases, you’re better served using = instead. The next examples are similar and are all legal code. However, the walrus operator doesn’t improve your code in any of these cases:

>>>

>>> lat = (lon := 0)  # Discouraged

>>> angle(phi = (lat := 59.9))  # Discouraged

>>> def distance(phi = (lat := 0), lam = (lon := 0)):  # Discouraged
...     pass
...

None of these examples make your code more readable. You should instead do the extra assignment separately by using a traditional assignment statement. See PEP 572 for more details about the reasoning.

There’s one use case where the := character sequence is already valid Python. In f-strings, a colon (:) is used to separate values from their format specification. Par exemple:

>>>

>>> x = 3
>>> f"x:=8"
'       3'

Le := in this case does look like a walrus operator, but the effect is quite different. To interpret x:=8 inside the f-string, the expression is broken into three parts: x, :, et =8.

Here, x is the value, : acts as a separator, and =8 is a format specification. According to Python’s Format Specification Mini-Language, in this context = specifies an alignment option. In this case, the value is padded with spaces in a field of width 8.

To use assignment expressions inside f-strings, you need to add parentheses:

>>>

>>> x = 3
>>> f"(x := 8)"
'8'

>>> x
8

This updates the value of x as expected. However, you’re probably better off using traditional assignments outside of your f-strings instead.

Let’s look at some other situations where assignment expressions are illegal:

  • Attribute and item assignment: You can only assign to simple names, not dotted or indexed names:

    >>>

    >>> (mapping[[[["hearts"] := "♥")
    SyntaxError: cannot use assignment expressions with subscript
    
    >>> (number.answer := 42)
    SyntaxError: cannot use assignment expressions with attribute
    

    This fails with a descriptive error message. There’s no straightforward workaround.

  • Iterable unpacking: You can’t unpack when using the walrus operator:

    >>>

    >>> lat, lon := 59.9, 10.8
    SyntaxError: invalid syntax
    

    If you add parentheses around the whole expression, it will be interpreted as a 3-tuple with the three elements lat, 59.9, et 10.8.

  • Augmented assignment: You can’t use the walrus operator combined with augmented assignment operators like +=. This raises a SyntaxError:

    >>>

    >>> compter +:= 1
    SyntaxError: invalid syntax
    

    The easiest workaround would be to do the augmentation explicitly. You could, for example, do (count := count + 1). PEP 577 originally described how to add augmented assignment expressions to Python, but the proposal was withdrawn.

When you’re using the walrus operator, it will behave similarly to traditional assignment statements in many respects:

  • Le scope of the assignment target is the same as for assignments. It will follow the LEGB rule. Typically, the assignment will happen in the local scope, but if the target name is already declared global ou alors nonlocal, that is honored.

  • Le precedence of the walrus operator can cause some confusion. It binds less tightly than all other operators except the comma, so you might need parentheses to delimit the expression that is assigned. As an example, note what happens when you don’t use parentheses:

    >>>

    >>> number = 3
    >>> if square := number ** 2 > 5:
    ...     imprimer(square)
    ...
    Vrai
    

    square is bound to the whole expression number ** 2 > 5. In other words, square gets the value Vrai and not the value of number ** 2, which was the intention. In this case, you can delimit the expression with parentheses:

    >>>

    >>> number = 3
    >>> if (square := number ** 2) > 5:
    ...     imprimer(square)
    ...
    9
    

    The parentheses make the if statement both clearer and actually correct.

    There’s one final gotcha. When assigning a tuple using the walrus operator, you always need to use parentheses around the tuple. Compare the following assignments:

    >>>

    >>> walrus = 3.7, Faux
    >>> walrus
    (3.7, False)
    
    >>> (walrus := 3.8, Vrai)
    (3.8, True)
    >>> walrus
    3.8
    
    >>> (walrus := (3.8, Vrai))
    (3.8, True)
    >>> walrus
    (3.8, True)
    

    Note that in the second example, walrus takes the value 3.8 and not the whole tuple 3.8, True. That’s because the := operator binds more tightly than the comma. This may seem a bit annoying. However, if the := operator bound less tightly than the comma, it would not be possible to use the walrus operator in function calls with more than one argument.

  • Le style recommendations for the walrus operator are mostly the same as for the = operator used for assignment. First, always add spaces around the := operator in your code. Second, use parentheses around the expression as necessary, but avoid adding extra parentheses that are not needed.

The general design of assignment expressions is to make them easy to use when they are helpful but to avoid overusing them when they might clutter up your code.

Walrus Operator Pitfalls

The walrus operator is a new syntax that is only available in Python 3.8 and later. This means that any code you write that uses the := syntax will only work on the most recent versions of Python.

If you need to support older versions of Python, you can’t ship code that uses assignment expressions. There are some projects, like walrus, that can automatically translate walrus operators into code that is compatible with older versions of Python. This allows you to take advantage of assignment expressions when writing your code and still distribute code that is compatible with more Python versions.

Experience with the walrus operator indicates that := will not revolutionize Python. Instead, using assignment expressions where they are useful can help you make several small improvements to your code that could benefit your work overall.

There are many times it’s possible for you to use the walrus operator, but where it won’t necessarily improve the readability or efficiency of your code. In those cases, you’re better off writing your code in a more traditional manner.

Conclusion

You now know how the new walrus operator works and how you can use it in your own code. By using the := syntax, you can avoid different kinds of repetition in your code and make your code both more efficient and easier to read and maintain. At the same time, you shouldn’t use assignment expressions everywhere. They will only help you in some use cases.

In this tutorial, you learned how to:

  • Identify the walrus operator and understand its meaning
  • Understand use cases for the walrus operator
  • Avoid repetitive code by using the walrus operator
  • Convert between code using the walrus operator and code using other assignment methods
  • Understand the impacts on backward compatibility when using the walrus operator
  • Use appropriate style in your assignment expressions

To learn more about the details of assignment expressions, see PEP 572. You can also check out the PyCon 2019 talk PEP 572: The Walrus Operator, where Dustin Ingram gives an overview of both the walrus operator and the discussion around the new PEP.



[ad_2]