
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:
La solution est l'argument rename=True
dans le constructeur:
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
À 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 tuplename
- le premier élément du tupleage
- 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