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 is appropriate - collections.namedtuple:


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

    Everyone knows that. Вот And here are some less well-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 recreate it entirely, a method was invented _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 an argument rename=Truein 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 an argument in the constructor defaults:


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

    defaultsassigns 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 python carry with them a weighty dander __dict__, in which the names and values ​​of all the attributes of an object lie:


    classPetObj: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 did the named tuple get rid of  __dict__? Read on ツ


    Rich inner world


    If you have been working with python for a long time, you probably know that a lightweight object can be created through a dander __slots__:


    classPetSlots:
        __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):


    classPet(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 the usual one tuple, to which three properties-properties were nailed:


    • type returns the zero element of the tuple
    • name - the first element of the tuple
    • age - the second element of the tuple

    And it is  __slots__necessary only in order that objects turned out easy. 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
    @dataclassclassPetData:
        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 pit class, the objects of which are exhausted under the weight __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)classPetFrozen:
        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:


    • honest iterable,
    • dynamic type declaration
    • named access to attributes
    • easy and unchangeable.

    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


    Also popular now: