Python comme cas ultime de C ++. Partie 2/2

À suivre. Commencer en Python comme cas ultime de C ++. Partie 1/2 ".


Variables et types de données


Maintenant que nous avons enfin compris les mathématiques, décidons quelles variables doivent signifier dans notre langue.


En C ++, un programmeur a le choix: utiliser des variables automatiques placées sur la pile ou conserver des valeurs dans la mémoire de données du programme, en ne plaçant que des pointeurs vers ces valeurs sur la pile. Et si nous choisissons une seule de ces options pour Python?


Bien sûr, nous ne pouvons pas toujours utiliser uniquement les valeurs des variables, car les grandes structures de données ne tiendront pas sur la pile, ou leur mouvement constant sur la pile créera des problèmes de performances. Par conséquent, nous n'utiliserons que des pointeurs en Python. Cela simplifiera conceptuellement la langue.


Donc l'expression


a = 3 

signifiera que nous avons créé un objet «3» dans la mémoire de données du programme (le soi-disant «tas») et fait du nom «a» une référence à lui. Et l'expression


 b = a 

dans ce cas, cela signifie que nous avons forcé la variable «b» à faire référence au même objet en mémoire auquel «a» fait référence, en d'autres termes, nous avons copié le pointeur.


Si tout est un pointeur, combien de types de liste devons-nous implémenter dans notre langage? Bien sûr, un seul est une liste de pointeurs! Vous pouvez l'utiliser pour stocker des entiers, des chaînes, d'autres listes, peu importe - après tout, ce sont des pointeurs.


Combien de types de tables de hachage devons-nous implémenter? (En Python, ce type est appelé "dictionnaire" - dict .) Un! Laissez-le associer des pointeurs aux clés avec des pointeurs aux valeurs.


Ainsi, nous n'avons pas besoin d'implémenter dans notre langage une grande partie de la spécification C ++ - les modèles, car nous effectuons toutes les opérations sur les objets, et les objets sont toujours accessibles par pointeur. Bien sûr, les programmes écrits en Python n'ont pas à se limiter à travailler avec des pointeurs: il existe des bibliothèques comme NumPy qui aident les scientifiques à travailler avec des tableaux de données en mémoire, comme ils le feraient avec Fortran. Mais la base du langage - des expressions comme «a = 3» - fonctionne toujours avec des pointeurs.


Le concept de «tout est un pointeur» simplifie également la composition des types à la limite. Vous voulez une liste de dictionnaires? Il suffit de créer une liste et d'y mettre des dictionnaires! Vous n'avez pas besoin de demander la permission à Python, vous n'avez pas besoin de déclarer des types supplémentaires, tout fonctionne hors de la boîte.


Mais que faire si nous voulons utiliser des objets composés comme clés? La clé du dictionnaire doit avoir une valeur immuable, sinon comment rechercher des valeurs par elle? Les listes sont sujettes à modification, elles ne peuvent donc pas être utilisées à ce titre. Pour de telles situations, Python a un type de données qui, comme une liste, est une séquence d'objets, mais, contrairement à une liste, cette séquence ne change pas. Ce type est appelé tuple ou tuple (prononcé «tuple» ou «tuple»).


Les tuples en Python résolvent un problème de langage de script de longue date. Si vous n'êtes pas impressionné par cette fonctionnalité, vous n'avez probablement jamais essayé d'utiliser des langages de script pour un travail sérieux avec les données, dans lequel vous ne pouvez utiliser que des chaînes ou uniquement des types primitifs comme clé dans les tables de hachage.


Une autre possibilité que les tuples nous donnent est de renvoyer plusieurs valeurs d'une fonction sans avoir à déclarer des types de données supplémentaires pour cela, comme vous devez le faire en C et C ++. De plus, pour faciliter l'utilisation de cette fonctionnalité, l'opérateur d'affectation était doté de la possibilité de décompresser automatiquement les tuples dans des variables distinctes.


 def get_address(): ... return host, port host, port = get_address() 

Le déballage a plusieurs effets secondaires utiles, par exemple, l'échange de valeurs variables peut être écrit comme suit:


 x, y = y, x 

Tout est un pointeur, ce qui signifie que les fonctions et les types de données peuvent être utilisés comme données. Si vous connaissez le livre «Design Patterns» des auteurs de «The Gang of Four», vous devez vous souvenir des méthodes complexes et déroutantes qu'il propose afin de paramétrer le choix du type d'objet créé par votre programme au moment de l'exécution. En effet, dans de nombreux langages de programmation, c'est difficile à faire! En Python, toutes ces difficultés disparaissent, car nous savons qu'une fonction peut renvoyer un type de données, que les fonctions et les types de données ne sont que des liens et que les liens peuvent être stockés, par exemple, dans des dictionnaires. Cela simplifie la tâche à la limite.


David Wheeler a déclaré: "Tous les problèmes de programmation sont résolus en créant un niveau supplémentaire d'indirection." L'utilisation de liens en Python est le niveau d'indirection qui a été traditionnellement utilisé pour résoudre de nombreux problèmes dans de nombreux langages, y compris C ++. Mais s'il est utilisé explicitement là-bas, ce qui entraîne une complication des programmes, alors en Python, il est utilisé implicitement, uniformément en ce qui concerne les données de tous types, et est convivial.


Mais si tout est un lien, à quoi se réfèrent ces liens? Les langages comme C ++ ont plusieurs types. Laissons en Python un seul type de données - un objet! Les spécialistes dans le domaine de la théorie des types secouent la tête de manière désapprobatrice, mais je pense qu'un seul type de données source, dont dérivent tous les autres types dans la langue, est une bonne idée qui garantit l'uniformité de la langue et sa facilité d'utilisation.


Pour des contenus de mémoire spécifiques, diverses implémentations Python (PyPy, Jython ou MicroPython) peuvent gérer la mémoire de différentes manières. Mais afin de mieux comprendre comment la simplicité et l'uniformité de Python sont implémentées, pour former le modèle mental correct, il est préférable de se tourner vers l'implémentation de référence Python en C appelée CPython, que nous pouvons télécharger sur python.org .


 struct { struct _typeobject *ob_type; /* followed by object's data */ } 

Ce que nous verrons dans le code source CPython est une structure qui se compose d'un pointeur vers des informations sur le type d'une variable donnée et une charge utile qui définit la valeur spécifique de la variable.


Comment fonctionnent les informations de type? Examinons à nouveau le code source de CPython.


 struct _typeobject { /* ... */ getattrfunc tp_getattr; setattrfunc tp_setattr; /* ... */ newfunc tp_new; freefunc tp_free; /* ... */ binaryfunc nb_add; binaryfunc nb_subtract; /* ... */ richcmpfunc tp_richcompare; /* ... */ } 

Nous voyons des pointeurs vers des fonctions qui fournissent toutes les opérations possibles pour un type donné: addition, soustraction, comparaison, accès aux attributs, indexation, découpage, etc. Ces opérations savent travailler avec la charge utile qui se trouve en mémoire sous un pointeur pour taper des informations, que ce soit un entier, une chaîne ou un objet d'un type créé par l'utilisateur.


Ceci est radicalement différent de C et C ++, dans lequel les informations de type sont associées aux noms et non aux valeurs des variables. En Python, tous les noms sont associés à des liens. La valeur par référence, quant à elle, est de type. C'est l'essence même des langages dynamiques.


Pour réaliser toutes les fonctionnalités du langage, il nous suffit de définir deux opérations sur les liens. L'une des plus évidentes est la copie. Lorsque nous attribuons une valeur à une variable, un emplacement dans un dictionnaire ou un attribut d'un objet, nous copions les liens. Il s'agit d'une opération simple, rapide et totalement sûre: la copie de liens ne modifie pas le contenu de l'objet.


La deuxième opération est un appel de fonction ou de méthode. Comme nous l'avons montré ci-dessus, un programme Python ne peut interagir avec la mémoire qu'à travers des méthodes implémentées dans des objets intégrés. Par conséquent, il ne peut pas provoquer une erreur liée à un accès à la mémoire.


Vous pouvez avoir une question: si toutes les variables contiennent des références, comment puis-je protéger la valeur d'une variable contre les changements en la transmettant à la fonction en tant que paramètre?


 n = 3 some_function(n) # Q: I just passed a pointer! # Could some_function() have changed “3”? 

La réponse est que les types simples en Python sont immuables: ils n'implémentent tout simplement pas la méthode responsable de la modification de leur valeur. L'immuable (immuable) int , float , tuple ou str fournit dans des langages comme "tout est un pointeur" le même effet sémantique que les variables automatiques fournissent en C.


Les types et les méthodes unifiés simplifient autant que possible l'utilisation de la programmation généralisée ou des génériques. Les fonctions min() , max() , sum() et similaires sont intégrées, il n'est pas nécessaire de les importer. Et ils fonctionnent avec tous les types de données dans lesquels des opérations de comparaison pour min() et max() sont implémentées, des ajouts pour sum() , etc.


Créer des objets


Nous avons découvert en termes généraux comment les objets devaient se comporter. Nous allons maintenant déterminer comment nous allons les créer. C'est une question de syntaxe de langage. C ++ prend en charge au moins trois façons de créer un objet:


  1. Automatique, en déclarant une variable de cette classe:
     my_class c(arg); 
  2. Utilisation du new opérateur:
     my_class *c = new my_class(arg); 
  3. Factory, en appelant une fonction arbitraire qui retourne un pointeur:
     my_class *c = my_factory(arg); 

Comme vous l'avez probablement déjà deviné, après avoir étudié la façon de penser des créateurs de Python dans les exemples ci-dessus, nous devons maintenant en choisir un.


Du même livre, The Gangs of Four, nous avons appris qu'une usine est le moyen le plus flexible et universel de créer des objets. Par conséquent, seule cette méthode est implémentée en Python.


En plus de l'universalité, cette méthode est bonne en ce que vous n'avez pas besoin de surcharger le langage avec une syntaxe inutile pour le garantir: un appel de fonction est déjà implémenté dans notre langage, et une fabrique n'est rien de plus qu'une fonction.


Une autre règle pour créer des objets en Python est la suivante: tout type de données est sa propre fabrique. Bien sûr, vous pouvez écrire n'importe quel nombre d'usines personnalisées supplémentaires (qui seront des fonctions ou des méthodes ordinaires, bien sûr), mais la règle générale restera valide:


 # Let's make type objects # their own type's factories! c = MyClass() i = int('7') f = float(length) s = str(bytes) 

Tous les types sont appelés objets et tous renvoient des valeurs de leur type, déterminées par les arguments passés dans l'appel.


Ainsi, en utilisant uniquement la syntaxe de base du langage, toutes les manipulations lors de la création d'objets, telles que les modèles «Arena» ou «Adaptive», peuvent être encapsulées, car une autre excellente idée empruntée au C ++ est que le type lui-même détermine comment cela se passe. engendrer ses objets, comment le new opérateur travaille pour lui.


Et NULL?


La gestion d'un pointeur nul ajoute de la complexité au programme, nous interdisons donc NULL. La syntaxe Python rend impossible la création d'un pointeur nul. Deux opérations élémentaires sur les pointeurs, dont nous avons parlé plus haut, sont définies de telle manière que toute variable pointe vers un objet.


En conséquence, l'utilisateur ne peut pas utiliser Python pour créer une erreur liée à un accès à la mémoire, telle qu'une erreur de segmentation ou hors des limites de la mémoire tampon. En d'autres termes, les programmes Python ne sont pas affectés par les deux types de vulnérabilités les plus dangereux qui menacent la sécurité d'Internet au cours des 20 dernières années.


Vous pouvez demander: «Si la structure des opérations sur les objets est inchangée, comme nous l'avons vu précédemment, alors comment les utilisateurs créeront-ils leurs propres classes, avec des méthodes et des attributs non répertoriés dans cette structure?»


La magie réside dans le fait que pour les classes personnalisées Python a une "préparation" très simple avec un petit nombre de méthodes implémentées. Voici les plus importants:


 struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; /* ... */ newfunc tp_new; /* ... */ } 

tp_new() crée une table de hachage pour la classe utilisateur, la même que pour le type dict . tp_getattr() extrait quelque chose de cette table de hachage, et tp_setattr() , au contraire, y met quelque chose. Ainsi, la capacité des classes arbitraires à stocker des méthodes et des attributs n'est pas fournie au niveau des structures du langage C, mais à un niveau supérieur - une table de hachage. (Bien sûr, à l'exception de certains cas liés à l'optimisation des performances.)


Modificateurs d'accès


Que faisons-nous de toutes ces règles et concepts qui sont construits autour de mots-clés C ++ private et protected ? Python, étant un langage de script, n'en a pas besoin. Nous avons déjà des parties «protégées» du langage - ce sont des données de types intégrés. En aucun cas, Python ne permettra à un programme, par exemple, de manipuler les bits d'un nombre à virgule flottante! Ce niveau d'encapsulation est suffisant pour maintenir l'intégrité de la langue elle-même. Nous, les créateurs de Python, pensons que l'intégrité du langage est le seul bon prétexte pour cacher des informations. Toutes les autres structures et données de programme utilisateur sont considérées comme publiques.


Vous pouvez écrire un trait de soulignement ( _ ) au début d'un nom d'attribut de classe pour avertir un collègue: vous ne devez pas vous fier à cet attribut. Mais le reste de Python a appris les leçons du début des années 90: alors beaucoup pensaient que la principale raison pour laquelle nous écrivons des programmes gonflés, illisibles et bogués est le manque de variables privées. Je pense que les 20 prochaines années ont convaincu tout le monde dans l'industrie de la programmation: les variables privées ne sont pas les seules, et loin d'être le remède le plus efficace pour les programmes gonflés et bogués. Par conséquent, les créateurs de Python ont décidé de ne même pas se soucier des variables privées et, comme vous pouvez le voir, elles n'ont pas échoué.


Gestion de la mémoire


Qu'advient-il de nos objets, nombres et chaînes à un niveau inférieur? Comment sont-ils stockés en mémoire exactement, comment CPython leur fournit-il un accès partagé, quand et dans quelles conditions sont-ils détruits?


Et dans ce cas, nous avons choisi la façon la plus générale, prévisible et productive de travailler avec la mémoire: du côté du programme C, tous nos objets sont des pointeurs partagés .


Compte tenu de ces connaissances, les structures de données que nous avons examinées précédemment dans la section «Variables et types de données» devraient être complétées comme suit:


 struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; /* followed by object's data */ } } 

Ainsi, chaque objet en Python (nous entendons l'implémentation de CPython, bien sûr) a son propre compteur de référence. Une fois qu'il devient nul, l'objet peut être supprimé.


Le mécanisme de comptage de liens ne repose pas sur des calculs ou des processus d'arrière-plan supplémentaires - un objet peut être détruit instantanément. De plus, il fournit une localisation élevée des données: souvent, la mémoire recommence à être utilisée immédiatement après avoir été libérée. L'objet qui vient d'être détruit a probablement été utilisé récemment, ce qui signifie qu'il se trouvait dans le cache du processeur. Par conséquent, l'objet nouvellement créé restera dans le cache. Ces deux facteurs - simplicité et localité - font du comptage de liens un moyen très productif de collecte des ordures.


(En raison du fait que les objets dans les programmes réels se réfèrent souvent les uns aux autres, le compteur de référence ne peut pas, dans certains cas, tomber à zéro même lorsque les objets ne sont plus utilisés dans le programme. Par conséquent, CPython dispose également d'un deuxième mécanisme de récupération de place - un arrière-plan, basé sur sur des générations d'objets - environ transl. )


Erreurs de développeur Python


Nous avons essayé de développer un langage qui serait assez simple pour les débutants, mais aussi assez attractif pour les professionnels. Dans le même temps, nous n'avons pas pu éviter les erreurs de compréhension et d'utilisation des outils que nous avons nous-mêmes créés.


Python 2, en raison de l'inertie de la pensée associée aux langages de script, a essayé de convertir les types de chaînes, comme le ferait un langage avec un typage faible. Si vous essayez de combiner une chaîne d'octets avec une chaîne en Unicode, l'interpréteur convertit implicitement la chaîne d'octets en Unicode à l'aide de la table de code disponible sur le système et présente le résultat en Unicode:


 >>> 'byte string ' + u'unicode string' u'byte string unicode string' 

En conséquence, certains sites Web ont très bien fonctionné lorsque leurs utilisateurs utilisaient l'anglais, mais ils ont produit des erreurs cryptiques lors de l'utilisation de caractères d'autres alphabets.


Ce bug de conception de langage a été corrigé dans Python 3:


 >>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str 

Une erreur similaire dans Python 2 était liée au tri «naïf» des listes constituées d'éléments incomparables:


 >>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b'] 

Dans ce cas, Python 3 indique clairement à l'utilisateur qu'il essaie de faire quelque chose de peu significatif:


 >>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str() 

Abus


Les utilisateurs abusent parfois de la nature dynamique du langage Python, puis, dans les années 90, lorsque les meilleures pratiques n'étaient pas encore largement connues, cela arrivait particulièrement souvent:


 class Address(object): def __init__(self, host, port): self.host = host self.port = port 

"Mais ce n'est pas optimal!" - Certains ont dit: - «Et si le port ne diffère pas de la valeur par défaut? Quoi qu'il en soit, nous dépensons un attribut de classe entier sur son stockage! » Et le résultat est quelque chose comme


 class Address(object): def __init__(self, host, port=None): self.host = host if port is not None: # so terrible self.port = port 

Ainsi, des objets du même type apparaissent dans le programme, qui ne peuvent cependant pas être exploités de manière uniforme, car certains ont un certain attribut, d'autres pas! Et nous ne pouvons pas toucher cet attribut sans vérifier sa présence à l'avance:


 # code was forced to use introspection # (terrible!) if hasattr(addr, 'port'): print(addr.port) 

Actuellement, l'abondance de hasattr() , isinstance() et d'autres introspections est un signe certain de mauvais code, et il est considéré comme la meilleure pratique de rendre les attributs toujours présents dans l'objet. Cela fournit une syntaxe plus simple lors de l'accès:


 # today's best practice: # every atribute always present if addr.port is not None: print(addr.port) 

Ainsi, les premières expériences avec des attributs ajoutés et supprimés dynamiquement se sont terminées, et maintenant nous regardons les classes en Python de la même manière qu'en C ++.


Une autre mauvaise habitude du début de Python était l'utilisation de fonctions dans lesquelles un argument peut avoir des types complètement différents. Par exemple, vous pourriez penser qu'il pourrait être trop difficile pour l'utilisateur de créer une liste de noms de colonnes à chaque fois, et vous devriez lui permettre de les passer également sur une seule ligne, où les noms des colonnes individuelles sont séparés, disons, par une virgule:


 class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns 

Mais cette approche peut donner lieu à des problèmes. Par exemple, que se passe-t-il si un utilisateur nous donne accidentellement une ligne qui n'est pas destinée à être utilisée comme une liste de noms de colonnes? Ou si le nom de la colonne doit contenir une virgule?


De plus, un tel code est plus difficile à maintenir, à déboguer et surtout à tester: dans les tests, il peut être possible de vérifier uniquement l'un des deux types que nous prenons en charge, mais la couverture sera toujours de 100% et nous ne testerons pas l'autre type.


En conséquence, nous sommes arrivés à la conclusion que Python permet à l'utilisateur de passer des arguments de n'importe quel type à des fonctions, mais la plupart d'entre eux dans la plupart des situations utiliseront une fonction de la même manière qu'en C: lui passer un argument du même type.


La nécessité d'utiliser eval() dans un programme est considérée comme un mauvais calcul architectural explicite. Très probablement, vous n'avez tout simplement pas compris comment faire la même chose d'une manière normale. − , Jupyter notebook - − eval() , Python ! , C++ .


, ( getattr() , hasattr() , isinstance() ) . , , , , : , , , !



: , . 20 , C++ Python. , , . .


, shared_ptr TensorFlow 2016 2018 .


TensorFlow − C++-, Python- ( C++ − TensorFlow, ).


image


TensorFlow, shared_ptr , . , .


C++? . , ? , , C++ Python!

Source: https://habr.com/ru/post/fr464405/


All Articles