Das Tupel eines gesunden Menschen

Benanntes Tupel
Dieser Artikel handelt von einer der besten Erfindungen von Python: namedtuple. Wir werden seine angenehmen Eigenschaften betrachten, von bekannt bis nicht offensichtlich. Der Grad des Eintauchens in das Thema wird allmählich zunehmen, daher hoffe ich, dass jeder etwas Interessantes für sich findet. Lass uns gehen!


Einführung


Sicherlich sind Sie mit einer Situation konfrontiert, in der Sie mehrere Eigenschaften des Objekts in einem Stück übertragen müssen. Zum Beispiel Informationen zu einem Haustier: Typ, Spitzname und Alter.


Oft ist es zu faul, eine separate Klasse für dieses Ding zu erstellen, und es werden Tupel verwendet:


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

Zur besseren Übersichtlichkeit ist ein benanntes Tupel - collections.namedtuple geeignet:


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

Jeder weiß das ツ Und hier sind ein paar weniger bekannte Funktionen:


Schnellwechselfelder


Was ist, wenn eine der Eigenschaften geändert werden muss? Frank altert und die Wagenkolonne ist unveränderlich. Um es nicht vollständig neu zu erstellen, haben wir die Methode _replace() entwickelt:


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

Und wenn Sie die gesamte Struktur veränderbar machen möchten - _asdict() :


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

Automatische Titeländerung


Angenommen, Sie importieren Daten aus einer CSV und verwandeln jede Zeile in ein Tupel. Die Feldnamen wurden aus dem Header der CSV-Datei übernommen. Aber etwas geht schief:


 # 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' 

Die Lösung ist das Argument rename=True im Konstruktor:


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

"Nicht erfolgreiche" Namen wurden gemäß den Seriennummern umbenannt.


Standardwerte


Wenn ein Tupel mehrere optionale Felder enthält, müssen Sie diese bei jedem Erstellen eines Objekts auflisten:


 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) 

Um dies zu vermeiden, geben Sie im Konstruktor die defaults an:


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

Standardmäßig werden Standardwerte vom Ende zugewiesen. Funktioniert in Python 3.7+


Bei älteren Versionen können Sie mit dem Prototyp ungeschickter das gleiche Ergebnis erzielen:


 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='') 

Aber mit defaults natürlich viel schöner.


Außergewöhnliche Leichtigkeit


Einer der Vorteile eines benannten Tupels ist die Leichtigkeit. Eine Armee von hunderttausend Tauben benötigt nur 10 Megabyte:


 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 

Wenn Sie Pet zum Vergleich zu einer normalen Klasse machen, belegt eine ähnliche Liste bereits 19 Megabyte.


Dies geschieht, weil gewöhnliche Objekte in Python einen schweren __dict__ Dander tragen, der die Namen und Werte aller Attribute des Objekts enthält:


 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} 

Die Namedtuple-Objekte haben kein Wörterbuch und benötigen daher weniger Speicher:


 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 

Aber wie wurde das genannte Tupel __dict__ los? Lesen Sie weiter ツ


Reiche innere Welt


Wenn Sie schon lange mit Python arbeiten, wissen Sie wahrscheinlich: Ein leichtes Objekt kann über den Dander __slots__ erstellt werden:


 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) 

"Slot" -Objekte haben kein Wörterbuch mit Attributen, daher nehmen sie wenig Speicherplatz ein. "Frank in den Slots" ist so leicht wie "Frank in der Autokolonne", siehe:


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

Wenn Sie entscheiden, dass namedtuple auch Slots verwendet, ist dies nicht weit von der Wahrheit entfernt. Wie Sie sich erinnern, werden bestimmte Tupelklassen dynamisch deklariert:


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

Der Namedtuple-Konstruktor verwendet verschiedene dunkle Magie und generiert so etwas wie diese Klasse (stark vereinfacht):


 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)) 

Das heißt, unser Haustier ist ein gewöhnliches tuple , an das drei Eigenschaftsmethoden mit Nägeln geheftet wurden:


  • type gibt das Nullelement des Tupels zurück
  • name - das erste Element des Tupels
  • age - das zweite Element des Tupels

Und __slots__ nur benötigt, um die Objekte hell zu machen. Infolgedessen nimmt Pet wenig Platz ein und kann als normales Tupel verwendet werden:


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

Listig erfunden, oder?


Datenklassen nicht unterlegen


Da sprechen wir über Codegenerierung. In Python 3.7 ist ein Übergenerator-Code erschienen, der keine gleichen Datenklassen hat.


Wenn Sie zum ersten Mal eine Datenklasse sehen, möchten Sie nur aus diesem Grund zu einer neuen Version der Sprache wechseln:


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

Wunder ist so gut! Aber es gibt eine Nuance - es ist fett:


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

Die Datenklasse generiert eine reguläre Python-Klasse, deren Objekte unter dem Gewicht von __dict__ erschöpft sind. Wenn Sie also Zeilen von der Basis lesen und in Objekte verwandeln, sind Datenklassen nicht die beste Wahl.


Aber warten Sie, Sie können eine Datenklasse wie ein Tupel einfrieren. Vielleicht wird es dann einfacher?


 @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 

Leider. Selbst eingefroren blieb es ein gewöhnliches schweres Objekt mit einem Wörterbuch von Attributen. Wenn Sie also leichte unveränderliche Objekte benötigen (die auch als normale Tupel verwendet werden können), ist namedtuple immer noch die beste Wahl.


⌘ ⌘ ⌘


Ich mag das genannte Tupel wirklich:


  • ehrlich iterable,
  • dynamische Typdeklaration
  • Benannter Attributzugriff
  • leicht und unveränderlich.

Gleichzeitig wird es in 150 Codezeilen implementiert. Was braucht man sonst noch für das Glück?


Wenn Sie mehr über die Standard-Python-Bibliothek erfahren möchten, abonnieren Sie den @ ohmypy- Kanal

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


All Articles