Pointeurs en Python: à quoi ça sert?

Pointeurs en Python: à quoi ça sert?

Si vous avez déjà travaillé avec des langages de niveau inférieur comme C ou C ++, vous avez probablement entendu parler de pointeurs. Les pointeurs vous permettent de créer une grande efficacité dans certaines parties de votre code. Ils provoquent également de la confusion pour les débutants et peuvent entraîner divers bogues de gestion de la mémoire, même pour les experts. Alors, où sont-ils en Python, et comment pouvez-vous simuler des pointeurs en Python?

Les pointeurs sont largement utilisés en C et C ++. Ce sont essentiellement des variables qui contiennent l'adresse mémoire d'une autre variable. Pour un rappel sur les pointeurs, vous pouvez envisager de vérifier ceoverview on C Pointers.

Dans cet article, vous allez mieux comprendre le modèle objet de Python et découvrir pourquoi les pointeurs en Python n'existent pas vraiment. Pour les cas où vous devez imiter le comportement d'un pointeur, vous apprendrez à simuler des pointeurs en Python sans le cauchemar de gestion de la mémoire.

Dans cet article, vous allez:

  • Découvrez pourquoi les pointeurs en Python n'existent pas

  • Explorez la différence entre les variables C et les noms Python

  • Simuler des pointeurs en Python

  • Expérimentez avec de vrais pointeurs en utilisantctypes

Note: Dans cet article, «Python» fera référence à l'implémentation de référence de Python en C, également appelée CPython. Comme l'article traite de certains éléments internes du langage, ces notes sont vraies pour CPython 3.7 mais peuvent ne pas l'être dans les itérations futures ou passées du langage.

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book qui vous montre les meilleures pratiques de Python avec des exemples simples que vous pouvez appliquer instantanément pour écrire du code + Pythonic plus beau.

Pourquoi Python n’a-t-il pas de pointeurs?

La vérité est que je ne sais pas. Les pointeurs en Python pourraient-ils exister nativement? Probablement, mais les pointeurs semblent aller à l'encontre desZen of Python. Les pointeurs encouragent les changements implicites plutôt qu'explicites. Souvent, ils sont complexes au lieu de simples, surtout pour les débutants. Pire encore, ils demandent des moyens de se tirer une balle dans le pied ou de faire quelque chose de vraiment dangereux, comme lire une partie de la mémoire que vous n'étiez pas censé faire.

Python a tendance à essayer d'abstraire les détails d'implémentation comme les adresses mémoire de ses utilisateurs. Python se concentre souvent sur la convivialité plutôt que sur la vitesse. Par conséquent, les pointeurs en Python n'ont pas vraiment de sens. Mais ne vous inquiétez pas, Python vous offre, par défaut, certains des avantages de l'utilisation de pointeurs.

La compréhension des pointeurs en Python nécessite un court détour dans les détails d'implémentation de Python. Plus précisément, vous devrez comprendre:

  1. Objets immuables et objets mutables

  2. Variables / noms Python

Conservez vos adresses mémoire et commençons.

Objets en Python

En Python, tout est un objet. Pour preuve, vous pouvez ouvrir une REPL et explorer en utilisantisinstance():

>>>

>>> isinstance(1, object)
True
>>> isinstance(list(), object)
True
>>> isinstance(True, object)
True
>>> def foo():
...     pass
...
>>> isinstance(foo, object)
True

Ce code vous montre que tout en Python est en effet un objet. Chaque objet contient au moins trois éléments de données:

  • Nombre de références

  • Type

  • Valeur

Lereference count sert à la gestion de la mémoire. Pour un examen approfondi des éléments internes de la gestion de la mémoire en Python, vous pouvez lireMemory Management in Python.

Le type est utilisé au niveau de la couche CPython pour garantir la sécurité du type pendant l'exécution. Enfin, il y a la valeur, qui est la valeur réelle associée à l'objet.

Cependant, tous les objets ne sont pas identiques. Vous devez comprendre une autre distinction importante: les objets immuables et les objets mutables. Comprendre la différence entre les types d'objets aide vraiment à clarifier la première couche de l'oignon qui est des pointeurs en Python.

Objets immuables et objets mutables

En Python, il existe deux types d'objets:

  1. Immutable objects ne peut pas être modifié.

  2. Mutable objects peut être modifié.

Comprendre cette différence est la première clé pour naviguer dans le paysage des pointeurs en Python. Voici une ventilation des types courants et s'ils sont mutables ou immuables:

Type Immuable?

int

Yes

float

Yes

bool

Yes

complex

Yes

tuple

Yes

frozenset

Yes

str

Yes

list

No

set

No

dict

No

Comme vous pouvez le voir, de nombreux types primitifs couramment utilisés sont immuables. Vous pouvez le prouver vous-même en écrivant du Python. Vous aurez besoin de quelques outils de la bibliothèque standard Python:

  1. id() renvoie l'adresse mémoire de l'objet.

  2. is renvoieTrue si et seulement si deux objets ont la même adresse mémoire.

Encore une fois, vous pouvez les utiliser dans un environnement REPL:

>>>

>>> x = 5
>>> id(x)
94529957049376

Dans le code ci-dessus, vous avez attribué la valeur5 àx. Si vous avez essayé de modifier cette valeur avec ajout, vous obtiendrez alors un nouvel objet:

>>>

>>> x += 1
>>> x
6
>>> id(x)
94529957049408

Même si le code ci-dessus semble modifier la valeur dex, vous obtenez un objetnew comme réponse.

Le typestr est également immuable:

>>>

>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
'real_python_rocks'
>>> id(s)
140637819609424

Encore une fois,s se termine par une adresse mémoiredifferent après l'opération+=.

Bonus: L'opérateur+= se traduit par divers appels de méthode.

Pour certains objets commelist,+= se traduira par__iadd__() (ajout sur place). Cela modifieraself et renverra le même ID. Cependant,str etint n’ont pas ces méthodes et aboutissent à des appels à__add__() au lieu de__iadd__().

Pour des informations plus détaillées, consultez la documentation sur les modèles de données Pythonhttps://docs.python.org/3/reference/datamodel.html#object.iadd.]

Essayer de muter directement la chaînes entraîne une erreur:

>>>

>>> s[0] = "R"
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'str' object does not support item assignment

Le code ci-dessus échoue et Python indique questr ne prend pas en charge cette mutation, ce qui est conforme à la définition selon laquelle le typestr est immuable.

Comparez cela avec un objet mutable, commelist:

>>>

>>> 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 montre une différence majeure entre les deux types d'objets. my_list a un identifiant à l'origine. Même après que4 est ajouté à la liste,my_list a l'idsame. C'est parce que le typelist est mutable.

Une autre façon de démontrer que la liste est modifiable est avec affectation:

>>>

>>> my_list[0] = 0
>>> my_list
[0, 2, 3, 4]
>>> id(my_list)
140637819575368

Dans ce code, vous mutatezmy_list et définissez son premier élément sur0. Cependant, il conserve le même identifiant même après cette affectation. Avec les objets mutables et immuables à l'écart, la prochaine étape de votre voyage versPython enlightenment consiste à comprendre l'écosystème variable de Python.

Comprendre les variables

Les variables Python sont fondamentalement différentes des variables en C ou C ++. En fait, Python n'a même pas de variables. Python has names, not variables.

Cela peut sembler pédant, et pour la plupart, il l'est. La plupart du temps, il est parfaitement acceptable de considérer les noms Python comme des variables, mais il est important de comprendre la différence. Cela est particulièrement vrai lorsque vous naviguez sur le sujet délicat des pointeurs en Python.

Pour aider à comprendre la différence, vous pouvez jeter un œil à la façon dont les variables fonctionnent en C, ce qu'elles représentent, puis comparer cela à la façon dont les noms fonctionnent en Python.

Variables en C

Supposons que vous ayez le code suivant qui définit la variablex:

int x = 2337;

Cette ligne de code comporte plusieurs étapes distinctes lors de son exécution:

  1. Allouez suffisamment de mémoire pour un entier

  2. Attribuez la valeur2337 à cet emplacement mémoire

  3. Indiquez quex pointe vers cette valeur

Montré dans une vue simplifiée de la mémoire, il pourrait ressembler à ceci:

In-Memory representation of X (2337)

Ici, vous pouvez voir que la variablex a un faux emplacement mémoire de0x7f1 et la valeur2337. Si, plus tard dans le programme, vous souhaitez modifier la valeur dex, vous pouvez effectuer les opérations suivantes:

x = 2338;

Le code ci-dessus attribue une nouvelle valeur (2338) à la variablex, doncoverwriting la valeur précédente. Cela signifie que la variablex estmutable. La disposition de la mémoire mise à jour affiche la nouvelle valeur:

New In-Memory representation of X (2338)

Notez que l'emplacement dex n'a pas changé, juste la valeur elle-même. C'est un point important. Cela signifie quexis the memory location, pas seulement un nom pour cela.

Une autre façon de penser ce concept est en termes de propriété. Dans un sens,x possède l'emplacement mémoire. x est, dans un premier temps, une boîte vide qui peut contenir exactement un entier dans lequel des valeurs entières peuvent être stockées.

Lorsque vous attribuez une valeur àx, vous placez une valeur dans la case appartenant àx. Si vous souhaitez introduire une nouvelle variable (y), vous pouvez ajouter cette ligne de code:

int y = x;

Ce code crée une boîtenew appeléey et copie la valeur dex dans la boîte. Maintenant, la disposition de la mémoire ressemblera à ceci:

In-Memory representation of X (2338) and Y (2338)

Notez le nouvel emplacement0x7f5 dey. Même si la valeur dex a été copiée dansy, la variabley possède une nouvelle adresse en mémoire. Par conséquent, vous pouvez écraser la valeur dey sans affecterx:

y = 2339;

Maintenant, la disposition de la mémoire ressemblera à ceci:

Updated representation of Y (2339)

Encore une fois, vous avez modifié la valeur ày, maisnotest son emplacement. De plus, vous n'avez pas du tout affecté la variable d'originex. Cela contraste fortement avec le fonctionnement des noms Python.

Noms en Python

Python n'a pas de variables. Il a des noms. Oui, c'est un point pédant, et vous pouvez certainement utiliser autant de termes que vous le souhaitez. Il est important de savoir qu'il existe une 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

Tout comme en C, le code ci-dessus est décomposé en plusieurs étapes distinctes lors de l'exécution:

  1. Créer unPyObject

  2. Définissez le code de type sur un entier pour lesPyObject

  3. Définissez la valeur sur2337 pour lesPyObject

  4. Créez un nom appeléx

  5. Pointezx vers le nouveauPyObject

  6. Augmenter le refcount desPyObject de 1

Note: LePyObject n'est pas le même que leobject de Python. Il 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 appelertypecode ourefcount directement, c'est parce que vous n'avez pas accès directement aux structures. Les appels de méthode commesys.getrefcount() peuvent aider à obtenir des éléments internes.

En mémoire, cela pourrait ressembler à ceci:

Python In-Memory representation of X (2337)

Vous pouvez voir que la disposition de la mémoire est très différente de la disposition C d'avant. Au lieu quex possède le bloc de mémoire où réside la valeur2337, l'objet Python nouvellement créé possède la mémoire où se trouve2337. Le nom Pythonx ne possède pas directement l’adresse mémoire deany de la même manière que la variable Cx possédait un slot statique en mémoire.

Si vous essayez d'attribuer une nouvelle valeur àx, vous pouvez essayer ce qui suit:

>>>

>>> x = 2338

Ce qui se passe ici est différent de l'équivalent C, mais pas trop différent de la liaison d'origine en Python.

Ce code:

  • Crée un nouveauPyObject

  • Définit le code de type sur un entier pour lesPyObject

  • Définit la valeur sur2338 pour lesPyObject

  • Pointsx vers les nouveauxPyObject

  • Augmente le refcount des nouveauxPyObject de 1

  • Diminue le refcount des anciensPyObject de 1

Maintenant en mémoire, cela ressemblerait à ceci:

Python Name Pointing to new object (2338)

Ce diagramme permet d’illustrer quex pointe vers une référence à un objet et ne possède pas l’espace mémoire comme auparavant. Il montre également que la commandex = 2338 n'est pas une affectation, mais plutôt une liaison du nomx à une référence.

De plus, l'objet précédent (qui contenait la valeur2337) est maintenant assis en mémoire avec un nombre de références de 0 et sera nettoyé par lesgarbage collector.

Vous pouvez introduire un nouveau nom,y, dans le mix comme dans l'exemple C:

>>>

>>> y = x

En mémoire, vous auriez un nouveau nom, mais pas nécessairement un nouvel objet:

X and Y Names pointing to 2338

Vous pouvez maintenant voir qu'un nouvel objet Python anot créé, juste un nouveau nom qui pointe vers le même objet. De plus, le décompte de l'objet a augmenté de un. Vous pouvez vérifier l'égalité d'identité des objets pour confirmer qu'ils sont identiques:

>>>

>>> y is x
True

Le code ci-dessus indique quex ety sont le même objet. Ne vous y trompez pas:y est toujours immuable.

Par exemple, vous pouvez effectuer un ajout sury:

>>>

>>> y += 1
>>> y is x
False

Après l'appel d'ajout, vous êtes renvoyé avec un nouvel objet Python. Maintenant, la mémoire ressemble à ceci:

x name and y name different objects

Un nouvel objet a été créé ety pointe maintenant vers le nouvel objet. Fait intéressant, il s'agit du même état final si vous aviez liéy à2339 directement:

>>>

>>> y = 2339

L'instruction ci-dessus entraîne le même état de mémoire de fin que l'addition. Pour récapituler, en Python, vous n'affectez pas de variables. Au lieu de cela, vous liez des noms à des références.

Une note sur les objets internes en Python

Maintenant que vous comprenez comment les objets Python sont créés et les noms liés à ces objets, il est temps de jeter une clé dans la machine. Cette clé porte le nom d'objets internés.

Supposons que vous ayez le code Python suivant:

>>>

>>> x = 1000
>>> y = 1000
>>> x is y
True

Comme ci-dessus,x ety sont tous deux des noms qui pointent vers le même objet Python. Mais l'objet Python qui contient la valeur1000 n'est pas toujours garanti d'avoir la même adresse mémoire. Par exemple, si vous deviez ajouter deux nombres ensemble pour obtenir1000, vous vous retrouveriez avec une adresse mémoire différente:

>>>

>>> x = 1000
>>> y = 499 + 501
>>> x is y
False

Cette fois, la lignex is y renvoieFalse. Si cela prête à confusion, ne vous inquiétez pas. Voici les étapes qui se produisent lorsque ce code est exécuté:

  1. Créer un objet Python (1000)

  2. Attribuez le nomx à cet objet

  3. Créer un objet Python (499)

  4. Créer un objet Python (501)

  5. Ajoutez ces deux objets ensemble

  6. Créer un nouvel objet Python (1000)

  7. Attribuez le nomy à cet objet

Technical Note: Les étapes ci-dessus se produisent uniquement lorsque ce code est exécuté à l'intérieur d'une REPL. Si vous deviez prendre l'exemple ci-dessus, le coller dans un fichier et exécuter le fichier, vous constateriez que la lignex is y renverraitTrue.

Cela se produit car les compilateurs sont intelligents. Le compilateur CPython tente d'effectuer des optimisations appeléespeephole optimizations, qui aident à enregistrer les étapes d'exécution chaque fois que possible. Pour des informations détaillées, vous pouvez consulterCPython’s peephole optimizer source code.

N'est-ce pas un gaspillage? Eh bien, oui, mais c'est le prix à payer pour tous les grands avantages de Python. Vous n'avez jamais à vous soucier de nettoyer ces objets intermédiaires ni même à savoir qu'ils existent! La joie est que ces opérations sont relativement rapides, et vous n'avez jamais eu à connaître aucun de ces détails jusqu'à présent.

Les développeurs principaux de Python, dans leur sagesse, ont également remarqué ce gaspillage et ont décidé de faire quelques optimisations. Ces optimisations entraînent un comportement qui peut surprendre les nouveaux arrivants:

>>>

>>> x = 20
>>> y = 19 + 1
>>> x is y
True

Dans cet exemple, vous voyez presque le même code qu'avant, sauf que cette fois le résultat estTrue. C'est le résultat d'objets internés. Python pré-crée un certain sous-ensemble d'objets en mémoire et les conserve dans l'espace de noms global pour une utilisation quotidienne.

Quels objets dépendent de l'implémentation de Python. CPython 3.7 internes les éléments suivants:

  1. Nombres entiers entre-5 et256

  2. Chaînes contenant uniquement des lettres, des chiffres ou des traits de soulignement ASCII

Le raisonnement derrière cela est que ces variables sont extrêmement susceptibles d'être utilisées dans de nombreux programmes. En internant ces objets, Python empêche les appels d'allocation de mémoire pour les objets utilisés de manière cohérente.

Les chaînes de moins de 20 caractères contenant des lettres ASCII, des chiffres ou des traits de soulignement seront internées. Le raisonnement derrière cela est que ceux-ci sont supposés être une sorte d'identité:

>>>

>>> s1 = "realpython"
>>> id(s1)
140696485006960
>>> s2 = "realpython"
>>> id(s2)
140696485006960
>>> s1 is s2
True

Ici vous pouvez voir ques1 ets2 pointent tous les deux vers la même adresse en mémoire. Si vous deviez introduire une lettre, un chiffre ou un trait de soulignement non ASCII, vous obtiendriez un résultat différent:

>>>

>>> s1 = "Real Python!"
>>> s2 = "Real Python!"
>>> s1 is s2
False

Étant donné que cet exemple contient un point d'exclamation (!), ces chaînes ne sont pas internées et sont des objets différents en mémoire.

Bonus: Si vous voulez vraiment que ces objets référencent le même objet interne, vous pouvez extrairesys.intern(). L'un des cas d'utilisation de cette fonction est décrit dans la documentation:

L'internement de chaînes est utile pour obtenir un peu de performance lors de la recherche dans un dictionnaire: si les clés d'un dictionnaire sont internées et que la clé de recherche est internée, les comparaisons de clés (après hachage) peuvent être effectuées par une comparaison de pointeurs au lieu d'une comparaison de chaînes. (Source)

Les objets internés sont souvent source de confusion. N'oubliez pas, en cas de doute, que vous pouvez toujours utiliserid() etis pour déterminer l'égalité des objets.

Simuler des pointeurs en Python

Ce n'est pas parce que les pointeurs en Python n'existent pas en mode natif que vous ne pouvez pas bénéficier des avantages de l'utilisation des pointeurs. En fait, il existe plusieurs façons de simuler des pointeurs en Python. Vous en apprendrez deux dans cette section:

  1. Utilisation de types mutables comme pointeurs

  2. Utilisation d'objets Python personnalisés

D'accord, allons droit au but.

Utilisation de types mutables comme pointeurs

Vous avez déjà appris les types mutables. Étant donné que ces objets sont mutables, vous pouvez les traiter comme s'ils étaient des pointeurs pour simuler le comportement du pointeur. Supposons que vous vouliez répliquer le code c suivant:

void add_one(int *x) {
    *x += 1;
}

Ce code prend un pointeur vers un entier (*x) puis incrémente la valeur de un. Voici une fonction principale pour exercer le code:

#include 

int main(void) {
    int y = 2337;
    printf("y = %d\n", y);
    add_one(&y);
    printf("y = %d\n", y);
    return 0;
}

Dans le code ci-dessus, vous affectez2337 ày, imprimez la valeur actuelle, incrémentez la valeur de un, puis imprimez la valeur modifiée. Le résultat de l'exécution de ce code serait le suivant:

y = 2337
y = 2338

Une façon de reproduire ce type de comportement en Python consiste à utiliser un type mutable. Pensez à utiliser une liste et à modifier le premier élément:

>>>

>>> def add_one(x):
...     x[0] += 1
...
>>> y = [2337]
>>> add_one(y)
>>> y[0]
2338

Ici,add_one(x) accède au premier élément et incrémente sa valeur de un. L'utilisation d'unlist signifie que le résultat final semble avoir modifié la valeur. Il existe donc des pointeurs en Python? Et bien non. Ceci n'est possible que parce quelist est un type mutable. Si vous essayez d'utiliser untuple, vous obtiendrez une erreur:

>>>

>>> z = (2337,)
>>> add_one(z)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in add_one
TypeError: 'tuple' object does not support item assignment

Le code ci-dessus démontre quetuple est immuable. Par conséquent, il ne prend pas en charge l'attribution d'élément. list n'est pas le seul type mutable. Une autre approche courante pour imiter les pointeurs en Python consiste à utiliser undict.

Supposons que vous disposiez d'une application dans laquelle vous souhaitez suivre chaque fois qu'un événement intéressant se produit. Une façon d'y parvenir serait de créer undict et d'utiliser l'un des é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 dictionnairecounters est utilisé pour garder une trace du nombre d'appels de fonction. Après avoir appeléfoo(), le compteur a augmenté à2 comme prévu. Tout cela parce quedict est mutable.

Gardez à l'esprit qu'il ne s'agit que du comportement du pointeursimulates et qu'il ne correspond pas directement aux vrais pointeurs en C ou C. C'est-à-dire que ces opérations sont plus coûteuses qu'elles ne le seraient en C ou C.

Utilisation d'objets Python

L'optiondict est un excellent moyen d'émuler des pointeurs en Python, mais parfois il devient fastidieux de se souvenir du nom de clé que vous avez utilisé. Cela est particulièrement vrai si vous utilisez le dictionnaire dans différentes parties de votre application. C'est là qu'une classe Python personnalisée peut vraiment aider.

Pour tirer parti du dernier exemple, supposez que vous souhaitez effectuer le suivi des métriques dans votre application. Créer une classe est un excellent moyen d'abstraire les détails embêtants:

class Metrics(object):
    def __init__(self):
        self._metrics = {
            "func_calls": 0,
            "cat_pictures_served": 0,
        }

Ce code définit une classeMetrics. Cette classe utilise toujours undict pour contenir les données réelles, qui se trouvent dans la variable membre_metrics. Cela vous donnera la mutabilité dont vous avez besoin. Il vous suffit maintenant de pouvoir accéder à ces valeurs. Une bonne façon de le faire est avec les propriétés:

class Metrics(object):
    # ...

    @property
    def func_calls(self):
        return self._metrics["func_calls"]

    @property
    def cat_pictures_served(self):
        return self._metrics["cat_pictures_served"]

Ce code utilise@property. Si vous ne connaissez pas les décorateurs, vous pouvez consulter cesPrimer on Python Decorators. Le décorateur@property vous permet ici d'accéder àfunc_calls etcat_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 accéder à ces noms en tant qu'attributs signifie que vous avez abstrait le fait que ces valeurs sont dans undict. Vous expliquez également plus clairement quels sont les noms des attributs. Bien sûr, vous devez pouvoir incrémenter ces valeurs:

class Metrics(object):
    # ...

    def inc_func_calls(self):
        self._metrics["func_calls"] += 1

    def inc_cat_pics(self):
        self._metrics["cat_pictures_served"] += 1

Vous avez introduit deux nouvelles méthodes:

  1. inc_func_calls()

  2. inc_cat_pics()

Ces méthodes modifient les valeurs des métriquesdict. Vous avez maintenant une classe que vous modifiez comme si vous modifiiez un pointeur:

>>>

>>> metrics = Metrics()
>>> metrics.inc_func_calls()
>>> metrics.inc_func_calls()
>>> metrics.func_calls
2

Ici, vous pouvez accéder àfunc_calls et appelerinc_func_calls() à divers endroits de vos applications et simuler des pointeurs en Python. Ceci est utile lorsque vous avez quelque chose comme des mesures qui doivent être utilisées et mises à jour fréquemment dans diverses parties de vos applications.

Note: Dans cette classe en particulier, rendre explicitesinc_func_calls() etinc_cat_pics() au lieu d'utiliser@property.setter empêche les utilisateurs de définir ces valeurs sur unint arbitraire ou une valeur invalide comme undict.

Voici la source complète de la classeMetrics:

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

Pointeurs réels avecctypes

D'accord, il y a peut-être des pointeurs en Python, en particulier CPython. En utilisant le module intégréctypes, vous pouvez créer de vrais pointeurs de style C en Python. Si vous n'êtes pas familier avecctypes, vous pouvez jeter un oeil àExtending Python With C Libraries and the “ctypes” Module.

La vraie raison pour laquelle vous utiliseriez ceci est si vous aviez besoin de faire un appel de fonction à une bibliothèque C qui nécessite un pointeur. Revenons à la fonction Cadd_one() d'avant:

void add_one(int *x) {
    *x += 1;
}

Là encore, ce code incrémente de un la valeur dex. Pour l'utiliser, compilez-le d'abord dans un objet partagé. En supposant que le fichier ci-dessus est stocké dansadd.c, vous pouvez le faire avecgcc:

$ gcc -c -Wall -Werror -fpic add.c
$ gcc -shared -o libadd1.so add.o

La première commande compile le fichier source C en un objet appeléadd.o. La deuxième commande prend ce fichier objet non lié et produit un objet partagé appelélibadd1.so.

libadd1.so doit être dans votre répertoire actuel. Vous pouvez le charger dans Python en utilisantctypes:

>>>

>>> import ctypes
>>> add_lib = ctypes.CDLL("./libadd1.so")
>>> add_lib.add_one
<_FuncPtr object at 0x7f9f3b8852a0>

Le codectypes.CDLL renvoie un objet qui représente l'objet partagélibadd1. Étant donné que vous avez définiadd_one() dans cet objet partagé, vous pouvez y accéder comme s'il s'agissait d'un autre objet Python. Avant d'appeler la fonction, vous devez spécifier la signature de la fonction. Cela permet à Python de s'assurer que vous transmettez le bon type à la fonction.

Dans ce cas, la signature de fonction est un pointeur sur un entier. ctypes vous permettra de le spécifier en utilisant le code suivant:

>>>

>>> add_one = add_lib.add_one
>>> add_one.argtypes = [ctypes.POINTER(ctypes.c_int)]

Dans ce code, vous définissez la signature de la fonction pour correspondre à ce que C attend. Maintenant, si vous essayez d'appeler ce code avec le mauvais type, vous obtiendrez un bel avertissement au lieu d'un comportement non défini:

>>>

>>> add_one(1)
Traceback (most recent call last):
  File "", line 1, in 
ctypes.ArgumentError: argument 1: : \
expected LP_c_int instance instead of int

Python lève une erreur, expliquant queadd_one() veut un pointeur au lieu d'un simple entier. Heureusement,ctypes a un moyen de passer des pointeurs vers ces fonctions. Tout d'abord, déclarez un entier de style C:

>>>

>>> x = ctypes.c_int()
>>> x
c_int(0)

Le code ci-dessus crée un entier de style Cx avec une valeur de0. ctypes fournit lesbyref() pratiques pour permettre de passer une variable par référence.

Note: Le termeby reference s'oppose au passage d'une variableby value.

Lorsque vous passez par référence, vous passez la référence à la variable d'origine, et donc les modifications seront reflétées dans la variable d'origine. Le passage par valeur entraîne une copie de la variable d'origine et les modifications ne sont pas reflétées dans l'original.

Vous pouvez utiliser ceci pour appeleradd_one():

>>>

>>> add_one(ctypes.byref(x))
998793640
>>> x
c_int(1)

Agréable! Votre entier a été incrémenté de un. Félicitations, vous avez utilisé avec succès de vrais pointeurs en Python.

Conclusion

Vous avez maintenant une meilleure compréhension de l'intersection entre les objets Python et les pointeurs. Même si certaines des distinctions entre les noms et les variables semblent pédantes, comprendre fondamentalement ces termes clés élargit votre compréhension de la façon dont Python gère les variables.

Vous avez également appris d'excellentes façons de simuler des pointeurs en Python:

  • Utilisation d'objets mutables comme pointeurs à faible surcharge

  • Création d'objets Python personnalisés pour une facilité d'utilisation

  • Déverrouiller des pointeurs réels avec le modulectypes

Ces méthodes vous permettent de simuler des pointeurs en Python sans sacrifier la sécurité de la mémoire fournie par Python.

Merci d'avoir lu. Si vous avez encore des questions, n'hésitez pas à nous contacter dans la section des commentaires ou sur Twitter.