Esistenza di tupla denominata mutabile in Python?


121

Qualcuno può modificare namedtuple o fornire una classe alternativa in modo che funzioni per oggetti mutabili?

Principalmente per la leggibilità, vorrei qualcosa di simile a namedtuple che faccia questo:

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 essere possibile decapare l'oggetto risultante. E per le caratteristiche della tupla denominata, l'ordinamento dell'output quando rappresentato deve corrispondere all'ordine dell'elenco dei parametri durante la costruzione dell'oggetto.


3
Vedi anche: stackoverflow.com/q/5131044 . C'è una ragione per cui non puoi usare semplicemente un dizionario?
senshin

@senshin Grazie per il collegamento. Preferisco non usare un dizionario per il motivo indicato in esso. Quella risposta è anche collegata a code.activestate.com/recipes/… , che è abbastanza vicino a quello che sto cercando .
Alexander

A differenza di namedtuples, sembra che non sia necessario essere in grado di fare riferimento agli attributi in base all'indice, quindi p[0]e p[1]sarebbero modi alternativi per fare riferimento xe y, rispettivamente, correggere?
martineau

Idealmente, sì, indicizzabile in base alla posizione come una semplice tupla oltre al nome e decomprimibile come una tupla. Questa ricetta di ActiveState è vicina, ma credo che utilizzi un dizionario normale invece di un OrderedDict. code.activestate.com/recipes/500261
Alexander

2
Una coppia denominata mutevole è chiamata classe.
gbtimmon

Risposte:


132

C'è un'alternativa mutevole a collections.namedtuple- recordclass .

Ha la stessa API e lo stesso footprint di memoria namedtuplee supporta le assegnazioni (dovrebbe essere anche più veloce). Per esempio:

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)

Per python 3.6 e versioni successive recordclass(da 0.5) supportano i suggerimenti sui tipi:

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)

C'è un esempio più completo (include anche confronti delle prestazioni).

A partire dalla recordclasslibreria 0.9 fornisce un'altra variante: la recordclass.structclassfunzione di fabbrica. Può produrre classi, le cui istanze occupano meno memoria rispetto alle __slots__istanze basate su. Questo può essere importante per le istanze con valori di attributo, che non hanno previsto cicli di riferimento. Può aiutare a ridurre l'utilizzo della memoria se è necessario creare milioni di istanze. Ecco un esempio illustrativo .


4
Mi piace. 'Questa libreria è in realtà una "prova di concetto" per il problema dell'alternativa "mutevole" della tupla con nome.
Alexander

1
recordclassè più lento, richiede più memoria e richiede estensioni C rispetto alla ricetta di Antti Haapala e namedlist.
GrantJ

recordclassè una versione mutabile di collection.namedtupleche eredita la sua API, impronta di memoria, ma supporta le assegnazioni. namedlistè in realtà un'istanza della classe Python con slot. È più utile se non hai bisogno di un accesso rapido ai suoi campi per indice.
intellimath

L'accesso agli attributi, ad recordclassesempio (python 3.5.2) è circa il 2-3% più lento rispetto anamedlist
intellimath,

Quando si utilizza una namedtuplesemplice creazione di classi Point = namedtuple('Point', 'x y'), Jedi può completare automaticamente gli attributi, mentre questo non è il caso recordclass. Se uso il codice di creazione più lungo (basato su RecordClass), allora Jedi comprende la Pointclasse, ma non il suo costruttore o attributi ... C'è un modo recordclassper lavorare bene con Jedi?
PhilMacKay

34

types.SimpleNamespace è stato introdotto in Python 3.3 e supporta i requisiti richiesti.

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)

1
Sono anni che cerco qualcosa di simile. Ottimo sostituto per una libreria dict punteggiata come dotmap
axwell

1
Questo richiede più voti positivi. È esattamente ciò che l'OP stava cercando, è nella libreria standard e non potrebbe essere più semplice da usare. Grazie!
Tom Zych,

3
-1 L'OP ha chiarito molto chiaramente con i suoi test ciò di cui ha bisogno e SimpleNamespacefallisce i test 6-10 (accesso per indice, decompressione iterativa, iterazione, comando ordinato, sostituzione sul posto) e 12, 13 (campi, slot). Nota che la documentazione (che hai collegato nella risposta) dice specificamente " SimpleNamespacepuò essere utile in sostituzione di class NS: pass. Tuttavia, per un tipo di record strutturato usa namedtuple()invece".
Ali

1
Anche -1, SimpleNamespacecrea un oggetto, non un costruttore di classi, e non può essere un sostituto di namedtuple. Il confronto dei tipi non funzionerà e il footprint di memoria sarà molto più alto.
RedGlyph

26

Come alternativa molto pitonica per questo compito, a partire da Python-3.7, puoi usare il dataclassesmodulo che non solo si comporta come un mutabile NamedTupleperché usa le normali definizioni di classe ma supporta anche altre caratteristiche di classe.

Da PEP-0557:

Sebbene utilizzino un meccanismo molto diverso, le classi di dati possono essere considerate come "coppie denominate mutabili con impostazioni predefinite". Poiché le classi di dati utilizzano la normale sintassi della definizione di classe, sei libero di utilizzare ereditarietà, metaclassi, docstring, metodi definiti dall'utente, class factory e altre funzionalità di classe Python.

Viene fornito un decoratore di classe che ispeziona una definizione di classe per le variabili con annotazioni di tipo come definito in PEP 526 , "Sintassi per annotazioni di variabili". In questo documento, tali variabili sono chiamate campi. Utilizzando questi campi, il decoratore aggiunge le definizioni dei metodi generati alla classe per supportare l'inizializzazione dell'istanza, una riproduzione, metodi di confronto e, facoltativamente, altri metodi come descritto nella sezione Specifiche . Una tale classe è chiamata Data Class, ma non c'è davvero niente di speciale nella classe: il decoratore aggiunge metodi generati alla classe e restituisce la stessa classe che le è stata assegnata.

Questa funzione è stata introdotta in PEP-0557 e puoi leggerla in maggiore dettaglio nel collegamento alla documentazione fornito.

Esempio:

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)

1
È stato reso molto chiaro con i test nell'OP cosa è necessario e dataclassnon supera i test 6-10 (accesso per indice, spacchettamento iterativo, iterazione, comando ordinato, sostituzione sul posto) e 12, 13 (campi, slot) in Python 3.7 .1.
Ali

1
anche se questo potrebbe non essere esattamente ciò che l'OP stava cercando, mi ha sicuramente aiutato :)
Martin CR

25

L'ultima namedlist 1.7 supera tutti i tuoi test sia con Python 2.7 che con Python 3.5 a partire dall'11 gennaio 2016. È un'implementazione pura di Python mentre recordclassè un'estensione C. Naturalmente, dipende dalle tue esigenze se un'estensione C è preferita o meno.

I tuoi test (ma vedi anche la nota sotto):

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

Output su Python 2.7

1. Mutazione dei valori di campo  
p: 10, 12

2. Stringa  
p: punto (x = 10, y = 12)

3. Rappresentanza  
Punto (x = 10, y = 12) 

4. Sizeof  
dimensione di p: 64 

5. Accesso per nome del campo  
p: 10, 12

6. Accesso per indice  
p: 10, 12

7. Disimballaggio iterativo  
p: 10, 12

8. Iterazione  
p: [10, 12]

9. Ordinato Dict  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Sostituzione inplace (aggiornamento?)  
p: punto (x = 100, y = 200)

11. Pickle and Unpickle  
Sottaceto con successo

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

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

L'unica differenza con Python 3.5 è che namedlistè diventato più piccolo, la dimensione è 56 (Python 2.7 riporta 64).

Nota che ho cambiato il tuo test 10 per la sostituzione sul posto. Il namedlistha un _replace()metodo che fa una copia, e che rende perfettamente senso per me perché l' namedtuplenella libreria standard si comporta allo stesso modo. La modifica della semantica del _replace()metodo sarebbe fonte di confusione. A mio parere, il _update()metodo dovrebbe essere utilizzato per gli aggiornamenti sul posto. O forse non sono riuscito a capire l'intento del tuo test 10?


C'è una sfumatura importante. I namedlistvalori del negozio nell'istanza dell'elenco. Il fatto è che cpythons' listè in realtà un array dinamico. In base alla progettazione, alloca più memoria del necessario per rendere più economica la mutazione dell'elenco.
intellimath

1
@intellimath namedlist è un po 'improprio. In realtà non eredita liste per impostazione predefinita utilizza l' __slots__ottimizzazione. Quando ho misurato, l'utilizzo della memoria era inferiore a recordclass: 96 byte contro 104 byte per sei campi su Python 2.7
GrantJ

@GrantJ Sì. recorclassutilizza più memoria perché è un tupleoggetto simile a una dimensione di memoria variabile.
intellimath

2
I voti negativi anonimi non aiutano nessuno. Cosa c'è di sbagliato nella risposta? Perché il voto negativo?
Ali

Amo la sicurezza contro gli errori di battitura che fornisce rispetto a types.SimpleNamespace. Sfortunatamente, a pylint non piace :-(
xverges

23

Sembra che la risposta a questa domanda sia no.

Di seguito è abbastanza vicino, ma non è tecnicamente modificabile. Questo sta creando una nuova namedtuple()istanza con un valore x aggiornato:

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

D'altra parte, puoi creare una classe semplice utilizzando __slots__che dovrebbe funzionare bene per l'aggiornamento frequente degli attributi di istanza di classe:

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

Per aggiungere a questa risposta, penso che __slots__sia un buon uso qui perché è efficiente in termini di memoria quando crei molte istanze di classe. L'unico svantaggio è che non puoi creare nuovi attributi di classe.

Ecco un thread rilevante che illustra l'efficienza della memoria - Dictionary vs Object - che è più efficiente e perché?

Il contenuto citato nella risposta di questo thread è una spiegazione molto succinta perché __slots__è più efficiente in termini di memoria: gli slot Python


1
Vicino, ma goffo. Diciamo che volevo fare un compito + =, quindi dovrei fare: p._replace (x = px + 10) vs px + = 10
Alexander

1
sì, non sta realmente cambiando la tupla esistente, sta creando una nuova istanza
kennes

7

Quanto segue è una buona soluzione per Python 3: Una classe minimale di __slots__e Sequenceclasse di base astratta; non fa il rilevamento di errori di fantasia o simili, ma funziona e si comporta principalmente come una tupla mutabile (tranne che per il controllo dei tipi).

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

Esempio:

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

Se vuoi, puoi avere anche un metodo per creare la classe (anche se usare una classe esplicita è più trasparente):

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

Esempio:

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

In Python 2 è necessario regolarlo leggermente: se si eredita da Sequence, la classe avrà un__dict__ e __slots__smetterà di funzionare.

La soluzione in Python 2 è non ereditare da Sequence, ma object. Se lo isinstance(Point, Sequence) == Truesi desidera, è necessario registrare NamedMutableSequencecome classe base per Sequence:

Sequence.register(NamedMutableSequence)

3

Implementiamolo con la creazione di tipi dinamici:

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)

Questo controlla gli attributi per vedere se sono validi prima di consentire il proseguimento dell'operazione.

Quindi questo è selezionabile? Sì se (e solo se) fai quanto segue:

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

La definizione deve essere nel tuo spazio dei nomi e deve esistere abbastanza a lungo da consentire a pickle di trovarla. Quindi, se lo definisci nel tuo pacchetto, dovrebbe funzionare.

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

Pickle fallirà se esegui le seguenti operazioni o rendi temporanea la definizione (esce dall'ambito quando la funzione termina, ad esempio):

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

E sì, conserva l'ordine dei campi elencati nella creazione del tipo.


Se aggiungi un __iter__metodo con for k in self._attrs_: yield getattr(self, k), questo supporterà lo spacchettamento come una tupla.
snapshoe

È anche abbastanza facile aggiungere __len__, __getitem__e __setiem__metodi per supportare l'ottenimento di valus per indice, come p[0]. Con questi ultimi bit, questa sembra la risposta più completa e corretta (a me comunque).
snapshoe

__len__e __iter__sono bravi. __getitem__e __setitem__può davvero essere mappato su self.__dict__.__setitem__eself.__dict__.__getitem__
MadMan2064

2

Le tuple sono per definizione immutabili.

È tuttavia possibile creare una sottoclasse di dizionario in cui è possibile accedere agli attributi con notazione a punti;

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}

2

Se vuoi un comportamento simile a namedtuples ma mutevole, prova namedlist

Nota che per essere modificabile non può essere una tupla.


Grazie per il collegamento. Questo sembra il più vicino finora, ma devo valutarlo in modo più dettagliato. A proposito, sono totalmente consapevole che le tuple sono immutabili, motivo per cui sto cercando una soluzione come namedtuple.
Alexander

0

A condizione che le prestazioni siano di poca importanza, si potrebbe usare un trucco stupido come:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])

1
Questa risposta non è spiegata molto bene. Sembra confuso se non si comprende la natura mutevole degli elenchi. --- In questo esempio ... per riassegnare z, devi chiamare mutable_z.z.pop(0)quindi mutable_z.z.append(new_value). Se sbagli, ti ritroverai con più di 1 elemento e il tuo programma si comporterà in modo imprevisto.
byxor

1
@byxor che, o si può solo: mutable_z.z[0] = newValue. È davvero un trucco, come affermato.
Srg

Oh sì, sono sorpreso di aver perso il modo più ovvio per riassegnarlo.
byxor

Mi piace, vero hack.
WebOrCode
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.