variable – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 L’expression d’assignation vient d’être acceptée http://sametmax.com/lexpression-dassignation-vient-detre-acceptee/ http://sametmax.com/lexpression-dassignation-vient-detre-acceptee/#comments Tue, 03 Jul 2018 09:15:47 +0000 http://sametmax.com/?p=24766 python-idea (mailing list sur laquelle, je vous le rappelle, tout le monde a le droit de participer), Guido a validé la PEP 572. (foo := bar) sera donc un code valide en Python 3.8.]]> Après des mois de débats sur python-idea (mailing list sur laquelle, je vous le rappelle, tout le monde a le droit de participer), Guido a validé la PEP 572. (foo := bar) sera donc un code valide en Python 3.8.

Je n’avais pas vu de feature plus controversée depuis les f-strings. Et je gage que, comme les f-strings, après un temps d’adaptation, la communauté va se demander comment on a vécu sans auparavant.

Un peu d’histoire

Il existe deux grandes catégories d’instructions dans les langages de programmation. Les déclarations et les expressions.

La déclaration, en anglais “statement”, est une action indépendante formulée sur une ligne. C’est une unité syntaxique, et une déclaration ne peut être contenue dans une autre déclaration sur la même ligne.

Ex:

import os

est une déclaration. Un import est tout seul sur sa ligne et ne se combine pas avec d’autres instructions.

L’expression, elle, est une combinaison de littéraux, variables, d’opérateurs et d’appels de fonctions qui retournent un résultat. Les expressions peuvent contenir d’autres expressions, et elles peuvent être contenues dans une déclaration.

Ex:

1 + 1 

est une expression. On peut, en effet, assigner le résultat de ce code à une variable, le passer à une fonction en paramètre ou le tester dans une condition.

Les langages, comme le COBOL, qui privilégient le style impératif utilisent majoritairement des déclarations. Les langages, comme LISP, qui privilégient le style fonctionnel, utilisent majoritairement des expressions.

Très souvent néanmoins, les langages populaires font largement usage des deux. L’expression est plus flexible et plus puissante. La déclaration force une opinion sur la structure du programme et évite les divergences de style. Selon la philosophie que l’on souhaite donner à son bébé, un créateur de langage va donc s’orienter plus vers l’une ou l’autre.

Python est multiparadigme et soutient le style impératif, fonctionnel et orienté objet. Il possède donc non seulement des expressions et des déclarations, mais également souvent les deux versions pour une même instruction.

Ex, les déclarations:

squares = []
for x in numbers:
    squares.append(x * x)

def mean(x):
    return x * x / 2

if is_ok:
    print(welcome)
else:
    print(error)

ont des expressions équivalentes:


squares = [x * x for x in number]

mean = lambda x: x * x / 2

print(welcome if is_ok else error)

Parfois Python choisit d’orienter le style du programmeur, non pas évitant de fournir des expressions, mais par le biais de la grammaire imposée. Ainsi il oblige à indenter, et n’autorise pas les lambdas à contenir de déclaration. C’est une autre stratégie pour imposer une philosophie au langage. Pour Python, la philosophie est que la capacité à s’exprimer doit rester riche, mais pas au détriment de la capacité à comprendre le code.

Une particularité de Python, c’est que l’assignation, c’est-à-dire le fait d’associer une valeur à une variable, est une déclaration. Ex:

a = 1

Comme les déclarations ne peuvent contenir d’autres déclarations, cette syntaxe interdit:

if a = 1:  

Ce n’est pas le cas dans de nombreux langages populaires, comme le C, PHP, le JS… Exemple en Ruby:

if (value = Settings.get('test_setting'))
  perform_action(value)
end

Ici, non seulement on assigne le résultat du get() à la variable value, mais en plus, on teste le résultat du get() avec le if.

Actuellement ceci est impossible en Python, et le même code serait:

value = Settings.get('test_setting')
if value:
  perform_action(value)

Ce n’est pas une erreur, c’est un choix de design. En effet une source de bug très courante en programmation est de vouloir faire une comparaison, mais de taper une assignation.

Ainsi, quelqu’un voudrait faire:

while reponse == "oui":
    ...
    reponse = input('Voulez-vous continuer ?')

Mais ferait:

while reponse = "oui": # erreur subtile et difficile à voir
    ...
    reponse = input('Voulez-vous continuer ?')

Ce qui ne compare pas DU TOUT la variable. Et en plus change son contenu. Dans ce cas précis, le résultat est une boucle infinie, mais parfaitement valide et qui ne provoquera pas d’erreur.

Pour éviter ce genre de bug que même les programmeurs aguerris font un jour de fatigue, la syntaxe a tout simplement été interdite en stipulant que l’assignation était toujours une déclaration.

La PEP 572

L’absence de cette fonctionnalité a eu d’excellents bénéfices. Je le vois régulièrement dans mes salles de classe, ce bug. La SyntaxError qui résulte de cette faute en Python permet de l’attraper avant qu’il ne fasse le moindre dégât.

Car c’est tout le problème de cette erreur: si la syntaxe est valide, le bug est silencieux, le code tourne, il ne fait juste pas du tout ce qu’on lui demande. C’est la plus pernicieuse des situations, avec des conséquences qui peuvent ne se déclarer que bien plus loin dans le code et une séance de débuggage des plus irritantes.

A mon sens, c’était une excellente décision de design.

Pourtant la PEP 572 revient dessus, en proposant, comme pour lambda, la boucle for ou la condition, un équivalent sous forme d’expression.

Pourquoi ?

Et bien Python doit aussi satisfaire les programmeurs chevronnés, qui en ont marre de se retrouver avec des situations comme :

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

Certes, ce code est clair et facile à comprendre, mais il est très verbeux. On doit faire avec une indentation artificiellement induite par la limite de la syntaxe, et non par la logique du raisonnement qui est ici parfaitement linéaire.

Comment donc réconcilier le désir d’éviter le fameux bug tout en satisfaisant les besoins d’expressivité, légitimes une fois qu’on quitte le nid des débutants ?

La solution proposée est triple :

  • Ajouter un nouvel opérateur dédié, permettant l’assignation sous forme d’expression.
  • Forcer l’usage des parenthèses pour encadrer cette expression.
  • Rendre les deux opérateurs d’assignation mutuellement exclusifs.

Le nouvel opérateur choisi est := et il ne peut exister qu’entre parenthèses. Il peut être utilisé là où un simple = ne serait pas autorisé. Mais, il ne peut PAS être utilisé là où on peut utiliser =. Le but est de ne jamais mettre ces deux opérateurs en concurrence: une situation permet l’un ou l’autre, jamais les deux.

= ne change pas. La seule différence, c’est qu’à partir de Python 3.8, vous aurez le droit de faire:

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

L’opérateur := permet donc bien, dans des situations très précises, d’obtenir un code plus cours et élégant, sans introduire pourtant d’ambiguïté et donc de bug potentiel. Il autorise une nouvelle forme d’expressivité, mais sa syntaxe est très marquée: impossible de le confondre avec son frère, et les parenthèses l’isolent du reste du code.

On ne se pose pas non plus la question de quand choisir = et := puisque:

a = 1

est valide, mais pas:

a := 1

Bien que:

(a := 1)

soit valide, personne n’aura envie d’utiliser vainement cette forme plus lourde.

L’usage de := est donc marginal, et cantonné à des cas particuliers.

Une opinion, c’est comme un trou du cul…

Personnellement j’étais mitigé sur l’idée. De plus, j’aurais préféré l’usage du mot clé as, puisqu’on l’utilisait déjà dans les imports, les context managers et la gestion des exceptions.

Chaque ajout d’expression rajoute également la possibilité d’abus. Si vous avez déjà vu ces horreurs à base de lambdas imbriquées ou d’intensions sans fin, vous savez de quoi je parle.

Avec :=, on peut vraiment se lancer dans du grand n’importe quoi:

Pour reprendre l’exemple de la PEP:

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

est très clair.

En revanche:

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

Est une abomination qu’il faut purger par le feu.

Mais par expérience, j’ai rarement vu en 15 ans de Python beaucoup d’abus de ses fonctionnalités avancées. Les bénéfices ont toujours dépassé le coût d’une large marge. Pourtant entre les décorateurs, les dunder methods et les meta classes, il y a matière à messe noire.

Par ailleurs j’avoue que je suis ravi de pouvoir enfin faire:

while (data := io.get(x)):

Et:

[bar(x) for z in stuff if (x := foo(z))]

La PEP mentionne aussi un exemple que je n’avais pas prévu, et qui souffle le chaud et le froid:

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

Peut devenir:

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

Comme l’auteur, j’approuve le fait que la première version est inutilement verbeuse, mais contrairement à lui, je trouve que la seconde est bien trop complexe pour être scannée d’un coup d’œil.

En revanche:

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

est tout à fait à mon goût.

Ceci démontre bien qu’il va falloir un temps d’adaptation avant que la communauté trouve l’équilibre entre Perl et BASIC. Quoi qu’il en soit, on n’aura pas à s’en soucier avant l’année prochaine, et même d’ici là, peu de code pourra en faire usage avant que la 3.8 soit largement installée.

De mon côté je m’attends à ce qu’on ignore majoritairement cette fonctionnalité, jusqu’au moment où elle apparaîtra dans un coin de l’esprit le temps d’un besoin ponctuel et local, pour être oubliée à nouveau jusqu’à l’occasion suivante. Comme Dieu Guido l’aura voulu. D’ailleurs, côté enseignement, je ne compte pas introduire l’opérateur dans mes cours, ou alors dans une section bonus, à moins qu’un participant ne pose la question.

Aller, vous pouvez râler en commentaire maintenant :)

]]>
http://sametmax.com/lexpression-dassignation-vient-detre-acceptee/feed/ 20 24766
Bien nommer ses variables en Python http://sametmax.com/bien-nommer-ses-variables-en-python/ http://sametmax.com/bien-nommer-ses-variables-en-python/#comments Thu, 09 Oct 2014 09:42:31 +0000 http://sametmax.com/?p=12376 There are only two hard things in Computer Science: cache invalidation and naming things. Phil Karlton]]>

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

Utiliser des bons noms est le geste de documentation le plus important d’un code. Je ne parle pas de bien formater le nom des variables. Pour ça, il y a le PEP8 et ce qu’il recommande tient en 3 lignes :

  • Le nom des classes en CamelCase
  • Les (pseudo) constantes en UPPER_CASE
  • Le reste en snake_case

C’est tout.

Non, je parle de choisir un nom adapté et descriptif.

Le post est long, et vous savez que quand le post est long, je vous mets une musique d’attente.

Explicit is better than implicit

En Python, il n’y a pas de déclaration de type. Le nom d’une variable a donc d’autant plus d’importance pour expliquer ce qu’il y a dedans.

Prenez cet exemple :

best = []
for k, v in data.items():
    if v > top:
        best.append(k)

Quand on lit ce bout de code, on se demande :

  • Best ? Mais best en quoi ?
  • Que contient data ?
  • Quel est la nature de top ?

Maintenant, avec des noms explicites :

best_players = []
for player, score in data.items():
    if score > top_score :
        best_players.append(player)

On comprend tout de suite de quoi il est question. L’algo n’a pas changé, seul le nommage a changé.

Et si on passe à une écriture plus compacte, le gain est encore plus net :

best = [k for k, v in data.items() if k > top]

VS

best_players = [player for player, score in data.items() if score > top_score]

Parfois, on veut la concision, mais faute de nommage, on doit se retourner vers les commentaires :

# Get players with the best scores
best = [k for k, v in data.items() if k > top]

Néanmoins, si on doit faire le choix entre commenter et bien nommer, le nommage doit avoir priorité. Le commentaire est important, mais c’est le dernier recours d’un code qui n’est pas explicite. Avoir un code clair doit être l’objectif premier. Ensuite, seulement, on le commente (abondamment toutefois, faut pas être radin).

Si vous avez suivi, je nomme mes variables en fonction de leur nature, pas leur type. On peut utiliser les règles de l’orthographe pour encore plus de précision, par exemple le pluriel.

fruits = ["kiwi", "banane", 'poire']
for fruit in fruits:
    print(fruit)

J’utilise le pluriel pour une liste de données, qui va donc potentiellement contenir plusieurs fruits. Mais j’utilise un singulier dans la boucle pour le fruit en court.

L’utilisation d’adjectifs est aussi bienvenue :

fruits peut devenir, après un traitement filtered_fruits, indiquant que la liste a subi un filtrage. Les mots en “ed” en anglais aident beaucoup à la qualification.

On évite au maximum les variables courtes. Certains cas sont néanmoins tolérés. Le premier est l’utilisation de i, x, yet z pour des indices.

for i, fruit in enumerate(fruits):
    # faire un truc

Les indices sont quelque chose de tellement courant en informatique qu’on ne va pas se gaver à l’appeler “indice” à chaque fois.

Le second est dans le cadre scientifique. On a souvent des variables pour un algo, des coordonnées, des valeurs géométriques ou mathématiques, qui n’ont pas de dénomination. Dans ce cas, inutile d’essayer d’inventer une nomenclature tordue. Exemple typique, l’algo pour pondre un MD5. Mais il faut compenser par des commentaires, sinon on s’y perd.

Il ne faut pas avoir peur des noms longs. Si, j’ai un jeu de données, que je filtre plusieurs fois, il est de bon ton de distinguer les différents jeux avec des noms détaillés :

sample = range(10)
squares = [x * x for x in sample]
even_squares = [x for x in squares if x % 2 == 0]
even_squares_tail = even_squares[-3:]

Faire des noms de plus de 10 caractères n’est pas sale. On est pas sur un tableau des scores d’une borne d’arcade des années 80.

J’utilise bien entendu des noms en anglais, ce qui est toujours préférable, mais si vous devez en mettre en fr, évitez à tout prix les accents malgré la possibilité de les utiliser en Python 3.

Conventions

Il existe quelques noms qui sont toujours utilisés de la même façon en Python.

self et cls sont les plus connus, en j’en parle déjà dans le dossier sur la POO.

Il y a args et kwargs, qu’on utilise avec l’opérateur splat, dont je parle ici.

Et puis il y en a de plus discrets.

_ est utilisé pour une variable qu’on ignore. Certaines opérations, comme l’unpacking, supposent la création de plusieurs variables. Si on n’est pas intéressé par l’une d’elles, on peut le signaler. Par exemple, l’ORM django permet d’obtenir un objet, et si il n’existe pas, de le créer. Cette fonction retourne un tuble (objet, bool), l’élément indiquant si l’objet a été créé ou non. Si cette information nous intéresse :

user, created = User.objects.get_or_create(username="sam")

Si cette information ne nous intéresse pas :

user, _ = User.objects.get_or_create(username="sam")

Ainsi le lecteur saura qu’il peut ignorer cette variable quand il parcourt le code.

Il y a aussi les alias. On ne peut pas utiliser certains noms comme list ou id qui sont des fonctions existantes en Python.

On s’arrange généralement en trouvant un synonyme, mais si ce n’est pas pratique, on change une lettre. list devient lst (rappelez vous que list est un nom déjà assez pourri, nommez plutôt le contenu), class devient klass, dict devient dct, etc.

Si on ne peut pas le faire, la convention est de rajouter un underscore à la fin du nom : id devient id_, max devient max_… Mais faites l’effort, avant, de chercher un synonyme. Je vois trop souvent des from_/to alors que certains contextes permettent parfaitement de les nommer start/end ou source/destination.

A ne pas confondre avec l’underscore AVANT le nom, qui est une convention pour dire qu’une variable ne fait pas partie de l’API publique.

Ce sont des béquilles, le choix d’un nom judicieux et clair est toujours préférable, mais ce sont des béquilles utiles.

Savoir quand nommer

Au-delà de donner un bon nom, il y a le fait de choisir quand il faut nommer, et quand il faut éviter de le faire.

Si seul le résultat final d’un traitement m’intéresse, alors, il vaut mieux utiliser une seule variable et mettre le nouveau résultat dedans à chaque fois :

fstab = [line.strip() for line in open('/etc/fstab') if line]
fstab = [line.lower() for line in fstab if not line.startswith('#')]
fstab = [line.split()[:3] for line in fstab]

il faut aussi savoir quand ne pas du tout créer une variable :

row = line.strip().split()
for col in row:
    # do something

ici, la variable intermédiaire est inutile :

for col in line.strip().split():
    # do something

L’inverse est aussi vrai :

for col in [int(x) for x in line.strip().split()]:
    # do something

La ligne devient beaucoup trop complexe, et ajouter une variable intermédiaire avec un bon nom va améliorer la lisibilité du programme :

numeric_col = [int(x) for x in line.strip().split()]
for col in numeric_col:
    # do something

On pourrait croire que je précise le type ici en utilisant “numeric”, mais je n’ai pas utilisé integer ou float. J’ai précisé la nature : des colonnes numériques. Il se trouve que pour des données aussi brutes, la nature se rapproche du type.

Si vous êtes du genre à utiliser des lambdas, cela s’applique aussi à vous.

Pour quelque chose de simple, une lambda inline est très lisible :

sorted(scores.items(), key=lambda score: score[1])

Mais pour quelque chose de complexe, une fonction complète est bien plus adaptée :

def calculate_rank(score):
    return sum(goals for sort, goal in score[1] if sort == 'A')

sorted(scores.items(), key=calculate_rank)

Plutôt qu’un horrible :

sorted(scores.items(), key=lambda x: sum(g for s, g in x[1] if s == 'A'))

Pourquoi je parle de lambda alors qu’on est sur du nommage ? Parce qu’une lambda est anonyme, alors qu’une fonction normale a un nom. Et ce nom exprime l’action de la fonction. Il documente.

Habitudes stylistiques

Ces règles là ne sont pas officielles, mais j’ai pu les constater dans nombre de bons codes.

La nom d’une fonction/méthode est aussi important, sinon plus, que le nom d’une variable. Il n’est pas rare que j’écrive des fonctions avec des noms bien dodus comme :

def get_last_downloaded_shemale_vids()

Si on oublie la docstring, on a déjà une bonne idée de ce que cette fonction fait. Cela n’empêche pas de docstringuer quand même pour annoncer des subtilités sur les types, les potentiels side effects, des choses à savoir sur le temps d’exécution, le format, les perfs, etc.

Mais il arrive souvent qu’une fonction ne fasse pas quelque chose d’aussi concret. Prenez par exemple une fonction dont le but est de sluggifier les strings d’une list.

Si la fonction transforme la liste, on va utiliser un verbe dans le nom :

slugify_items(data)

Si par contre la fonction retourne une liste avec les éléments modifiés, on va utiliser le participe passé :

data = slugified_items(data)

C’est subtil, mais la sémantique est différente. Dans le premier cas, on s’attend à un effet de bord. Dans le second cas, on s’attend à une nouvelle liste, ou comme souvent en Python, à un générateur.

Quand on a affaire à une méthode en Python, on utilise rarement le préfixe get_. Personnellement je l’utilise parfois pour des actions complexes, ou des méthodes de classe.

Mais généralement, on préférera utiliser directement le nom de ce qu’on veut récupérer. Exemple :

comments = blog_post.get_comments(spam=False) # NON
comments = blog_post.comments(spam=False) # Oui

Si on n’a pas besoin de passer de paramètre, alors une property est plus appropriée :

comments = blog_post.comments # Ouiiiiiiiiiiiiiii

J’en profite pour faire remarquer qu’il est très classe de prononcer plusieurs fois très vite “sans paramètre, une propriété est plus appropriée”.

Enfin, il arrive qu’on ait besoin de spécifier des rôles techniques et des interactions entre plusieurs bouts de code : hiérarchie, composition, dépendances, etc. Ce sont les choses les plus compliquées à comprendre quand on lit du code : voir le tableau au complet, ce qui lie les différents blocs.

Il ne faut pas hésiter à nommer ses éléments pour cela. Apprendre le nom des design patterns aide beaucoup, mais même si on n’est pas top moumoute niveau vocabulaire, on peut faire des choses aussi simple que :

class BaseAuthenticator(object):
    #...

class PwdAuthenticator(BaseAuthenticator):
    #...

class KeyAuthenticator(BaseAuthenticator):
    #...

Si vous lisez BaseAuthentificator, vous n’avez pas besoin de voir qu’elle est parente d’autres classes plus bas pour savoir que ce n’est probablement pas une classe instanciable, mais sans doute une classe interface ou une classe abstraite. De quoi se faciliter une lecture en diagonale.

FAIL

Voici quelques exemples de noms qui ratent complètement l’objectif de documentation :

def do_query_database():
    # ...

def query_database():
    # ...
    do_query_database()
    # ...

J’en croise dans le code source de Django, et ça me fait hurler. Sérieux ça veut dire quoi ? Qu’est-ce qui a été extrait ? Dans quel but ? On a plus de question APRÈS avoir lu le nom qu’avant, c’est encore pire qu’un mauvais nom, c’est un nom méchant.

Dans ce cas, il faut essayer d’expliquer au maximum ce que l’appendice – qui va me faire choper une péritonite – que vous avez mis de côté fait :

def excute_and_send_query():
    # ...

def query_database():
    # ...
    excute_and_send_query()
    # ...

Un truc également exaspérant, c’est l’usage d’un vocabulaire ambigu :

def make_best_player_list():
    # ...

On sait ce que ça fait, ça fabrique une liste des meilleurs joueurs. Le contexte nous permet d’évaluer le résultat le plus probable. Maintenant un cas beaucoup moins clair :

def make_query():
    # ...

Ca envoie la requête, ça construit la requête ou les deux ? Make est un mot qui peut vouloir dire fabriquer ou exécuter. Ici il vaut mieux utiliser un vocabulaire plus explicite comme :

def build_sql_query():
    # ...

ou

def send_db_query():
    # ...

Là on sait qui fait quoi. Quitte à faire :

def db_query():
    build_sql_query()
    send_db_query()

Et oui, diviser le travail en plusieurs sous unités bien nommées, puis les regrouper dans un bloc plus général est aussi une forme de documentation. Créer des fonctions n’est pas qu’une question de maintenance ou de perf.

Savoir bien nommer les choses vient avec de l’entraînement. Au début, il faut prendre le temps de le faire. Il faut s’arrêter, et se mettre à la place d’un autre dev qui n’a pas encore eu son café.

Allez, détendez-vous, ce blog est plein de code qui ne suit pas les conseils de cet article. Faites juste au mieux ok ?

]]>
http://sametmax.com/bien-nommer-ses-variables-en-python/feed/ 29 12376