Le tuple d'une personne en bonne santé

Tuple nommé
Cet article concerne l'une des meilleures inventions de Python: namedtuple. Nous considérerons ses caractéristiques agréables, de bien connues à non évidentes. Le niveau d'immersion dans le sujet augmentera progressivement, j'espère donc que chacun trouvera quelque chose d'intéressant pour lui-même. C'est parti!


Présentation


Vous êtes certainement confronté à une situation où vous devez transférer plusieurs propriétés de l'objet en une seule pièce. Par exemple, des informations sur un animal de compagnie: type, surnom et âge.


Souvent, c'est trop paresseux pour créer une classe distincte pour cette chose, et des tuples sont utilisés:


("pigeon", "", 3) ("fox", "", 7) ("parrot", "", 1) 

Pour plus de clarté, un tuple nommé - collections.namedtuple convient:


 from collections import namedtuple Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="", age=3) >>> frank.age 3 

Tout le monde le sait ツ Et voici quelques fonctionnalités moins connues:


Champs de changement rapide


Que se passe-t-il si l'une des propriétés doit être modifiée? Frank vieillit et le cortège est immuable. Afin de ne pas le recréer entièrement, nous avons trouvé la méthode _replace() :


 >>> frank._replace(age=4) Pet(type='pigeon', name='', age=4) 

Et si vous voulez rendre la structure entière mutable - _asdict() :


 >>> frank._asdict() OrderedDict([('type', 'pigeon'), ('name', ''), ('age', 3)]) 

Changement de titre automatique


Supposons que vous importiez des données à partir d'un CSV et transformiez chaque ligne en un tuple. Les noms de champ ont été extraits de l'en-tête du fichier CSV. Mais quelque chose ne va pas:


 # headers = ("name", "age", "with") >>> Pet = namedtuple("Pet", headers) ValueError: Type names and field names cannot be a keyword: 'with' # headers = ("name", "age", "name") >>> Pet = namedtuple("Pet", headers) ValueError: Encountered duplicate field name: 'name' 

La solution est l'argument rename=True dans le constructeur:


 # headers = ("name", "age", "with", "color", "name", "food") Pet = namedtuple("Pet", headers, rename=True) >>> Pet._fields ('name', 'age', '_2', 'color', '_4', 'food') 

Les noms «infructueux» ont été renommés conformément aux numéros de série.


Valeurs par défaut


Si un tuple a un tas de champs facultatifs, vous devez toujours les répertorier chaque fois que vous créez un objet:


 Pet = namedtuple("Pet", "type name alt_name") >>> Pet("pigeon", "") TypeError: __new__() missing 1 required positional argument: 'alt_name' >>> Pet("pigeon", "", None) Pet(type='pigeon', name='', alt_name=None) 

Pour éviter cela, spécifiez les defaults par defaults dans le constructeur:


 Pet = namedtuple("Pet", "type name alt_name", defaults=("",)) >>> Pet("pigeon", "") Pet(type='pigeon', name='', alt_name='') 

defaults assigne des valeurs par défaut à partir de la queue. Fonctionne en python 3.7+


Pour les versions plus anciennes, vous pouvez plus maladroitement obtenir le même résultat grâce au prototype:


 Pet = namedtuple("Pet", "type name alt_name") default_pet = Pet(None, None, "") >>> default_pet._replace(type="pigeon", name="") Pet(type='pigeon', name='', alt_name='') >>> default_pet._replace(type="fox", name="") Pet(type='fox', name='', alt_name='') 

Mais avec les defaults par defaults , bien sûr, beaucoup plus agréable.


Légèreté extraordinaire


L'un des avantages d'un tuple nommé est la légèreté. Une armée de cent mille pigeons ne prendra que 10 mégaoctets:


 from collections import namedtuple import objsize # 3rd party Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="", age=None) pigeons = [frank._replace(age=idx) for idx in range(100000)] >>> round(objsize.get_deep_size(pigeons)/(1024**2), 2) 10.3 

À titre de comparaison, si vous faites de Pet une classe ordinaire, une liste similaire occupera déjà 19 mégaoctets.


Cela se produit car les objets ordinaires en python portent un lourd squame __dict__, qui contient les noms et les valeurs de tous les attributs de l'objet:


 class PetObj: def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_obj = PetObj(type="pigeon", name="", age=3) >>> frank_obj.__dict__ {'type': 'pigeon', 'name': '', 'age': 3} 

Les objets nommés tuple sont dépourvus de ce dictionnaire, et prennent donc moins de mémoire:


 frank = Pet(type="pigeon", name="", age=3) >>> frank.__dict__ AttributeError: 'Pet' object has no attribute '__dict__' >>> objsize.get_deep_size(frank_obj) 335 >>> objsize.get_deep_size(frank) 239 

Mais comment le tuple nommé s'est-il débarrassé de __dict__ ? Lisez la suite ツ


Un monde intérieur riche


Si vous travaillez avec python depuis longtemps, alors vous savez probablement: un objet léger peut être créé via les __slots__ __slots__:


 class PetSlots: __slots__ = ("type", "name", "age") def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_slots = PetSlots(type="pigeon", name="", age=3) 

Les objets "slot" n'ont pas de dictionnaire avec des attributs, donc ils prennent peu de mémoire. "Frank on the slots" est aussi léger que "Frank on the cortège", voir:


 >>> objsize.get_deep_size(frank) 239 >>> objsize.get_deep_size(frank_slots) 231 

Si vous décidez que namedtuple utilise également des slots, ce n'est pas loin de la vérité. Comme vous vous en souvenez, des classes de tuple spécifiques sont déclarées dynamiquement:


 Pet = namedtuple("Pet", "type name age") 

Le constructeur namedtuple utilise une magie noire différente et génère quelque chose comme cette classe (simplifiant considérablement):


 class Pet(tuple): __slots__ = () type = property(operator.itemgetter(0)) name = property(operator.itemgetter(1)) age = property(operator.itemgetter(2)) def __new__(cls, type, name, age): return tuple.__new__(cls, (type, name, age)) 

Autrement dit, notre animal de compagnie est un tuple ordinaire, auquel trois méthodes de propriété ont été épinglées avec des clous:


  • type renvoie l'élément nul du tuple
  • name - le premier élément du tuple
  • age - le deuxième élément du tuple

Et __slots__ nécessaire que pour éclairer les objets. En conséquence, Pet prend peu de place et peut être utilisé comme un tuple régulier:


 >>> frank.index("") 1 >>> type, _, _ = frank >>> type 'pigeon' 

Astucieusement inventé, hein?


Pas inférieur aux classes de données


Puisque nous parlons de génération de code. En python 3.7, un code uber-generator est apparu, qui n'a pas d'égale - les dataclasses.


Lorsque vous voyez une classe de données pour la première fois, vous souhaitez passer à une nouvelle version du langage juste pour le plaisir:


 from dataclasses import dataclass @dataclass class PetData: type: str name: str age: int 

Le miracle est si bon! Mais il y a une nuance - c'est gras:


 frank_data = PetData(type="pigeon", name="", age=3) >>> objsize.get_deep_size(frank_data) 335 >>> objsize.get_deep_size(frank) 239 

La classe de données génère une classe python régulière, dont les objets sont épuisés sous le poids de __dict__ . Donc, si vous lisez des lignes de la base et les transformez en objets, les classes de données ne sont pas le meilleur choix.


Mais attendez, vous pouvez geler une classe de données comme un tuple. Peut-être que cela deviendra plus facile?


 @dataclass(frozen=True) class PetFrozen: type: str name: str age: int frank_frozen = PetFrozen(type="pigeon", name="", age=3) >>> objsize.get_deep_size(frank_frozen) 335 

Hélas. Même figé, il est resté un objet de poids ordinaire avec un dictionnaire d'attributs. Donc, si vous avez besoin d'objets légers immuables (qui peuvent également être utilisés comme tuples réguliers) - namedtuple est toujours le meilleur choix.


⌘ ⌘ ⌘


J'aime vraiment le tuple nommé:


  • honnête itérable,
  • déclaration de type dynamique
  • Accès aux attributs nommés
  • léger et immuable.

Et en même temps, il est implémenté dans 150 lignes de code. Que faut-il d'autre pour le bonheur?


Si vous voulez en savoir plus sur la bibliothèque Python standard, abonnez-vous au canal @ohmypy

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


All Articles