variables – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Si vous ne savez pas ce que contient une variable, vous ne comprenez pas le programme http://sametmax.com/si-vous-ne-savez-pas-ce-que-contient-une-variable-vous-ne-comprenez-pas-le-programme/ http://sametmax.com/si-vous-ne-savez-pas-ce-que-contient-une-variable-vous-ne-comprenez-pas-le-programme/#comments Tue, 12 Feb 2013 13:41:27 +0000 http://sametmax.com/?p=4521 L’immense majorité des questions qu’on me pose sur le fonctionnement d’un programme (ou sur des détails dans les tutos) peut être résolu d’une seule manière.

En sachant ce qu’il y a dans les variables.

Un ordinateur est une machine à états. Les variables représentent cet état. Le seul moyen de comprendre ce que fait un programme, c’est de savoir à quel moment il est dans quel état.

Quand vous ne comprenez pas ce que fait un programme, la première et plus importante question à vous poser et donc :

Que contient cette variable ?

Répondre à la question

La beauté de Python, c’est qu’il vous permet de très facilement répondre à cette question par vous même. Tout ce que vous avez à faire c’est copier le code, le mettre dans un fichier (ou un shell), et le lancer.

Ne restez pas passif devant un tuto. Vous n’en comprendrez que la moitié (au mieux).

Un tuto, une doc, un cours, un snippet ne se lit pas, il se travaille.

Voici tout ce que vous pouvez faire à une variable pour obtenir des informations sur elle :

>>> i = 1
>>> print(i) # afficher la variable
1
>>> print(type(i)) # afficher son type

>>> print(i.__class__) # afficher la classe dont elle est issue

>>> print(i.__class__.__name__) # affiche le nom de sa classe
>>> print(dir(i)) # afficher les méthodes de cet objet
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real']
>>> help(i) # aide sur cet objet
Help on int object:

class int(object)
 |  int(x[, base]) -> integer
 |  
 |  Convert a string or number to an integer, if possible.  A floating point
 |  argument will be truncated towards zero (this does not include a string
 |  representation of a floating point number!)  When converting a string, use
 |  the optional base.  It is an error to supply a base when converting a
 |  non-string.  If base is zero, the proper base is guessed based on the
 |  string content.  If the argument is outside the integer range a
 |  long object will be returned instead.
 |  
 |  Methods defined here:
 |  
 |  __abs__(...)
 |      x.__abs__() <==> abs(x)
...

Ces informations répondent à la question “qu’est-ce que c’est”, “comment c’est configuré” et “qu’est-ce que ça peut faire”.

C’est très important. Vous ne pouvez pas comprendre ce que fait un programme sans ces informations.

En Python une variable peut contenir n’importe quoi

J’ai dis n’importe quoi.

Pas juste les types de base :

>>> i = 'a'
>>> type(i)

>>> i = {}
>>> type(i)

>>> i = []
>>> type(i)

Ni même juste des résultats de fonctions :

>>> i = test()
>>> type(i)

Non. Les variables Python peuvent contenir les fonctions elles-même !

>>> type(test)

>>> i = test
>>> i()
1
>>> type(i)

Et là ça devient important de savoir de quoi on parle :

>>> i.__name__
'test'

Mais les variables peuvent aussi contenir des instances de vos classes :

>>> class Essai(object):
...     pass
... 
>>> i = Essai()
>>> type(i)

Sauf que les variables peuvent aussi contenir les classes elles-même !

>>> type(Essai) # les classes sont de type 'type'. Funky !

>>> i = Essai # ceci n'est PAS une instance. Ce n'est PAS Essai().
>>> type(Essai)

>>> i.__class__

>>> i.__name__
'Essai'

Les fonctions et les classes ne sont pas juste des noms en Python. Ce sont des choses que vous pouvez manipuler. Donc on les retrouve dans des variables.

Donc vous n’avez pas le choix. Vous devez savoir de quoi on parle. Et il faut donc bien comprendre ce qu’est un type, et qu’est-ce qui est de quel type :

>>> type(str) # str est une classe

>>> type(str()) # le résultat de l'appel de str est une string

>>> type(Essai) # Essai est une classe

>>> type(Essai()) # le résultat de l'appel de Essai est un objet Essai

>>> type('1') # '1' est une string

>>> type(1) # 1 est un int

>>> type(True)
 # True est un boolean

Si vous ne comprenez pas les types, vous ne pouvez PAS comprendre un programme.

Ce n’est pas honteux. Arrêtez d’essayer un truc impossible, c’est tout. Revenez en arrière. Allez apprendre ce qu’est un type x ou z.

Puis retournez essayer de comprendre le programme.

Il n’y a PAS d’alternative.

Quand vous lisez un programme, vous devez savoir ce qu’il y a dans une variable :

  • quel est son type ?
  • quelle est sa valeur ?
  • qu’est-ce que je peux faire avec (méthodes, attributs, etc) ?

Pdb à la rescousse

Donc quand vous lisez un tuto, vous allez copier / coller du code. Vous allez le lancer. Si vous ne le faites pas, vous ne comprendrez pas.

Maintenant, dans un premier temps vous allez utiliser des print.

print type(truc)
print truc.__class__
print dir(truc)

Et pour les fonctions et les classes :

print truc.__name__

Mais on peut faire mieux. Utilisez le debugger de Python intégré : PDB. J’ai écrit un bon article sur la question, mais pour résumer, quand vous voulez avoir des informations sur une variable dans un programme, juste avant la variable faites :

import ipdb; ipdb.set_trace()

Puis lancez le programme. Un shell va s’ouvrir, bloquant le programme à cet endroit. Et vous pourrez manipuler la variable à ce moment du programme.

Pour quitter, entrez q.

]]>
http://sametmax.com/si-vous-ne-savez-pas-ce-que-contient-une-variable-vous-ne-comprenez-pas-le-programme/feed/ 11 4521
Valeurs et références en Python http://sametmax.com/valeurs-et-references-en-python/ http://sametmax.com/valeurs-et-references-en-python/#comments Mon, 12 Nov 2012 19:28:28 +0000 http://sametmax.com/?p=2869 Petit article en complément de l’article de Réchèr.

Il y a plusieurs manières de passer une variable en informatique: par valeur ou par référence. Et dans les langages bas niveau comme le C, on se pose la question: “passe-t-on la valeur ? un pointer ? un pointer vers un pointer ?”

En Python ce n’est pas la question puisque tout se passe par référence. Tous les objets. Dans tous les cas.

La question est donc plutôt: “ça veut dire quoi passer par référence ?”

Assignation et référence

Une référence est comme une adresse qui dit où la donnée se trouve en mémoire.

Quand vous faites ceci:

a = [1, 2, 3]

Vous n’assignez pas la liste à la variable “a”, vous assignez une référence vers la liste, donc une sorte d’adresse qui indique où elle se trouve en mémoire. (en vérité vous n’assignez rien, les variables sont des étiquettes en Python, mais on va ignorer ce détail ici).

Et quand vous faites:

print a

Python va chercher dans a la référence, et retrouver la liste en suivant “l’adresse”.

C’est important car ça veut dire deux choses.

1. Quand vous faites ça:

b = a

Vous ne copiez pas la liste. Vous copiez la référence. Du coup, on ne prend pas deux fois la place en mémoire, et la copie est très rapide

2. Quand vous faites ça:

b = a

Vous ne copiez pas la liste. Vous copiez la référence. Bis.

Et si vous faites:

b.append(4)

Alors:

print a

Va donner…

[1, 2, 3, 4]

Car en faisant append() sur b, Python va trouver une référence, retrouver la liste derrière la référence, et faire un append() sur la liste. Comme c’est la même réference dans a et b (puisqu’on l’a copié), c’est la même liste derrière. Tout ce qu’on applique à a, s’applique donc à b, et vice-versa.

Si vous voulez faire une vraie copie, alors il faut recréer une toute nouvelle liste. Par exemple:

b = list(a)

Mutable et non mutable

L’assignation par référence n’a vraiment d’importance que dans le cas où un objet est mutable. En Python, il existe en effet deux types d’objets: les mutables (listes, dictionnaires, sets, objets custo, etc) et les non mutables (strings, int, floats, tuples, etc).

Les mutables sont ceux qu’on peut modifier après leur création. Les non mutables sont ceux qu’on ne peut pas modifier après création.

On ne peut pas modifier 152 une fois que l’objet int est créé. Mais on peut rajouter des éléments à une liste après qu’elle soit créée. Les ints sont non mutables. Les lists sont mutables.

Cela peut surprendre, mais les strings sont non mutables. Même quand vous faites:

>>> pa = "papa"
>>> pi = pa.replace("a", "i")
>>> print pa
papa
>>> print pi
pipi

Vous ne modifiez pas la chaîne originale: vous créez une copie de la chaîne, et la chaîne de départ n’a pas bougé.

C’est important car si on fait une référence vers un type non mutable, on s’en oint le pourtour anal avec pelle à tarte: il ne peut pas être modifié. Peu importe que la référence soit copiée à droite et à gauche. Mais si l’objet est mutable, chaque copie de la référence ajoute un endroit dans le programme duquel on peut modifier l’objet.

Cela a des implications parfois assez coquines.

Ainsi:

>>> l = [0] * 3
>>> l # une liste de 3 zéros
[0, 0, 0]
>>> l[1] += 1
>>> l
[0, 1, 0]

Ici tout se comporte comme prévu. Les ints sont non mutables, donc on ne s’aperçoit pas d’un détail important: [0] * 3 copie la référence à 0 trois fois. Quand on fait +=, ça remplace l’ancien int par un nouveau, donc un seul item de la liste est changé.

Mais si on fait vicieusement:

>>> l = [[0]] * 3
>>> l # une liste de listes d'un seul zéro chacune
[[0], [0], [0]]
>>> l[1][0] += 1
>>> l
[[1], [1], [1]]

Ici on a copié 3 fois la référence vers la même liste. Du coup une modification affecte les 3 items. Doh.

Rappels: les tuples ne sont pas mutables. Et on peut passer d’un tuple à une liste avec tuple(l) et list(t). Pensez-y si vous rencontrez ce genre de problème.

Passage des arguments de fonction par référence

Quand vous faites cela:

def encore_une_fonction_d_exemple_inutile(l):
    l.append(4)
    return l

>>> l1 = [1, 2, 3]
>>> encore_une_fonction_d_exemple_inutile(l1)
>>> print l1
[1, 2, 3, 4]

Vous noterez que la liste a été modifiée. C’est parce que l’on passe une référence à la liste quand on la passe en argument. Toute modification de la liste dans la fonction est donc visible en dehors de la fonction. Si le paramètre était immutable, encore une fois on s’en ficherait. Mais là, comme liste est mutable, notre fonction possède ce qu’on nomme un effet de bord: quand on l’appelle, elle a des conséquences sur des objets qui existent en dehors d’elle même.

Il est généralement de bon ton d’éviter les effets de bord, aussi, essayez toujours de travailler sur des copies: utilisez le slicing, les listes en intention, les générateurs, etc., pour retourner les nouvelles valeurs plutôt que de modifier l’objet original.

Ici:

def encore_une_fonction_d_exemple_inutile(l):
    return l1 + [4]

Nous permet de retourner une nouvelle liste. Si la liste prend beaucoup de place en mémoire, on peut faire ça:

def encore_une_fonction_d_exemple_inutile(l):

    for x in l:
        yield x
    yield 4

Ce qui aurait pour effet de retourner un itérable similaire, sans prendre plus de mémoire.

Valeurs par défaut et référence

Quand on utilise une valeur par défaut, par exemple dans la déclaration des paramètres d’une fonction, on initialise une référence qui va rester la même pour toute la durée du programme.

def encore_une_fonction_d_exemple_inutile(l=[1, 2, 3]):
    l.append(4)
    return l

>>> encore_une_fonction_d_exemple_inutile()
>>> encore_une_fonction_d_exemple_inutile()
>>> print l1
[1, 2, 3, 4, 4]

On constate ici que 4 a été ajouté deux fois dans la liste. En effet, l est l’argument par défaut, et ici, il est initialisé à la référence pointant sur une liste [1, 2, 3]. Pas sur la valeur [1, 2, 3]. [1, 2, 3] est stocké quelque part dans un monde invisible que seul Guido Van Rossum connait, et l contient juste la référence à cette liste.

Cette référence est gardée, et à chaque appel, c’est la même liste qui est utilisée. Si on appelle deux fois la fonction, la première fois c’est la liste [1, 2, 3, 4] à laquelle on ajoute 4. Puis comme c’est la même référence, on ajoute ensuite 4 à la même liste, à laquelle on avait déjà ajouté 4.

Bref, évitez les mutables dans les paramètres par défaut à moins de savoir ce que vous faites (cache ou memoization).

Variables de classe et références

La même problématique existe pour les classes. Si vous faites:

class AuChocolat(objet):

    supplements = ['chantilly', 'praline']

>>> g1 = AuChocolat()
>>> g2 = AuChocolat()
>>> g1.supplements.pop()
'praline'
>>> g2.supplements
['chantilly']

Même principe: toute variable de classe pointe pour toute la durée du programme sur la même réference. La référence va être partagée entre toutes les instances de la classe. C’est donc la même liste !

Maintient d’une référence

Python garde un objet en mémoire tant qu’il existe une référence vers cet objet.

>>> a = 1
>>> b = a
>>> del a
>>> print b
1

Si je supprime a, il reste b, donc l’objet est toujours en mémoire. Si je supprime b également, Python va lancer le processus de nettoyage pour effacer l’objet de la mémoire.

Une référence existe toujours dans le cadre d’un scope. Par exemple, si une référence est copiée dans une fonction, quand la fonction se termine, la copie de la référence est supprimée, et ne compte plus comme une voix pour le maintient de l’objet en mémoire.

En Python, il n’existe donc aucun moyen de supprimer un objet. On peut juste supprimer toutes ses références, et attendre que Python s’aperçoive que tout le monde s’en branle à présent de l’objet, et le supprime.

Dans certains cas particuliers, on veut qu’une référence à un objet ne compte pas comme une voix. On peut le faire en utilisant le module weakref, mais cela ne marche qu’avec les classes que l’on code soit-même.

class ALaVanille(object):
    pass

>>> import weakref
>>> g1 = ALaVanille()
>>> g2 = weakref.proxy(g1)
>>> g2

>>> g1.foo = "bar"
>>> g2.foo
'bar'
>>> del g1
>>> g2

Dès qu’on supprime g1, g2 proxy vers None, car la référence vers l’instance de ALaVanille ne compte pas pour garder en vie l’objet.

]]>
http://sametmax.com/valeurs-et-references-en-python/feed/ 23 2869