
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:
A solução é o argumento rename=True
no construtor:
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
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 tuplaname
- o primeiro elemento da tuplaage
- 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