Existência de tupla nomeada mutável em Python?

121

Alguém pode alterar namedtuple ou fornecer uma classe alternativa para que funcione para objetos mutáveis?

Principalmente para facilitar a leitura, gostaria de algo semelhante a namedtuple que faça isso:

from Camelot import namedgroup

Point = namedgroup('Point', ['x', 'y'])
p = Point(0, 0)
p.x = 10

>>> p
Point(x=10, y=0)

>>> p.x *= 10
Point(x=100, y=0)

Deve ser possível conservar o objeto resultante. E de acordo com as características da tupla nomeada, a ordem da saída quando representada deve corresponder à ordem da lista de parâmetros ao construir o objeto.

Alexandre
fonte
3
Consulte também: stackoverflow.com/q/5131044 . Existe uma razão pela qual você não pode simplesmente usar um dicionário?
senshin
@senshin Obrigado pelo link. Prefiro não usar dicionário pelo motivo nele apontado. Essa resposta também está vinculada a code.activestate.com/recipes/… , que é muito próximo do que estou procurando.
Alexander
Ao contrário de namedtuples, parece que você não precisa ser capaz de referenciar os atributos por índice, ou seja, p[0]e p[1]seriam formas alternativas de referenciar xe y, respectivamente, corrigir?
martineau,
Idealmente, sim, indexável por posição, como uma tupla simples, além de por nome, e descompactado como uma tupla. Esta receita do ActiveState está perto, mas acredito que ela usa um dicionário regular em vez de um OrderedDict. code.activestate.com/recipes/500261
Alexander,
2
Um namedtuple mutável é chamado de classe.
gbtimmon de

Respostas:

132

Existe uma alternativa mutável para collections.namedtuple- recordclass .

Ele tem a mesma API e pegada de memória namedtuplee oferece suporte a atribuições (deve ser mais rápido também). Por exemplo:

from recordclass import recordclass

Point = recordclass('Point', 'x y')

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

Para python 3.6 e superior recordclass(desde 0.5), suporta dicas de tipo:

from recordclass import recordclass, RecordClass

class Point(RecordClass):
   x: int
   y: int

>>> Point.__annotations__
{'x':int, 'y':int}
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

Há um exemplo mais completo (também inclui comparações de desempenho).

Desde a recordclassbiblioteca 0.9 fornece outra variante - recordclass.structclassfunção de fábrica. Ele pode produzir classes, cujas instâncias ocupam menos memória do que as __slots__instâncias baseadas em. Isso pode ser importante para as instâncias com valores de atributo, que não pretendem ter ciclos de referência. Isso pode ajudar a reduzir o uso de memória se você precisar criar milhões de instâncias. Aqui está um exemplo ilustrativo .

intelimatização
fonte
4
Gosto disso. 'Esta biblioteca é na verdade uma' prova de conceito 'para o problema da alternativa' mutável 'da tupla nomeada.'
Alexander,
1
recordclassé mais lento, ocupa mais memória e requer extensões C em comparação com a receita de Antti Haapala e namedlist.
GrantJ
recordclassé uma versão mutável do collection.namedtupleque herda sua api, pegada de memória, mas oferece suporte a atribuições. namedlisté na verdade uma instância da classe Python com slots. É mais útil se você não precisa de acesso rápido a seus campos por índice.
Intellimath
O acesso de atributos, por recordclassexemplo (python 3.5.2) é cerca de 2-3% mais lento do que paranamedlist
intellimath
Ao usar namedtupleuma criação de classe simples Point = namedtuple('Point', 'x y'), o Jedi pode preencher automaticamente os atributos, embora este não seja o caso recordclass. Se eu usar o código de criação mais longo (baseado em RecordClass), então Jedi entende a Pointclasse, mas não seu construtor ou atributos ... Existe uma maneira de começar recordclassa trabalhar bem com Jedi?
PhilMacKay
34

types.SimpleNamespace foi introduzido no Python 3.3 e oferece suporte aos requisitos solicitados.

from types import SimpleNamespace
t = SimpleNamespace(foo='bar')
t.ham = 'spam'
print(t)
namespace(foo='bar', ham='spam')
print(t.foo)
'bar'
import pickle
with open('/tmp/pickle', 'wb') as f:
    pickle.dump(t, f)
futuro funky
fonte
1
Há anos procuro algo assim. Excelente substituto para uma biblioteca ditado pontilhada como dotmap
axwell
1
Isso precisa de mais votos positivos. É exatamente o que o OP estava procurando, está na biblioteca padrão e não poderia ser mais simples de usar. Obrigado!
Tom Zych
3
-1 O OP deixou muito claro com seus testes o que ele precisa e SimpleNamespacefalhou nos testes 6-10 (acesso por índice, desempacotamento iterativo, iteração, dicionário ordenado, substituição in-loco) e 12, 13 (campos, slots). Observe que a documentação (que você vinculou na resposta) diz especificamente " SimpleNamespacepode ser útil como um substituto para class NS: pass. No entanto, para um tipo de registro estruturado, use namedtuple()".
Ali
1
-1 também SimpleNamespacecria um objeto, não um construtor de classe, e não pode substituir o namedtuple. A comparação de tipos não funcionará e a área de cobertura da memória será muito maior.
RedGlyph
26

Como uma alternativa muito Pythônica para esta tarefa, desde o Python-3.7, você pode usar um dataclassesmódulo que não apenas se comporta como um mutável, NamedTupleporque usa definições de classe normais, mas também suporta outros recursos de classe.

Do PEP-0557:

Embora usem um mecanismo muito diferente, as classes de dados podem ser consideradas "duplas nomeadas mutáveis ​​com padrões". Como as classes de dados usam a sintaxe de definição de classe normal, você está livre para usar herança, metaclasses, docstrings, métodos definidos pelo usuário, fábricas de classe e outros recursos de classe Python.

É fornecido um decorador de classe que inspeciona uma definição de classe para variáveis ​​com anotações de tipo conforme definido em PEP 526 , "Sintaxe para anotações de variáveis". Neste documento, essas variáveis ​​são chamadas de campos. Usando esses campos, o decorador adiciona definições de método geradas à classe para dar suporte à inicialização de instância, repr, métodos de comparação e, opcionalmente, outros métodos, conforme descrito na seção Especificação . Essa classe é chamada de Classe de Dados, mas não há realmente nada de especial sobre a classe: o decorador adiciona métodos gerados à classe e retorna a mesma classe que foi fornecida.

Este recurso é apresentado no PEP-0557 e você pode ler sobre ele em mais detalhes no link de documentação fornecido.

Exemplo:

In [20]: from dataclasses import dataclass

In [21]: @dataclass
    ...: class InventoryItem:
    ...:     '''Class for keeping track of an item in inventory.'''
    ...:     name: str
    ...:     unit_price: float
    ...:     quantity_on_hand: int = 0
    ...: 
    ...:     def total_cost(self) -> float:
    ...:         return self.unit_price * self.quantity_on_hand
    ...:    

Demo:

In [23]: II = InventoryItem('bisc', 2000)

In [24]: II
Out[24]: InventoryItem(name='bisc', unit_price=2000, quantity_on_hand=0)

In [25]: II.name = 'choco'

In [26]: II.name
Out[26]: 'choco'

In [27]: 

In [27]: II.unit_price *= 3

In [28]: II.unit_price
Out[28]: 6000

In [29]: II
Out[29]: InventoryItem(name='choco', unit_price=6000, quantity_on_hand=0)
Kasravnd
fonte
1
Ficou muito claro com os testes no OP o que é necessário e os dataclasstestes de falha 6-10 (acesso por índice, desempacotamento iterativo, iteração, dicionário ordenado, substituição in-loco) e 12, 13 (campos, slots) no Python 3.7 .1.
Ali
1
embora isso possa não ser especificamente o que o OP estava procurando, certamente me ajudou :)
Martin CR
25

A lista de nomes 1.7 mais recente passou em todos os seus testes com Python 2.7 e Python 3.5 em 11 de janeiro de 2016. É uma implementação de Python pura, enquanto recordclassé uma extensão C. Claro, depende de seus requisitos se uma extensão C é preferida ou não.

Seus testes (mas também veja a nota abaixo):

from __future__ import print_function
import pickle
import sys
from namedlist import namedlist

Point = namedlist('Point', 'x y')
p = Point(x=1, y=2)

print('1. Mutation of field values')
p.x *= 10
p.y += 10
print('p: {}, {}\n'.format(p.x, p.y))

print('2. String')
print('p: {}\n'.format(p))

print('3. Representation')
print(repr(p), '\n')

print('4. Sizeof')
print('size of p:', sys.getsizeof(p), '\n')

print('5. Access by name of field')
print('p: {}, {}\n'.format(p.x, p.y))

print('6. Access by index')
print('p: {}, {}\n'.format(p[0], p[1]))

print('7. Iterative unpacking')
x, y = p
print('p: {}, {}\n'.format(x, y))

print('8. Iteration')
print('p: {}\n'.format([v for v in p]))

print('9. Ordered Dict')
print('p: {}\n'.format(p._asdict()))

print('10. Inplace replacement (update?)')
p._update(x=100, y=200)
print('p: {}\n'.format(p))

print('11. Pickle and Unpickle')
pickled = pickle.dumps(p)
unpickled = pickle.loads(pickled)
assert p == unpickled
print('Pickled successfully\n')

print('12. Fields\n')
print('p: {}\n'.format(p._fields))

print('13. Slots')
print('p: {}\n'.format(p.__slots__))

Saída em Python 2.7

1. Mutação de valores de campo  
p: 10, 12

2. Corda  
p: Ponto (x = 10, y = 12)

3. Representação  
Ponto (x = 10, y = 12) 

4. Tamanho de  
tamanho de p: 64 

5. Acesso pelo nome do campo  
p: 10, 12

6. Acesso por índice  
p: 10, 12

7. Desempacotamento iterativo  
p: 10, 12

8. Iteração  
p: [10, 12]

9. Ditado Ordenado  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Substituição local (atualização?)  
p: Ponto (x = 100, y = 200)

11. Pickle e Unpickle  
Em conserva com sucesso

12. Campos  
p: ('x', 'y')

13. Slots  
p: ('x', 'y')

A única diferença com o Python 3.5 é que o namedlisttornou-se menor, o tamanho é 56 (relatórios do Python 2.7 64).

Observe que eu mudei seu teste 10 para substituição no local. O namedlisttem um _replace()método que faz uma cópia superficial e que faz todo o sentido para mim porque o namedtuplena biblioteca padrão se comporta da mesma maneira. Mudar a semântica do _replace()método seria confuso. Na minha opinião, o _update()método deve ser usado para atualizações no local. Ou talvez eu não tenha entendido a intenção do seu teste 10?

Todos
fonte
Existe uma nuance importante. Os namedlistvalores da loja na instância da lista. A coisa é que cpython's listé realmente uma matriz dinâmica. Por design, ele aloca mais memória do que o necessário para tornar a mutação da lista mais barata.
intellimath
1
@intellimath namedlist é um pouco incorreto. Na verdade, ele não herda liste, por padrão, usa a __slots__otimização. Quando fiz a medição, o uso de memória foi inferior a recordclass: 96 bytes vs 104 bytes para seis campos no Python 2.7
GrantJ
@GrantJ Sim. recorclassusa mais memória porque é um tupleobjeto semelhante com tamanho de memória variável.
Intellimath
2
Votos negativos anônimos não estão ajudando ninguém. O que há de errado com a resposta? Por que o downvote?
Ali
Eu amo a segurança contra erros de digitação que ele fornece com relação a types.SimpleNamespace. Infelizmente, o pylint não gosta disso :-(
xverges
23

Parece que a resposta a essa pergunta é não.

Abaixo está bem parecido, mas não é tecnicamente mutável. Isso é criar uma nova namedtuple()instância com um valor x atualizado:

Point = namedtuple('Point', ['x', 'y'])
p = Point(0, 0)
p = p._replace(x=10) 

Por outro lado, você pode criar uma classe simples usando __slots__que deve funcionar bem para atualizar atributos de instância de classe com frequência:

class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

Para adicionar a essa resposta, acho que __slots__é um bom uso aqui porque é eficiente em termos de memória quando você cria muitas instâncias de classe. A única desvantagem é que você não pode criar novos atributos de classe.

Aqui está um tópico relevante que ilustra a eficiência da memória - Dicionário vs Objeto - que é mais eficiente e por quê?

O conteúdo citado na resposta deste tópico é uma explicação muito sucinta porque __slots__é mais eficiente em termos de memória - slots Python

Kennes
fonte
1
Perto, mas desajeitado. Digamos que eu quisesse fazer uma atribuição + =, então eu precisaria fazer: p._replace (x = px + 10) vs. px + = 10
Alexander
1
sim, não está realmente mudando a tupla existente, está criando uma nova instância
kennes
7

O que se segue é uma boa solução para o Python 3: Uma classe mínima, utilizando __slots__e Sequenceclasse base resumo; não faz detecção de erros extravagante ou algo semelhante, mas funciona e se comporta principalmente como uma tupla mutável (exceto para typecheck).

from collections import Sequence

class NamedMutableSequence(Sequence):
    __slots__ = ()

    def __init__(self, *a, **kw):
        slots = self.__slots__
        for k in slots:
            setattr(self, k, kw.get(k))

        if a:
            for k, v in zip(slots, a):
                setattr(self, k, v)

    def __str__(self):
        clsname = self.__class__.__name__
        values = ', '.join('%s=%r' % (k, getattr(self, k))
                           for k in self.__slots__)
        return '%s(%s)' % (clsname, values)

    __repr__ = __str__

    def __getitem__(self, item):
        return getattr(self, self.__slots__[item])

    def __setitem__(self, item, value):
        return setattr(self, self.__slots__[item], value)

    def __len__(self):
        return len(self.__slots__)

class Point(NamedMutableSequence):
    __slots__ = ('x', 'y')

Exemplo:

>>> p = Point(0, 0)
>>> p.x = 10
>>> p
Point(x=10, y=0)
>>> p.x *= 10
>>> p
Point(x=100, y=0)

Se quiser, você pode ter um método para criar a classe também (embora usar uma classe explícita seja mais transparente):

def namedgroup(name, members):
    if isinstance(members, str):
        members = members.split()
    members = tuple(members)
    return type(name, (NamedMutableSequence,), {'__slots__': members})

Exemplo:

>>> Point = namedgroup('Point', ['x', 'y'])
>>> Point(6, 42)
Point(x=6, y=42)

No Python 2, você precisa ajustá-lo ligeiramente - se você herdar de Sequence, a classe terá um__dict__ e o __slots__deixará de funcionar.

A solução em Python 2 é não herdar de Sequence, mas object. Se isinstance(Point, Sequence) == Truedesejar, você precisa registrar o NamedMutableSequencecomo uma classe base para Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
fonte
3

Vamos implementar isso com a criação de tipo dinâmico:

import copy
def namedgroup(typename, fieldnames):

    def init(self, **kwargs): 
        attrs = {k: None for k in self._attrs_}
        for k in kwargs:
            if k in self._attrs_:
                attrs[k] = kwargs[k]
            else:
                raise AttributeError('Invalid Field')
        self.__dict__.update(attrs)

    def getattribute(self, attr):
        if attr.startswith("_") or attr in self._attrs_:
            return object.__getattribute__(self, attr)
        else:
            raise AttributeError('Invalid Field')

    def setattr(self, attr, value):
        if attr in self._attrs_:
            object.__setattr__(self, attr, value)
        else:
            raise AttributeError('Invalid Field')

    def rep(self):
         d = ["{}={}".format(v,self.__dict__[v]) for v in self._attrs_]
         return self._typename_ + '(' + ', '.join(d) + ')'

    def iterate(self):
        for x in self._attrs_:
            yield self.__dict__[x]
        raise StopIteration()

    def setitem(self, *args, **kwargs):
        return self.__dict__.__setitem__(*args, **kwargs)

    def getitem(self, *args, **kwargs):
        return self.__dict__.__getitem__(*args, **kwargs)

    attrs = {"__init__": init,
                "__setattr__": setattr,
                "__getattribute__": getattribute,
                "_attrs_": copy.deepcopy(fieldnames),
                "_typename_": str(typename),
                "__str__": rep,
                "__repr__": rep,
                "__len__": lambda self: len(fieldnames),
                "__iter__": iterate,
                "__setitem__": setitem,
                "__getitem__": getitem,
                }

    return type(typename, (object,), attrs)

Isso verifica os atributos para ver se eles são válidos antes de permitir que a operação continue.

Então, isso é pickleable? Sim se (e somente se) você fizer o seguinte:

>>> import pickle
>>> Point = namedgroup("Point", ["x", "y"])
>>> p = Point(x=100, y=200)
>>> p2 = pickle.loads(pickle.dumps(p))
>>> p2.x
100
>>> p2.y
200
>>> id(p) != id(p2)
True

A definição deve estar em seu namespace e deve existir por tempo suficiente para que pickle a encontre. Portanto, se você definir que está em seu pacote, deve funcionar.

Point = namedgroup("Point", ["x", "y"])

Pickle irá falhar se você fizer o seguinte ou tornar a definição temporária (sai do escopo quando a função termina, digamos):

some_point = namedgroup("Point", ["x", "y"])

E sim, ele preserva a ordem dos campos listados na criação do tipo.

MadMan2064
fonte
Se você adicionar um __iter__método com for k in self._attrs_: yield getattr(self, k), isso suportará descompactação como uma tupla.
snapshoe
Também é muito fácil de adicionar __len__, __getitem__e __setiem__métodos para apoiar ficando valus pelo índice, como p[0]. Com essas últimas partes, esta parece ser a resposta mais completa e correta (para mim, pelo menos).
snapshoe
__len__e __iter__são bons. __getitem__e __setitem__pode realmente ser mapeado para self.__dict__.__setitem__eself.__dict__.__getitem__
MadMan2064
2

As tuplas são, por definição, imutáveis.

No entanto, você pode criar uma subclasse de dicionário onde pode acessar os atributos com notação de ponto;

In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:class AttrDict(dict):
:
:    def __getattr__(self, name):
:        return self[name]
:
:    def __setattr__(self, name, value):
:        self[name] = value
:--

In [2]: test = AttrDict()

In [3]: test.a = 1

In [4]: test.b = True

In [5]: test
Out[5]: {'a': 1, 'b': True}
Roland Smith
fonte
2

Se você deseja um comportamento semelhante ao namedtuples, mas mutável, tente namedlist

Observe que, para ser mutável, não pode ser uma tupla.

agomcas
fonte
Obrigado pelo link. Isso parece o mais próximo até agora, mas preciso avaliá-lo com mais detalhes. A propósito, estou totalmente ciente de que as tuplas são imutáveis, e é por isso que estou procurando uma solução como namedtuple.
Alexander
0

Desde que o desempenho seja de pouca importância, pode-se usar um hack bobo como:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
fonte
1
Esta resposta não está muito bem explicada. Parece confuso se você não entende a natureza mutável das listas. --- Neste exemplo ... para reatribuir z, você tem que chamar mutable_z.z.pop(0)então mutable_z.z.append(new_value). Se errar, você acabará com mais de 1 elemento e seu programa se comportará de maneira inesperada.
byxor
1
@byxor que, ou você poderia apenas: mutable_z.z[0] = newValue. Na verdade, é um hack, como afirmado.
Srg
Oh sim, estou surpreso por ter perdido a maneira mais óbvia de reatribuí-lo.
byxor
Eu gosto disso, hack de verdade.
WebOrCode