Si vous avez déjà travaillé avec des langages de bas niveau comme C ou C ++, vous avez probablement entendu parler de pointeurs. Ils vous permettent d'augmenter considérablement l'efficacité de différents morceaux de code. Mais ils peuvent également dérouter les novices - et même les développeurs expérimentés - et conduire à des bugs de gestion de la mémoire. Y a-t-il des pointeurs en Python, puis-je les émuler d'une manière ou d'une autre?
Les pointeurs sont largement utilisés en C et C ++. En fait, ce sont des variables qui contiennent les adresses mémoire auxquelles se trouvent les autres variables. Pour rafraîchir les pointeurs, lisez cette
critique .
Grâce à cet article, vous comprendrez mieux le modèle objet en Python et découvrirez pourquoi les pointeurs n'existent pas réellement dans ce langage. Si vous avez besoin de simuler le comportement de pointeurs, vous apprendrez à les émuler sans le cauchemar de gestion de la mémoire qui l'accompagne.
Avec cet article, vous:
- Découvrez pourquoi Python n'a pas de pointeurs.
- Apprenez la différence entre les variables C et les noms en Python.
- Apprenez à émuler des pointeurs en Python.
- Utilisez des
ctypes
expérimenter avec de vrais pointeurs.
Remarque : Ici, le terme «Python» est appliqué à l'implémentation Python en C, connue sous le nom de CPython. Toutes les discussions sur le périphérique de langage sont valides pour CPython 3.7, mais peuvent ne pas correspondre aux itérations suivantes.
Pourquoi n'y a-t-il pas de pointeurs en Python?
Je ne sais pas. Les pointeurs peuvent-ils exister en Python nativement? Probablement, mais apparemment, les pointeurs contredisent le concept de
Zen of Python , car ils provoquent des changements implicites au lieu de changements explicites. Les pointeurs sont souvent assez complexes, surtout pour les débutants. De plus, ils vous poussent à des décisions infructueuses ou à faire quelque chose de vraiment dangereux, comme lire dans une zone de mémoire, où vous n'auriez pas dû le lire.
Python essaie d'abstraire les détails d'implémentation de l'utilisateur, comme une adresse mémoire. Souvent, dans ce langage, l'accent est mis sur l'utilisabilité et non sur la vitesse. Par conséquent, les pointeurs en Python n'ont pas beaucoup de sens. Mais ne vous inquiétez pas, la langue par défaut vous offre certains des avantages de l'utilisation des pointeurs.
Pour comprendre les pointeurs en Python, passons brièvement en revue les fonctionnalités de l'implémentation du langage. En particulier, vous devez comprendre:
- Quels sont les objets mutables et immuables.
- Comment les variables / noms sont-ils organisés en Python.
Accrochez-vous à vos adresses mémoire, c'est parti!
Objets en Python
Tout en Python est un objet. Par exemple, ouvrez REPL et voyez comment
isinstance()
:
>>> isinstance(1, object) True >>> isinstance(list(), object) True >>> isinstance(True, object) True >>> def foo(): ... pass ... >>> isinstance(foo, object) True
Ce code montre que tout en Python est en fait un objet. Chaque objet contient au moins trois types de données:
- Compteur de référence.
- Tapez
- La valeur.
Un compteur de référence est utilisé pour gérer la mémoire. Les détails sur cette gestion sont écrits dans
Gestion de la mémoire en Python . Le type est utilisé au niveau CPython pour assurer la sécurité du type pendant l'exécution. Et la valeur est la valeur réelle associée à l'objet.
Mais tous les objets ne sont pas identiques. Il y a une différence importante: les objets sont mutables et immuables. Comprendre cette distinction entre les types d'objets vous aidera à mieux comprendre la première couche de l'oignon appelée «pointeurs en Python».
Objets mutables et immuables
Il existe deux types d'objets en Python:
- Objets immuables (ne peuvent pas être modifiés);
- Objets modifiables (sous réserve de modifications).
Reconnaître cette différence est la première clé pour voyager à travers le monde des pointeurs en Python. Voici la caractéristique d'immuabilité de certains types populaires:
Comme vous pouvez le voir, la plupart des types primitifs couramment utilisés sont immuables. Vous pouvez le vérifier en écrivant du code Python. Vous aurez besoin de deux outils de la bibliothèque standard:
id()
renvoie l'adresse mémoire de l'objet;
is
renvoie True
si et seulement si deux objets ont la même adresse mémoire.
Vous pouvez exécuter ce code dans un environnement REPL:
>>> x = 5 >>> id(x) 94529957049376
Ici, nous définissons la variable
x
à
5
. Si vous essayez de modifier la valeur en utilisant l'addition, vous obtiendrez un nouvel objet:
>>> x += 1 >>> x 6 >>> id(x) 94529957049408
Bien qu'il puisse sembler que ce code modifie simplement la valeur de
x
, en réalité, vous obtenez un
nouvel objet comme réponse.
Le type
str
est également immuable:
>>> s = "real_python" >>> id(s) 140637819584048 >>> s += "_rocks" >>> s 'real_python_rocks' >>> id(s) 140637819609424
Et dans ce cas,
s
après l'opération
+=
obtient une adresse mémoire
différente .
Bonus : l'opérateur
+=
traduit par différents appels de méthode.
Pour certains objets, comme une liste,
+=
converti en
__iadd__()
(ajout local). Il se changera et renverra le même ID. Cependant,
str
et
int
n'ont pas ces méthodes et par conséquent,
__add__()
sera appelé à la place de
__iadd__()
.
Voir la
documentation du modèle de données Python
pour plus de détails .Lorsque nous essayons de modifier directement la valeur de chaîne de
s
nous obtenons une erreur:
>>> s[0] = "R"
Trace arrière (les derniers appels sont affichés en dernier):
File "<stdin>", line 1, in <mdule> TypeError: 'str' object does not support item assignment
Le code ci-dessus se bloque et Python signale que
str
ne prend pas en charge cette modification, ce qui correspond à la définition d'immuabilité du type
str
.
Comparez avec un objet mutable, par exemple, avec une liste:
>>> my_list = [1, 2, 3] >>> id(my_list) 140637819575368 >>> my_list.append(4) >>> my_list [1, 2, 3, 4] >>> id(my_list) 140637819575368
Ce code illustre la principale différence entre les deux types d'objets. Initialement,
my_list
a un ID. Même après avoir ajouté
4
à la liste,
my_list
toujours
le même ID. La raison en est que la
list
types est modifiable.
Voici une autre démonstration de la mutabilité des listes à l'aide de l'affectation:
>>> my_list[0] = 0 >>> my_list [0, 2, 3, 4] >>> id(my_list) 140637819575368
Dans ce code, nous avons changé
my_list
et l'
my_list
mis à
0
comme premier élément. Cependant, la liste a conservé le même ID après cette opération. La prochaine étape sur notre chemin vers l'
apprentissage de Python sera d'explorer son écosystème.
Nous traitons des variables
Les variables en Python sont fondamentalement différentes des variables en C et C ++. Essentiellement, ils n'existent tout simplement pas en Python.
Au lieu de variables, il y a des noms .
Cela peut sembler pédant, et pour la plupart c'est le cas. Le plus souvent, vous pouvez prendre des noms en Python comme variables, mais vous devez comprendre la différence. Ceci est particulièrement important lorsque vous étudiez un sujet aussi difficile que les pointeurs.
Pour vous faciliter la tâche, voyons comment fonctionnent les variables en C, ce qu'elles représentent, puis comparons avec le travail des noms en Python.
Variables en C
Prenez le code qui définit la variable
x
:
int x = 2337;
L'exécution de cette ligne courte passe par plusieurs étapes différentes:
- Allouer suffisamment de mémoire pour un nombre.
- Affectation de
2337
à cet emplacement mémoire.
- Le mappage que
x
indique cette valeur.
Une mémoire simplifiée pourrait ressembler à ceci:

Ici, la variable
x
a une fausse adresse de
0x7f1
et une valeur de
2337
. Si vous souhaitez ultérieurement modifier la valeur de
x
, vous pouvez le faire:
x = 2338;
Ce code définit la variable
x
nouvelle valeur de
2338
,
2338
ainsi la valeur
précédente . Cela signifie que la variable
x
mutable . Schéma de mémoire mis à jour pour la nouvelle valeur:

Veuillez noter que l'emplacement de
x
pas changé, seulement la valeur elle-même. C'est important. Cela nous dit que
x
est
un lieu en mémoire , et pas seulement un nom.
Vous pouvez également considérer cette question comme faisant partie du concept de propriété. D'une part,
x
possède une place en mémoire. Tout d'abord,
x
est une boîte vide qui ne peut contenir qu'un seul entier, dans lequel les valeurs entières peuvent être stockées.
Lorsque vous attribuez à
x
une valeur, vous placez la valeur dans une boîte qui appartient à
x
. Si vous souhaitez introduire une nouvelle variable
y
, vous pouvez ajouter cette ligne:
int y = x;
Ce code crée une nouvelle boîte appelée
y
et copie la valeur de
x
dedans. Maintenant, le circuit mémoire ressemble à ceci:

Notez le nouvel emplacement
y
-
0x7f5
. Bien que la valeur
x
été copiée dans
x
, la variable
y
possède une nouvelle adresse en mémoire. Par conséquent, vous pouvez remplacer la valeur de
y
sans affecter
x
:
y = 2339;
Maintenant, le circuit mémoire ressemble à ceci:

Je le répète: vous avez changé la valeur de
y
, mais pas l'emplacement. De plus, vous n'avez pas affecté la variable d'origine
x
.
Avec des noms en Python, la situation est complètement différente.
Noms en Python
Il n'y a pas de variables en Python, des noms à la place. Vous pouvez utiliser le terme «variables» à votre discrétion, mais il est important de connaître la différence entre les variables et les noms.
Prenons le code équivalent de l'exemple C ci-dessus et écrivons-le en Python:
>>> x = 2337
Comme en C, le code passe par plusieurs étapes distinctes lors de l'exécution de ceci:
- PyObject est créé.
- Le numéro de PyObject se voit attribuer un code de type.
2337
reçoit une valeur pour PyObject.
- Le nom
x
est créé. x
pointe vers le nouveau PyObject.- Le nombre de références de PyObject est incrémenté de 1.
Remarque :
PyObject n'est pas identique à un objet en Python, cette entité est spécifique à CPython et représente la structure de base de tous les objets Python.
PyObject est défini comme une structure C, donc si vous vous demandez pourquoi vous ne pouvez pas appeler directement le code de type ou le compteur de référence, la raison en est que vous n'avez pas d'accès direct aux structures.
L'appel de méthodes comme
sys.getrefcount () peut aider à obtenir une sorte de substance interne.
Si nous parlons de mémoire, cela peut ressembler à ceci:

Ici, le circuit de mémoire est très différent du circuit en C montré ci-dessus. Au lieu d'avoir
x
posséder un bloc de mémoire qui stocke la valeur
2337
, un objet Python fraîchement créé possède la mémoire dans laquelle
2337
vit. Le nom Python
x
ne possède directement
aucune adresse en mémoire, tout comme une variable C possède une cellule statique.
Si vous souhaitez attribuer à
x
nouvelle valeur, essayez ce code:
>>> x = 2338
Le comportement du système sera différent de ce qui se passe en C, mais il ne différera pas trop de la liaison d'origine en Python.
Dans ce code:
- Un nouveau PyObject est créé.
- Le numéro de PyObject se voit attribuer un code de type.
2
reçoit une valeur pour PyObject.
x
pointe vers le nouveau PyObject.
- Le compte de référence du nouveau PyObject est incrémenté de 1.
- Le compte de référence de l'ancien PyObject est réduit de 1.
Maintenant, le circuit mémoire ressemble à ceci:

Cette illustration montre que
x
pointe vers une référence à un objet et ne possède pas la zone mémoire comme auparavant. Vous voyez également que la commande
x = 2338
n'est pas une affectation, mais plutôt une liaison du nom
x
au lien.
De plus, l'objet précédent (contenant la valeur
2337
) est maintenant en mémoire avec un décompte de référence de 0 et sera supprimé
par le garbage collector .
Vous pouvez saisir un nouveau nom
y
, comme dans l'exemple C:
>>> y = x
Un nouveau nom apparaîtra en mémoire, mais pas nécessairement un nouvel objet:

Vous voyez maintenant qu'un nouvel objet Python n'a
pas été créé, seul un nouveau nom a
été créé qui pointe vers le même objet. De plus, le compteur de références d'objets a augmenté de 1. Vous pouvez vérifier l'équivalence de l'identité des objets pour confirmer leur identité:
>>> y is x True
Ce code montre que
x
et
y
sont un seul objet. Mais ne vous y trompez pas:
y
est toujours immuable. Par exemple, vous pouvez effectuer une opération d'addition avec
y
:
>>> y += 1 >>> y is x False
Une fois l'ajout appelé, vous retournerez un nouvel objet Python. Maintenant, la mémoire ressemble à ceci:

Un nouvel objet a été créé et
y
pointe maintenant vers lui. Il est curieux que nous obtenions exactement le même état final si nous relions directement
y
à
2339
:
>>> y = 2339
Après cette expression, nous obtenons un tel état final de la mémoire, comme dans l'opération d'addition. Permettez-moi de vous rappeler qu'en Python, vous n'affectez pas de variables, mais liez des noms à des liens.
À propos des stagiaires en Python
Vous comprenez maintenant comment de nouveaux objets sont créés en Python et comment les noms y sont attachés. Il est temps de parler des objets internés.
Nous avons ce code Python:
>>> x = 1000 >>> y = 1000 >>> x is y True
Comme précédemment,
x
et
y
sont des noms pointant vers le même objet Python. Mais cet objet contenant la valeur
1000
ne peut pas toujours avoir la même adresse mémoire. Par exemple, si vous additionnez deux nombres et obtenez 1000, vous obtiendrez une autre adresse:
>>> x = 1000 >>> y = 499 + 501 >>> x is y False
Cette fois, la chaîne
x is y
renvoie
False
. Si vous êtes gêné, ne vous inquiétez pas. Voici ce qui se passe lorsque ce code est exécuté:
- Un objet Python est créé (
1000
).
- On lui donne le nom
x
.
- Un objet Python est créé (
499
).
- Un objet Python est créé (
501
).
- Ces deux objets s'additionnent.
- Un nouvel objet Python est créé (
1000
).
- On lui donne le nom
y
.
Explications techniques : Les étapes décrites n'ont lieu que lorsque ce code est exécuté dans le REPL. Si vous prenez l'exemple ci-dessus, collez-le dans le fichier et exécutez-le, puis la ligne
x is y
retournera
True
.
La raison en est l'esprit rapide du compilateur CPython, qui essaie d'effectuer des
optimisations de judas qui aident à enregistrer autant que possible les étapes d'exécution du code. Les détails peuvent être trouvés dans le
code source de l'optimiseur de judas CPython .
Mais n'est-ce pas du gaspillage? Eh bien, oui, mais vous payez ce prix pour tous les grands avantages de Python. Vous n'avez pas besoin de penser à supprimer de tels objets intermédiaires, et vous n'avez même pas besoin de connaître leur existence! La plaisanterie est que ces opérations sont effectuées assez rapidement et que vous ne les connaissez pas avant ce moment.
Les créateurs de Python ont sagement remarqué cette surcharge et ont décidé de faire plusieurs optimisations. Leur résultat est un comportement qui peut surprendre les débutants:
>>> x = 20 >>> y = 19 + 1 >>> x is y True
Dans cet exemple, le code est presque le même que ci-dessus, sauf que nous obtenons
True
. Tout tourne autour des objets internés. Python pré-crée un sous-ensemble spécifique d'objets en mémoire et les stocke dans l'espace de noms global pour une utilisation quotidienne.
Quels objets dépendent de l'implémentation Python? Dans CPython 3.7, les internés sont:
- Entiers allant de
-5
à 256
.
- Chaînes contenant uniquement des lettres, des chiffres ou des traits de soulignement ASCII.
En effet, ces variables sont très souvent utilisées dans de nombreux programmes. En internant, Python empêche l'allocation de mémoire pour les objets persistants.
Les lignes de moins de 20 caractères et contenant des lettres ASCII, des chiffres ou des traits de soulignement seront internées car elles sont censées être utilisées comme identifiants:
>>> s1 = "realpython" >>> id(s1) 140696485006960 >>> s2 = "realpython" >>> id(s2) 140696485006960 >>> s1 is s2 True
Ici,
s1
et
s2
pointent vers la même adresse en mémoire. Si nous n'insérions pas de lettre, de chiffre ou de soulignement ASCII, nous obtiendrions un résultat différent:
>>> s1 = "Real Python!" >>> s2 = "Real Python!" >>> s1 is s2 False
Cet exemple utilise un point d'exclamation, donc les chaînes ne sont pas internes et sont différents objets en mémoire.
Bonus : si vous voulez que ces objets se réfèrent au même objet interné, vous pouvez utiliser
sys.intern()
. Une façon d'utiliser cette fonctionnalité est décrite dans la documentation:
L'internement de chaînes est utile pour une légère augmentation des performances de recherche de dictionnaire: si les clés du dictionnaire et la clé à rechercher sont internées, des comparaisons de clés (après hachage) peuvent être effectuées en comparant des pointeurs plutôt que des chaînes. ( Source )
Les internés confondent souvent les programmeurs. N'oubliez pas que si vous commencez à douter, vous pouvez toujours utiliser
id()
et
is
pour déterminer l'équivalence des objets.
Émulation de pointeur Python
Le fait que les pointeurs soient absents nativement en Python ne signifie pas que vous ne pouvez pas profiter des pointeurs. Il existe en fait plusieurs façons d'émuler des pointeurs en Python. Ici, nous regardons deux d'entre eux:
- Utilisez-les comme pointeurs vers des types mutables.
- Utilisation d'objets Python spécialement préparés.
Utiliser comme pointeurs de type mutable
Vous savez déjà ce que sont les types mutables. C'est grâce à leur mutabilité que nous pouvons émuler le comportement des pointeurs. Disons que vous devez répliquer ce code:
void add_one(int *x) { *x += 1; }
Ce code prend un pointeur sur un nombre (
*x
) et incrémente la valeur de 1. Voici la fonction principale pour exécuter le code:
Dans le fragment ci-dessus, nous avons attribué
y
à
2337
, affiché la valeur actuelle, augmenté de 1, puis affiché une nouvelle valeur. L'écran suivant apparaît à l'écran:
y = 2337 y = 2338
Une façon de reproduire ce comportement en Python consiste à utiliser un type mutable. Par exemple, appliquez une liste et modifiez le premier élément:
>>> def add_one(x): ... x[0] += 1 ... >>> y = [2337] >>> add_one(y) >>> y[0] 2338
Ici,
add_one(x)
fait référence au premier élément et augmente sa valeur de 1. L'utilisation de la liste signifie qu'en conséquence, nous obtenons la valeur modifiée. Il y a donc des pointeurs en Python? Non. Le comportement décrit est devenu possible car la liste est un type mutable. Si vous essayez d'utiliser un tuple, vous obtenez une erreur:
>>> z = (2337,) >>> add_one(z)
Trace arrière (les derniers appels passent en dernier):
File "<stdin>", line 1, in <module> File "<stdin>", line 2, in add_one TypeError: 'tuple' object does not support item assignment
Ce code démontre l'immuabilité du tuple, il ne prend donc pas en charge l'attribution d'élément.
list
pas le seul type mutable; les pointeurs partiels sont également émulés à l'aide de
dict
.
Supposons que vous ayez une application qui devrait suivre l'occurrence d'événements intéressants. Cela peut être fait en créant un dictionnaire et en utilisant l'un de ses éléments comme compteur:
>>> counters = {"func_calls": 0} >>> def bar(): ... counters["func_calls"] += 1 ... >>> def foo(): ... counters["func_calls"] += 1 ... bar() ... >>> foo() >>> counters["func_calls"] 2
Dans cet exemple, le dictionnaire utilise des compteurs pour suivre le nombre d'appels de fonction. Après avoir appelé
foo()
compteur a augmenté de 2, comme prévu. Et tout cela grâce à la
dict
.
N'oubliez pas, ce n'est qu'une
émulation du comportement d'un pointeur, cela n'a rien à voir avec de vrais pointeurs en C et C ++. On peut dire que ces opérations sont plus chères que si elles étaient effectuées en C ou C ++.
Utilisation d'objets Python
dict
est un excellent moyen d'émuler des pointeurs en Python, mais il est parfois fastidieux de se souvenir du nom de clé que vous avez utilisé. Surtout si vous utilisez le dictionnaire dans différentes parties de l'application. Une classe Python personnalisée peut vous aider ici.
Imaginons que vous ayez besoin de suivre les mesures dans une application. Un excellent moyen d'ignorer les détails ennuyeux est de créer une classe:
class Metrics(object): def __init__(self): self._metrics = { "func_calls": 0, "cat_pictures_served": 0, }
Ce code définit la classe
Metrics
. Il utilise toujours le dictionnaire pour stocker des données à jour qui se trouvent dans la
_metrics
membre
_metrics
. Cela vous donnera la mutabilité requise. Il ne vous reste plus qu'à accéder à ces valeurs. Vous pouvez le faire en utilisant les propriétés:
class Metrics(object):
Ici, nous utilisons
@property . Si vous débutez avec les décorateurs, lisez l'article
Primer on Python Decorators . Dans ce cas, le décorateur
@property
vous permet d'accéder à
func_calls
et
cat_pictures_served
, comme s'il s'agissait d'attributs:
>>> metrics = Metrics() >>> metrics.func_calls 0 >>> metrics.cat_pictures_served 0
Le fait que vous puissiez faire référence à ces noms en tant qu'attributs signifie que vous êtes éloigné du fait que ces valeurs sont stockées dans le dictionnaire. De plus, vous rendez les noms d'attributs plus explicites. Bien sûr, vous devriez pouvoir augmenter les valeurs:
class Metrics(object):
:
inc_func_calls()
inc_cat_pics()
metrics
. , , :
>>> metrics = Metrics() >>> metrics.inc_func_calls() >>> metrics.inc_func_calls() >>> metrics.func_calls 2
func_calls
inc_func_calls()
Python. , -
metrics
, .
: ,
inc_func_calls()
inc_cat_pics()
@property.setter
int
, .
Metrics
:
class Metrics(object): def __init__(self): self._metrics = { "func_calls": 0, "cat_pictures_served": 0, } @property def func_calls(self): return self._metrics["func_calls"] @property def cat_pictures_served(self): return self._metrics["cat_pictures_served"] def inc_func_calls(self): self._metrics["func_calls"] += 1 def inc_cat_pics(self): self._metrics["cat_pictures_served"] += 1
ctypes
, - Python, CPython? ctypes , C. ctypes,
Extending Python With C Libraries and the «ctypes» Module .
, , . -
add_one()
:
void add_one(int *x) { *x += 1; }
,
x
1. , (shared) . ,
add.c
, gcc:
$ gcc -c -Wall -Werror -fpic add.c $ gcc -shared -o libadd1.so add.o
C
add.o
.
libadd1.so
.
libadd1.so
. ctypes Python:
>>> import ctypes >>> add_lib = ctypes.CDLL("./libadd1.so") >>> add_lib.add_one <_FuncPtr object at 0x7f9f3b8852a0>
ctypes.CDLL ,
libadd1
.
add_one()
, , Python-. , . Python , .
, ctypes :
>>> add_one = add_lib.add_one >>> add_one.argtypes = [ctypes.POINTER(ctypes.c_int)]
, C. , , :
>>> add_one(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> ctypes.ArgumentError: argument 1: <class 'TypeError'>: \ expected LP_c_int instance instead of int
Python ,
add_one()
, . , ctypes . :
>>> x = ctypes.c_int() >>> x c_int(0)
x
0
. ctypes
byref()
, .
:
.
, . , .
add_one()
:
>>> add_one(ctypes.byref(x)) 998793640 >>> x c_int(1)
Super! 1. , Python .
Conclusion
Python . , Python.
Python:
Python .