📜 ⬆️ ⬇️

Healthy people cortege

Named tuple
This article is about one of the best inventions of Python: the named tuple (namedtuple). We look at its nice features, from well-known to non-obvious. The level of immersion in the topic will increase gradually, so I hope everyone will find something interesting for themselves. Go!


Introduction


Surely you are faced with a situation where you need to pass several properties of an object in one piece. For example, information about a pet: type, nickname and age.


Often create a separate class for this case laziness, and use tuples:


("pigeon", "Френк", 3) ("fox", "Клер", 7) ("parrot", "Питер", 1) 

For greater clarity, a named tuple - collections.namedtuple :


 from collections import namedtuple Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="Френк", age=3) >>> frank.age 3 

Everyone knows this. Вот Here are some of the lesser-known features:


Quick field change


What if one of the properties needs to be changed? Frank is aging, and the tuple is immutable. In order not to re-create it entirely, the _replace() method was _replace() :


 >>> frank._replace(age=4) Pet(type='pigeon', name='Френк', age=4) 

And if you want to make the whole structure changeable - _asdict() :


 >>> frank._asdict() OrderedDict([('type', 'pigeon'), ('name', 'Френк'), ('age', 3)]) 

Automatic name replacement


Suppose you import data from CSV and turn each line into a tuple. The field names were taken from the header of the CSV file. But something goes wrong:


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

The solution is the argument rename=True in the constructor:


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

"Unsuccessful" names were renamed in accordance with the sequence numbers.


Default values


If a tuple has a bunch of optional fields, you still have to list them every time you create an object:


 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) 

To avoid this, specify the defaults argument in the constructor:


 Pet = namedtuple("Pet", "type name alt_name", defaults=("нет",)) >>> Pet("pigeon", "Френк") Pet(type='pigeon', name='Френк', alt_name='нет') 

defaults sets default values ​​from tail. Works in python 3.7+


For older versions, you can more clumsily achieve the same result through a 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='нет') 

But with defaults , of course, much nicer.


Extraordinary lightness


One of the advantages of a named tuple is lightness. An army of one hundred thousand pigeons will take only 10 megabytes:


 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 

For comparison, if you make Pet a regular class, the same list will take up 19 megabytes.


This happens because ordinary objects in the python carry with them a weighty dander __dict__ , in which the names and values ​​of all the attributes of the object lie:


 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} 

Namedupup objects are deprived of this dictionary, and therefore occupy less memory:


 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 

But how could a named tuple get rid of __dict__ ? Read on ツ


Rich inner world


If you have been working with python for a long time, then you probably know: you can create a lightweight object through the __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) 

Slot objects do not have a dictionary with attributes, so they take up little memory. “Frank on slots” is as light as “Frank on a tuple”, see:


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

If you decide that namedtuple also uses slots, this is not far from the truth. As you remember, concrete tuple classes are declared dynamically:


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

The namedtuple constructor uses different dark magic and generates something like this class (I greatly simplify):


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

That is, our Pet is an ordinary tuple , to which three properties-methods were nailed:



And __slots__ needed only to make the objects light. As a result, Pet and takes up little space, and can be used as a normal tuple:


 >>> frank.index("Френк") 1 >>> type, _, _ = frank >>> type 'pigeon' 

Slyly invented, eh?


Not inferior to data classes


Since we are talking about code generation. In Python 3.7, an uber code generator appeared that has no equal - data classes (dataclasses).


When you first see the data class, you want to switch to a new version of the language just for the sake of it:


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

A miracle is so good! But there is a nuance - it is fat:


 frank_data = PetData(type="pigeon", name="Френк", age=3) >>> objsize.get_deep_size(frank_data) 335 >>> objsize.get_deep_size(frank) 239 

The data class generates the usual python class, the objects of which are exhausted under the weight of __dict__ . So if you read a car of lines from the base and turn them into objects, data classes are not the best choice.


But wait, the data class can be “frozen” like a tuple. Maybe then it will be easier?


 @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 

Alas. Even frozen, it remained an ordinary weighty object with a dictionary of attributes. So if you need light immutable objects (which can also be used as regular tuples) - namedtuple is still the best choice.


⌘ ⌘


I really like the named tuple:



And it is implemented in 150 lines of code. What else is needed for happiness ツ


If you want to learn more about the standard Python library, subscribe to the @ohmypy channel



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