A tupla de uma pessoa saudável

Tupla nomeada
Este artigo é sobre uma das melhores invenções do Python: nomeada dupla. Vamos considerar suas características agradáveis, de conhecidas a não óbvias. O nível de imersão no tópico aumentará gradualmente, por isso espero que todos encontrem algo interessante para si. Vamos lá!


1. Introdução


Certamente você se depara com uma situação em que precisa transferir várias propriedades do objeto em uma única peça. Por exemplo, informações sobre um animal de estimação: tipo, apelido e idade.


Muitas vezes, é muito preguiçoso criar uma classe separada para essa coisa e as tuplas são usadas:


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

Para maior clareza, uma tupla nomeada - collections.namedtuple é adequada:


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

Todo mundo sabe disso ツ E aqui estão alguns recursos menos conhecidos:


Campos de mudança rápida


E se uma das propriedades precisar ser alterada? Frank está envelhecendo e o desfile é imutável. Para não recriá-lo completamente, criamos o método _replace() :


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

E se você quiser tornar toda a estrutura mutável - _asdict() :


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

Mudança automática de título


Suponha que você importe dados de um CSV e transforme cada linha em uma tupla. Os nomes dos campos foram retirados do cabeçalho do arquivo CSV. Mas algo dá errado:


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

A solução é o argumento rename=True no construtor:


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

Os nomes "sem êxito" foram renomeados de acordo com os números de série.


Valores padrão


Se uma tupla tiver vários campos opcionais, você ainda precisará listá-los sempre que criar um 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 isso, especifique os defaults no construtor:


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

defaults atribui valores padrão a partir da cauda. Funciona em python 3.7+


Para versões mais antigas, você pode obter mais desajeitadamente o mesmo resultado através do protótipo:


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

Mas com os defaults , é claro, muito melhor.


Leveza extraordinária


Um dos benefícios de uma tupla nomeada é a leveza. Um exército de cem mil pombos terá apenas 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 

Para comparação, se você transformar Pet em uma classe comum, uma lista semelhante já ocupará 19 megabytes.


Isso acontece porque objetos comuns em python carregam um __dict__ __dict__ pesado, que contém os nomes e valores de todos os atributos do 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} 

Os objetos de nome nomeado são desprovidos desse dicionário e, portanto, ocupam menos memória:


 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 

Mas como a tupla nomeada se livrou de __dict__ ? Leia em ツ


Rico mundo interior


Se você trabalha com python há muito tempo, provavelmente sabe: um objeto leve pode ser criado através do __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) 

Os objetos "slot" não possuem um dicionário com atributos e, portanto, ocupam pouca memória. "Frank nas faixas horárias" é tão leve quanto "Frank nas paradas", veja:


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

Se você decidir que o nomeado duplo também usa slots, isso não está longe da verdade. Como você se lembra, classes de tupla específicas são declaradas dinamicamente:


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

O construtor nomeduplo usa magia negra diferente e gera algo como esta classe (simplificando bastante):


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

Ou seja, nosso animal de estimação é uma tuple comum, na qual três métodos de propriedade foram fixados com unhas:


  • type retorna o elemento nulo da tupla
  • name - o primeiro elemento da tupla
  • age - o segundo elemento da tupla

E __slots__ necessário apenas para iluminar os objetos. Como resultado, o Pet ocupa pouco espaço e pode ser usado como uma tupla regular:


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

Astuciosamente inventado, hein?


Não inferior às classes de dados


Já que estamos falando sobre geração de código. No python 3.7, apareceu um código uber-gerador, que não tem igual - classes de dados.


Quando você vê uma classe de dados pela primeira vez, deseja mudar para uma nova versão do idioma apenas para isso:


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

Milagre é tão bom! Mas há uma nuance - é gordo:


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

A classe de dados gera uma classe python regular, cujos objetos são esgotados com o peso de __dict__ . Portanto, se você estiver lendo linhas da base e transformando-as em objetos, as classes de dados não são a melhor opção.


Mas espere, você pode congelar uma classe de dados como uma tupla. Talvez então se torne mais 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 

Infelizmente. Mesmo congelado, continuava sendo um objeto comum e pesado, com um dicionário de atributos. Portanto, se você precisar de objetos leves e imutáveis ​​(que também podem ser usados ​​como tuplas regulares), o nome nomuple ainda é a melhor opção.


⌘ ⌘ ⌘


Eu realmente gosto da tupla nomeada:


  • iterável honesto,
  • declaração de tipo dinâmico
  • Acesso ao atributo nomeado
  • leve e imutável.

E, ao mesmo tempo, é implementado em 150 linhas de código. O que mais é necessário para a felicidade?


Se você quiser saber mais sobre a biblioteca Python padrão, assine o canal @ohmypy

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


All Articles