La tupla de una persona sana.

Tupla nombrada
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:


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

La solución es el argumento rename=True en el constructor:


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

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

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 tupla
  • name : el primer elemento de la tupla
  • age - 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

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


All Articles