Existenz eines veränderlichen benannten Tupels in Python?

120

Kann jemand namedtuple ändern oder eine alternative Klasse bereitstellen, damit sie für veränderbare Objekte funktioniert?

In erster Linie aus Gründen der Lesbarkeit möchte ich etwas Ähnliches wie namedtuple, das dies tut:

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)

Es muss möglich sein, das resultierende Objekt zu beizen. Und gemäß den Merkmalen des benannten Tupels muss die Reihenfolge der Ausgabe bei der Darstellung mit der Reihenfolge der Parameterliste beim Erstellen des Objekts übereinstimmen.

Alexander
quelle
3
Siehe auch: stackoverflow.com/q/5131044 . Gibt es einen Grund, warum Sie nicht einfach ein Wörterbuch verwenden können?
Senshin
@senshin Danke für den Link. Ich bevorzuge es, aus dem darin genannten Grund kein Wörterbuch zu verwenden. Diese Antwort ist auch mit code.activestate.com/recipes/… verknüpft , was ziemlich nahe an dem liegt, wonach ich suche.
Alexander
Anders als bei namedtuples, es scheint , dass Sie keine Notwendigkeit, die Attribute von Index verweisen zu können, das heißt so p[0]und p[1]würde alternative Möglichkeiten zur Referenz sein xund yjeweils richtig?
Martineau
Idealerweise ja, indizierbar nach Position wie ein einfaches Tupel zusätzlich zum Namen und entpackt wie ein Tupel. Dieses ActiveState-Rezept ist nah, aber ich glaube, es verwendet ein reguläres Wörterbuch anstelle eines OrderedDict. code.activestate.com/recipes/500261
Alexander
2
Ein veränderbares namens-Tupel wird als Klasse bezeichnet.
Gbtimmon

Antworten:

131

Es gibt eine veränderbare Alternative zu collections.namedtuple- recordclass .

Es hat die gleiche API und den gleichen Speicherbedarf wie namedtupleund unterstützt Zuweisungen (es sollte auch schneller sein). Beispielsweise:

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)

Für Python 3.6 und höher recordclass(seit 0.5) werden folgende Schreibweisen unterstützt:

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)

Es gibt ein vollständigeres Beispiel (es enthält auch Leistungsvergleiche).

Seit 0.9 recordclassbietet die Bibliothek eine weitere Variante - die recordclass.structclassFactory-Funktion. Es kann Klassen erzeugen, deren Instanzen weniger Speicher belegen als __slots__Instanzen auf Basis. Dies kann für Instanzen mit Attributwerten wichtig sein, für die keine Referenzzyklen vorgesehen sind. Dies kann dazu beitragen, die Speichernutzung zu reduzieren, wenn Sie Millionen von Instanzen erstellen müssen. Hier ist ein anschauliches Beispiel .

Intellimath
quelle
4
Mag ich. "Diese Bibliothek ist tatsächlich ein" Proof of Concept "für das Problem der" veränderlichen "Alternative des benannten Tupels."
Alexander
1
recordclassist langsamer, benötigt mehr Speicher und erfordert C-Erweiterungen im Vergleich zu Antti Haapalas Rezept und namedlist.
GrantJ
recordclassist eine veränderbare Version collection.namedtuple, die die API und den Speicherbedarf erbt, aber Zuweisungen unterstützt. namedlistist eigentlich eine Instanz der Python-Klasse mit Slots. Es ist nützlicher, wenn Sie keinen schnellen Zugriff auf die Felder per Index benötigen.
Intellimath
Der Attributzugriff zum recordclassBeispiel (Python 3.5.2) ist ungefähr 2-3% langsamer als fürnamedlist
Intellimath
Bei Verwendung einer namedtupleeinfachen Klassenerstellung Point = namedtuple('Point', 'x y')kann Jedi Attribute automatisch vervollständigen, während dies nicht der Fall ist recordclass. Wenn ich den längeren Erstellungscode (basierend auf RecordClass) verwende, versteht Jedi die PointKlasse, aber nicht ihren Konstruktor oder ihre Attribute ... Gibt es eine Möglichkeit, recordclassgut mit Jedi zusammenzuarbeiten?
PhilMacKay
34

types.SimpleNamespace wurde in Python 3.3 eingeführt und unterstützt die angeforderten Anforderungen.

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)
Funky-Zukunft
quelle
1
Ich habe seit Jahren nach so etwas gesucht. Großartiger Ersatz für eine gepunktete Diktatbibliothek wie dotmap
axwell
1
Dies erfordert mehr Upvotes. Es ist genau das, wonach das OP gesucht hat, es befindet sich in der Standardbibliothek und es könnte nicht einfacher zu bedienen sein. Vielen Dank!
Tom Zych
3
-1 Das OP machte mit seinen Tests sehr deutlich, was er benötigt, und SimpleNamespacescheitert an den Tests 6-10 (Zugriff per Index, iteratives Entpacken, Iteration, geordnetes Diktat, Ersetzen vor Ort) und 12, 13 (Felder, Slots). Beachten Sie, dass in der Dokumentation (die Sie in der Antwort verlinkt haben) ausdrücklich steht: " SimpleNamespaceKann als Ersatz für nützlich sein class NS: pass. Verwenden Sie namedtuple()stattdessen stattdessen einen strukturierten Datensatztyp ."
Ali
1
-1 SimpleNamespaceerstellt ebenfalls ein Objekt, keinen Klassenkonstruktor, und kann kein Ersatz für namedtuple sein. Der Typvergleich funktioniert nicht und der Speicherbedarf ist viel höher.
RedGlyph
26

Als sehr pythonische Alternative für diese Aufgabe können Sie seit Python-3.7 ein dataclassesModul verwenden, das sich nicht nur wie eine veränderbare verhält, NamedTupleda sie normale Klassendefinitionen verwenden, sondern auch andere Klassenfunktionen unterstützen.

Aus PEP-0557:

Obwohl sie einen ganz anderen Mechanismus verwenden, können Datenklassen als "veränderbare benannte Tupel mit Standardwerten" betrachtet werden. Da Datenklassen die normale Syntax der Klassendefinition verwenden, können Sie Vererbung, Metaklassen, Dokumentzeichenfolgen, benutzerdefinierte Methoden, Klassenfabriken und andere Python-Klassenfunktionen verwenden.

Es wird ein Klassendekorator bereitgestellt, der eine Klassendefinition auf Variablen mit Typanmerkungen überprüft, wie in PEP 526 , "Syntax für Variablenanmerkungen" definiert. In diesem Dokument werden solche Variablen als Felder bezeichnet. Mithilfe dieser Felder fügt der Dekorateur der Klasse generierte Methodendefinitionen hinzu, um die Instanzinitialisierung, eine Repr, Vergleichsmethoden und optional andere Methoden zu unterstützen, wie im Abschnitt Spezifikation beschrieben . Eine solche Klasse wird als Datenklasse bezeichnet, aber die Klasse hat wirklich nichts Besonderes: Der Dekorateur fügt der Klasse generierte Methoden hinzu und gibt dieselbe Klasse zurück, die sie erhalten hat.

Diese Funktion wird in PEP-0557 eingeführt. Weitere Informationen finden Sie unter dem bereitgestellten Dokumentationslink.

Beispiel:

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)
Kasramvd
quelle
1
Mit den Tests im OP wurde sehr deutlich gemacht, was in den dataclassTests 6-10 (Zugriff per Index, iteratives Entpacken, Iteration, geordnetes Diktat, Ersetzen an Ort und Stelle) und 12, 13 (Felder, Slots) in Python 3.7 erforderlich ist und was nicht .1.
Ali
1
Obwohl dies möglicherweise nicht genau das ist, wonach das OP gesucht hat, hat es mir sicherlich geholfen :)
Martin CR
25

Die neueste Namedlist 1.7 besteht alle Ihre Tests mit Python 2.7 und Python 3.5 ab dem 11. Januar 2016. Es handelt sich um eine reine Python-Implementierung, während recordclasses sich um eine C-Erweiterung handelt. Natürlich hängt es von Ihren Anforderungen ab, ob eine C-Erweiterung bevorzugt wird oder nicht.

Ihre Tests (siehe aber auch den Hinweis unten):

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

Ausgabe unter Python 2.7

1. Mutation von Feldwerten  
p: 10, 12

2. String  
p: Punkt (x = 10, y = 12)

3. Vertretung  
Punkt (x = 10, y = 12) 

4. Größe von  
Größe von p: 64 

5. Zugriff über Feldname  
p: 10, 12

6. Zugriff per Index  
p: 10, 12

7. Iteratives Auspacken  
p: 10, 12

8. Iteration  
p: [10, 12]

9. Bestelltes Diktat  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Ersatz ersetzen (Update?)  
p: Punkt (x = 100, y = 200)

11. Beizen und Lösen  
Erfolgreich eingelegt

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

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

Der einzige Unterschied zu Python 3.5 besteht darin, dass die namedlistGröße kleiner geworden ist und 56 beträgt (Python 2.7 meldet 64).

Beachten Sie, dass ich Ihren Test 10 für den direkten Austausch geändert habe. Das namedlisthat eine _replace()Methode, die eine flache Kopie erstellt, und das ist für mich vollkommen sinnvoll, da sich das namedtuplein der Standardbibliothek genauso verhält. Das Ändern der Semantik der _replace()Methode wäre verwirrend. Meiner Meinung nach sollte die _update()Methode für In-Place-Updates verwendet werden. Oder habe ich die Absicht Ihres Tests 10 nicht verstanden?

Ali
quelle
Es gibt wichtige Nuancen. Die namedlistSpeicherwerte in der Listeninstanz. Die Sache ist , dass cpython‚s listist eigentlich ein dynamisches Array. Durch das Design wird mehr Speicher als erforderlich zugewiesen, um die Mutation der Liste billiger zu machen.
Intellimath
1
@intellimath namedlist ist ein bisschen falsch. Es erbt nicht von listund verwendet standardmäßig die __slots__Optimierung. Als ich gemessen habe, war die Speichernutzung geringer als recordclass: 96 Bytes gegenüber 104 Bytes für sechs Felder in Python 2.7
GrantJ
@GrantJ Ja. recorclassverwendet mehr Speicher, da es sich um ein tupleähnliches Objekt mit variabler Speichergröße handelt.
Intellimath
2
Anonyme Abstimmungen helfen niemandem. Was ist falsch an der Antwort? Warum das Downvote?
Ali
Ich liebe die Sicherheit gegen Tippfehler, die es in Bezug auf bietet types.SimpleNamespace. Leider mag es Pylint nicht :-(
xverges
23

Die Antwort auf diese Frage scheint nein zu sein.

Unten ist ziemlich nah, aber es ist technisch nicht veränderlich. Dadurch wird eine neue namedtuple()Instanz mit einem aktualisierten x-Wert erstellt:

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

Auf der anderen Seite können Sie eine einfache Klasse erstellen, mit der sich Klasseninstanzattribute __slots__häufig aktualisieren lassen:

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

Um diese Antwort zu ergänzen, ist sie meiner Meinung nach __slots__hier nützlich, da sie speichereffizient ist, wenn Sie viele Klasseninstanzen erstellen. Der einzige Nachteil ist, dass Sie keine neuen Klassenattribute erstellen können.

Hier ist ein relevanter Thread, der die Speichereffizienz veranschaulicht - Dictionary vs Object - der effizienter ist und warum?

Der zitierte Inhalt in der Antwort dieses Threads ist eine sehr prägnante Erklärung, warum __slots__speichereffizienter ist - Python-Slots

kennes
quelle
1
Nah, aber klobig. Nehmen wir an, ich wollte eine + = Aufgabe machen, dann müsste ich: p._replace (x = px + 10) vs. px + = 10
Alexander
1
Ja, es ändert nicht wirklich das vorhandene Tupel, es erstellt eine neue Instanz
Kennes
7

Im Folgenden ist eine gute Lösung für Python 3: Eine minimale Klasse mit __slots__und Sequenceabstrakte Basisklasse; macht keine ausgefallene Fehlererkennung oder ähnliches, aber es funktioniert und verhält sich meistens wie ein veränderliches Tupel (außer 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')

Beispiel:

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

Wenn Sie möchten, können Sie auch eine Methode zum Erstellen der Klasse verwenden (obwohl die Verwendung einer expliziten Klasse transparenter ist):

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

Beispiel:

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

In Python 2 müssen Sie es leicht anpassen - wenn Sie von erben Sequence, hat die Klasse ein__dict__ und das __slots__funktioniert nicht mehr.

Die Lösung in Python 2 besteht darin, nicht von zu erben Sequence, sondern object. Falls isinstance(Point, Sequence) == Truegewünscht, müssen Sie die NamedMutableSequenceals Basisklasse registrieren, um Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
quelle
3

Lassen Sie uns dies mit dynamischer Typerstellung implementieren:

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)

Dadurch werden die Attribute überprüft, um festzustellen, ob sie gültig sind, bevor der Vorgang fortgesetzt werden kann.

Also ist das pickleable? Ja, wenn (und nur wenn) Sie Folgendes tun:

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

Die Definition muss sich in Ihrem Namespace befinden und lange genug vorhanden sein, damit pickle sie finden kann. Wenn Sie dies so definieren, dass es in Ihrem Paket enthalten ist, sollte es funktionieren.

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

Pickle schlägt fehl, wenn Sie Folgendes tun oder die Definition temporär machen (z. B. außerhalb des Gültigkeitsbereichs, wenn die Funktion endet):

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

Und ja, die Reihenfolge der in der Typerstellung aufgeführten Felder bleibt erhalten.

MadMan2064
quelle
Wenn Sie eine __iter__Methode mit hinzufügen for k in self._attrs_: yield getattr(self, k), wird das Entpacken wie ein Tupel unterstützt.
Snapshoe
Es ist auch recht einfach zu addieren __len__, __getitem__und __setiem__Methoden immer valus von Index zu unterstützen, wie p[0]. Mit diesen letzten Bits scheint dies die vollständigste und korrekteste Antwort zu sein (für mich jedenfalls).
Snapshoe
__len__und __iter__sind gut. __getitem__und __setitem__kann wirklich zugeordnet werden self.__dict__.__setitem__undself.__dict__.__getitem__
MadMan2064
2

Tupel sind per Definition unveränderlich.

Sie können jedoch eine Wörterbuchunterklasse erstellen, in der Sie mit Punktnotation auf die Attribute zugreifen können.

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

Wenn Sie ein ähnliches Verhalten wie namedtuples, aber veränderlich möchten, versuchen Sie es mit namedlist

Beachten Sie, dass es kein Tupel sein kann , um veränderlich zu sein .

Agomcas
quelle
Danke für den Link. Dies scheint das bisher am nächsten liegende zu sein, aber ich muss es genauer bewerten. Übrigens bin ich mir völlig bewusst, dass Tupel unveränderlich sind, weshalb ich nach einer Lösung wie namedtuple suche.
Alexander
0

Vorausgesetzt, die Leistung ist von geringer Bedeutung, könnte man einen dummen Hack verwenden wie:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
quelle
1
Diese Antwort ist nicht sehr gut erklärt. Es sieht verwirrend aus, wenn Sie die Veränderlichkeit von Listen nicht verstehen. --- In diesem Beispiel ... zu re-assign z, müssen Sie rufen mutable_z.z.pop(0)dann mutable_z.z.append(new_value). Wenn Sie dies falsch verstehen, erhalten Sie mehr als ein Element und Ihr Programm verhält sich unerwartet.
bis zum
1
@byxor das, oder du könntest einfach : mutable_z.z[0] = newValue. Es ist in der Tat ein Hack, wie gesagt.
Srg
Oh ja, ich bin überrascht, dass ich den offensichtlicheren Weg verpasst habe, es neu zuzuweisen.
Byxor
Ich mag es, echter Hack.
WebOrCode