
Este artículo trata sobre uno de los mejores inventos de Python: namedtuple. Consideraremos sus características agradables, desde conocidas hasta no obvias. El nivel de inmersión en el tema aumentará gradualmente, por lo que espero que todos encuentren algo interesante para ellos. Vamos!
Introduccion
Seguramente se enfrenta a una situación en la que necesita transferir varias propiedades del objeto en una sola pieza. Por ejemplo, información sobre una mascota: tipo, apodo y edad.
A menudo es demasiado vago crear una clase separada para esto, y se usan tuplas:
("pigeon", "", 3) ("fox", "", 7) ("parrot", "", 1)
Para mayor claridad, una tupla con nombre - collections.namedtuple
es adecuada:
from collections import namedtuple Pet = namedtuple("Pet", "type name age") frank = Pet(type="pigeon", name="", age=3) >>> frank.age 3
Todos lo saben ツ Y aquí hay algunas características menos conocidas:
Campos de cambio rápido
¿Qué pasa si una de las propiedades necesita ser cambiada? Frank está envejeciendo, y la caravana es inmutable. Para no recrearlo por completo, se nos ocurrió el método _replace()
:
>>> frank._replace(age=4) Pet(type='pigeon', name='', age=4)
Y si quieres hacer que toda la estructura sea mutable - _asdict()
:
>>> frank._asdict() OrderedDict([('type', 'pigeon'), ('name', ''), ('age', 3)])
Cambio automático de título
Suponga que importa datos de un CSV y convierte cada línea en una tupla. Los nombres de campo se tomaron del encabezado del archivo CSV. Pero algo sale mal:
La solución es el argumento rename=True
en el constructor:
Los nombres "sin éxito" se renombraron de acuerdo con los números de serie.
Valores por defecto
Si una tupla tiene un montón de campos opcionales, aún debe enumerarlos cada vez que crea un objeto:
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)
Para evitar esto, especifique los defaults
en el constructor:
Pet = namedtuple("Pet", "type name alt_name", defaults=("",)) >>> Pet("pigeon", "") Pet(type='pigeon', name='', alt_name='')
defaults
predeterminados asignan valores predeterminados de la cola. Funciona en python 3.7+
Para versiones anteriores, puede lograr de manera más torpe el mismo resultado a través del prototipo:
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='')
Pero con los defaults
, por supuesto, mucho mejor.
Extraordinaria ligereza
Uno de los beneficios de una tupla con nombre es la ligereza. Un ejército de cien mil palomas tomará solo 10 megabytes:
from collections import namedtuple import objsize
A modo de comparación, si convierte a Pet en una clase ordinaria, una lista similar ya ocupará 19 megabytes.
Esto sucede porque los objetos ordinarios en Python llevan una gran __dict__
__dict__, que contiene los nombres y valores de todos los atributos del objeto:
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}
Los objetos namedtuple carecen de este diccionario y, por lo tanto, ocupan menos memoria:
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
Pero, ¿cómo se libró la tupla nombrada de __dict__
? Sigue leyendo ツ
Rico mundo interior
Si ha estado trabajando con Python durante mucho tiempo, entonces probablemente sepa: se puede crear un objeto liviano a través de la __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)
Los objetos "Slot" no tienen un diccionario con atributos, por lo que ocupan poca memoria. "Frank en las máquinas tragamonedas" es tan ligero como "Frank en la caravana", ver:
>>> objsize.get_deep_size(frank) 239 >>> objsize.get_deep_size(frank_slots) 231
Si decides que namedtuple también usa slots, esto no está lejos de la verdad. Como recordará, las clases de tuplas específicas se declaran dinámicamente:
Pet = namedtuple("Pet", "type name age")
El constructor namedtuple usa magia oscura diferente y genera algo como esta clase (simplificando enormemente):
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))
Es decir, nuestra mascota es una tuple
ordinaria, a la que se han clavado tres métodos de propiedad con clavos:
type
devuelve el elemento nulo de la tuplaname
: el primer elemento de la tuplaage
- el segundo elemento de la tupla
Y __slots__
solo __slots__
necesita para que los objetos sean ligeros. Como resultado, Pet ocupa poco espacio y puede usarse como una tupla regular:
>>> frank.index("") 1 >>> type, _, _ = frank >>> type 'pigeon'
Ingeniosamente inventado, ¿eh?
No es inferior a las clases de datos
Ya que estamos hablando de generación de código. En Python 3.7, apareció un código uber-generador, que no tiene igual - clases de datos.
Cuando vea por primera vez una clase de datos, desea cambiar a una nueva versión del idioma solo por el bien de esta:
from dataclasses import dataclass @dataclass class PetData: type: str name: str age: int
¡El milagro es tan bueno! Pero hay un matiz: es gordo:
frank_data = PetData(type="pigeon", name="", age=3) >>> objsize.get_deep_size(frank_data) 335 >>> objsize.get_deep_size(frank) 239
La clase de datos genera una clase de python regular, cuyos objetos se agotan bajo el peso de __dict__
. Entonces, si está leyendo líneas desde la base y convirtiéndolas en objetos, las clases de datos no son la mejor opción.
Pero espera, puedes congelar una clase de datos como una tupla. Tal vez entonces será más fácil?
@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
Por desgracia Incluso congelado, seguía siendo un objeto pesado ordinario con un diccionario de atributos. Entonces, si necesita objetos inmutables ligeros (que también se pueden usar como tuplas regulares), namedtuple sigue siendo la mejor opción.
⌘ ⌘ ⌘
Realmente me gusta la tupla nombrada:
- honesto iterable,
- declaración de tipo dinámico
- Acceso a atributos con nombre
- Ligero e inmutable.
Y al mismo tiempo se implementa en 150 líneas de código. ¿Qué más se necesita para la felicidad?
Si desea saber más sobre la biblioteca estándar de Python, suscríbase al canal @ohmypy