Czy istnieje zmienna nazwana krotka w Pythonie?

121

Czy ktoś może zmienić namedtuple lub zapewnić alternatywną klasę, tak aby działała dla obiektów zmiennych?

Przede wszystkim ze względu na czytelność chciałbym coś podobnego do namedtuple, które robi to:

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)

Powstały przedmiot musi być możliwy do wytrawienia. Zgodnie z charakterystyką nazwanej krotki kolejność danych wyjściowych, gdy są reprezentowane, musi odpowiadać kolejności na liście parametrów podczas konstruowania obiektu.

Alexander
źródło
3
Zobacz też: stackoverflow.com/q/5131044 . Czy jest jakiś powód, dla którego nie możesz po prostu użyć słownika?
senshin
@senshin Dzięki za link. Wolę nie używać słownika z powodów w nim wskazanych. Ta odpowiedź również łączyła się z code.activestate.com/recipes/… , co jest bardzo bliskie temu, czego szukam.
Alexander
W przeciwieństwie do namedtuples, wydaje się, że nie ma potrzeby odwoływania się do atrybutów za pomocą indeksu, tj. Tak p[0]i p[1]czy byłyby alternatywne sposoby odwoływania się xi yodpowiednio, prawda?
martineau
Idealnie, tak, indeksowalny według pozycji, jak zwykła krotka oprócz nazwy, i rozpakowujący się jak krotka. Ten przepis ActiveState jest bliski, ale uważam, że używa zwykłego słownika zamiast OrderedDict. code.activestate.com/recipes/500261
Alexander
2
Zmienną nazwę namedtuple nazywamy klasą.
gbtimmon

Odpowiedzi:

132

Istnieje zmienna alternatywa dla collections.namedtuple- recordclass .

Ma to samo API i ślad pamięci, co namedtuplei obsługuje przypisania (powinno być również szybsze). Na przykład:

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)

Dla Pythona 3.6 i nowszych recordclass(od 0.5) obsługują wskazówki typu:

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)

Jest bardziej kompletny przykład (zawiera również porównania wydajności).

Od wersji 0.9 recordclassbiblioteka udostępnia inny wariant - recordclass.structclassfunkcję fabryczną. Może tworzyć klasy, których instancje zajmują mniej pamięci niż __slots__instancje oparte na. Może to być ważne w przypadku instancji z wartościami atrybutów, które nie miały mieć cykli odwołań. Może pomóc zmniejszyć zużycie pamięci, jeśli musisz utworzyć miliony instancji. Oto przykład ilustrujący .

intellimath
źródło
4
Lubię to. "Ta biblioteka jest faktycznie" dowodem słuszności koncepcji "dla problemu" zmiennej "alternatywy nazwanej krotki."
Alexander
1
recordclassjest wolniejszy, zajmuje więcej pamięci i wymaga rozszerzeń C w porównaniu z recepturą Antti Haapala i namedlist.
GrantJ
recordclassjest zmienną wersją tego, collection.namedtuplektóra dziedziczy swój interfejs API, ślad pamięci, ale obsługuje przypisania. namedlistjest w rzeczywistości instancją klasy Pythona z gniazdami. Jest to bardziej przydatne, jeśli nie potrzebujesz szybkiego dostępu do jego pól według indeksu.
intellimath
Na recordclassprzykład dostęp do atrybutów (python 3.5.2) jest około 2-3% wolniejszy niż w przypadkunamedlist
intellimath
Podczas korzystania z namedtupleprostego tworzenia klas Point = namedtuple('Point', 'x y')Jedi może automatycznie uzupełniać atrybuty, podczas gdy nie jest to prawdą recordclass. Jeśli użyję dłuższego kodu tworzenia (na podstawie RecordClass), Jedi rozumie Pointklasę, ale nie rozumie jej konstruktora lub atrybuty ... Czy jest sposób, aby recordclassładnie pracować z Jedi?
PhilMacKay,
34

types.SimpleNamespace został wprowadzony w Pythonie 3.3 i obsługuje żądane wymagania.

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-future
źródło
1
Szukałem czegoś takiego od lat. Świetny zamiennik dla kropkowanej biblioteki dykt, takiej jak dotmap
axwell
1
To wymaga więcej głosów pozytywnych. Dokładnie tego szukał OP, znajduje się w standardowej bibliotece i nie może być prostszy w użyciu. Dzięki!
Tom Zych
3
-1 OP jasno określił swoimi testami, czego potrzebuje i SimpleNamespacenie zdaje testów 6-10 (dostęp przez indeks, iteracyjne rozpakowywanie, iteracja, uporządkowany dykt, zamiana na miejscu) i 12, 13 (pola, sloty). Zwróć uwagę, że dokumentacja (do której odsyłasz w odpowiedzi) wyraźnie mówi SimpleNamespacemoże być przydatna jako zamiennik class NS: pass. Jednak w przypadku rekordu strukturalnego użyj namedtuple()zamiast tego”.
Ali
1
-1 także SimpleNamespacetworzy obiekt, a nie konstruktor klasy, i nie może zastąpić namedtuple. Porównanie typów nie zadziała, a zużycie pamięci będzie znacznie większe.
RedGlyph
26

Jako bardzo Pythonowa alternatywa dla tego zadania, od czasu Python-3.7, możesz użyć dataclassesmodułu, który nie tylko zachowuje się jak zmienny, NamedTupleponieważ używa normalnych definicji klas, ale obsługuje również inne funkcje klas.

Od PEP-0557:

Chociaż używają one zupełnie innego mechanizmu, klasy danych można traktować jako „zmienne nazwane krotki z ustawieniami domyślnymi”. Ponieważ klasy danych używają normalnej składni definicji klas, możesz swobodnie używać dziedziczenia, metaklas, ciągów dokumentów, metod zdefiniowanych przez użytkownika, fabryk klas i innych funkcji klas Pythona.

Dostarczany jest dekorator klasy, który sprawdza definicję klasy pod kątem zmiennych z adnotacjami typu, jak zdefiniowano w PEP 526 , „Składnia adnotacji zmiennych”. W tym dokumencie takie zmienne nazywane są polami. Korzystając z tych pól, dekorator dodaje wygenerowane definicje metod do klasy w celu obsługi inicjalizacji wystąpienia, repr, metod porównawczych i opcjonalnie innych metod opisanych w specyfikacji sekcji . Taka klasa nazywa się klasą danych, ale tak naprawdę nie ma w niej nic specjalnego: dekorator dodaje wygenerowane metody do klasy i zwraca tę samą klasę, którą otrzymał.

Ta funkcja została wprowadzona w PEP-0557 , o której można przeczytać bardziej szczegółowo, korzystając z łącza do dokumentacji.

Przykład:

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

Próbny:

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
źródło
1
Z testów w OP jasno wynika, co jest potrzebne i dataclassnie przechodzi testów 6-10 (dostęp przez indeks, iteracyjne rozpakowywanie, iteracja, uporządkowany dykt, zamiana na miejscu) oraz 12, 13 (pola, gniazda) w Pythonie 3.7 .1.
Ali
1
chociaż może to nie być dokładnie to, czego szukał OP, to z pewnością mi pomogło :)
Martin CR
25

Najnowsza lista nazwana 1.7 przechodzi wszystkie testy zarówno z Pythonem 2.7, jak i Pythonem 3.5 na dzień 11 stycznia 2016 r. Jest to implementacja w czystym Pythonie, natomiast rozszerzenie recordclassjest w C. Oczywiście to zależy od Twoich wymagań, czy preferowane jest rozszerzenie C.

Twoje testy (ale zobacz też uwagę poniżej):

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

Dane wyjściowe w Pythonie 2.7

1. Mutacja wartości pól  
p: 10, 12

2. Ciąg  
p: Punkt (x = 10, y = 12)

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

4. Rozmiar  
rozmiar p: 64 

5. Dostęp według nazwy pola  
p: 10, 12

6. Dostęp według indeksu  
p: 10, 12

7. Rozpakowywanie iteracyjne  
p: 10, 12

8. Iteracja  
p: [10, 12]

9. Zamówiony dykt  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Wymiana w miejscu (aktualizacja?)  
p: Punkt (x = 100, y = 200)

11. Pickle and Unpickle  
Marynowane pomyślnie

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

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

Jedyną różnicą w stosunku do Pythona 3.5 jest to, że namedliststał się mniejszy, rozmiar wynosi 56 (Python 2.7 raportuje 64).

Zwróć uwagę, że zmieniłem Twój test 10 na wymianę na miejscu. namedlistMa _replace()metody, która robi kopię płytkie i że ma sens dla mnie, ponieważ namedtuplew bibliotece standardowej zachowuje się w ten sam sposób. Zmiana semantyki _replace()metody byłaby myląca. Moim zdaniem ta _update()metoda powinna być używana do aktualizacji w miejscu. A może nie zrozumiałem celu twojego testu 10?

Ali
źródło
Jest ważny niuans. Wartości namedlistprzechowywania w instancji listy. Chodzi o to, że cpythonjest listto właściwie tablica dynamiczna. Z założenia przydziela więcej pamięci niż to konieczne, aby uczynić mutację listy tańszą.
intellimath
1
@intellimath namedlist jest trochę myląca. W rzeczywistości nie dziedziczy listpo __slots__optymalizacji i domyślnie korzysta z niej . Kiedy recordclass
mierzyłem
@GrantJ Yes. recorclasszużywa więcej pamięci, ponieważ jest tuplepodobnym obiektem o zmiennej wielkości pamięci.
intellimath,
2
Anonimowe głosy przeciw nikomu nie pomagają. Co jest nie tak z odpowiedzią? Dlaczego głos przeciw?
Ali
Uwielbiam bezpieczeństwo przed literówkami, które zapewnia types.SimpleNamespace. Niestety, pylintowi się to nie podoba :-(
xverges
23

Wygląda na to, że odpowiedź na to pytanie brzmi: nie.

Poniżej jest dość blisko, ale technicznie nie można go zmienić. To jest tworzenie nowej namedtuple()instancji ze zaktualizowaną wartością x:

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

Z drugiej strony możesz utworzyć prostą klasę, __slots__która powinna dobrze działać w przypadku częstego aktualizowania atrybutów instancji klas:

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

Aby dodać do tej odpowiedzi, myślę, że __slots__jest to dobre użycie, ponieważ zapewnia wydajną pamięć podczas tworzenia wielu instancji klas. Jedynym minusem jest to, że nie możesz tworzyć nowych atrybutów klas.

Oto jeden istotny wątek, który ilustruje wydajność pamięci - słownik kontra obiekt - który jest bardziej wydajny i dlaczego?

Cytowana treść odpowiedzi w tym wątku jest bardzo zwięzłym wyjaśnieniem, dlaczego __slots__jest bardziej wydajna pamięć - sloty Pythona

Kennes
źródło
1
Blisko, ale niezgrabnie. Powiedzmy, że chciałem wykonać zadanie + =, musiałbym wtedy zrobić: p._replace (x = px + 10) vs. px + = 10
Alexander
1
tak, tak naprawdę nie zmienia istniejącej krotki, ale tworzy nową instancję
kennes
7

Oto dobre rozwiązanie dla Pythona 3: Minimalna klasa używająca __slots__i Sequenceabstrakcyjna klasa bazowa; nie ma ochoty na wykrywanie błędów lub coś podobnego, ale działa i zachowuje się głównie jak zmienna krotka (z wyjątkiem sprawdzania typu).

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

Przykład:

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

Jeśli chcesz, możesz mieć również metodę tworzenia klasy (chociaż użycie jawnej klasy jest bardziej przejrzyste):

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

Przykład:

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

W Pythonie 2 musisz nieco to zmienić - jeśli będziesz dziedziczyć z Sequence, klasa będzie miała a,__dict__ a __slots__przestanie działać.

Rozwiązaniem w Pythonie 2 nie jest dziedziczenie z Sequence, ale object. Jeśli isinstance(Point, Sequence) == Truechcesz, musisz zarejestrować NamedMutableSequencejako klasę bazową, aby Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
źródło
3

Zaimplementujmy to z dynamicznym tworzeniem typów:

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)

Sprawdza atrybuty, aby sprawdzić, czy są prawidłowe, zanim pozwolisz na kontynuację operacji.

Więc czy to jest marynowane? Tak, jeśli (i tylko wtedy) wykonasz następujące czynności:

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

Definicja musi znajdować się w Twojej przestrzeni nazw i musi istnieć wystarczająco długo, aby pikle ją znalazło. Więc jeśli zdefiniujesz to jako w swoim pakiecie, powinno działać.

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

Pickle nie powiedzie się, jeśli wykonasz następujące czynności lub sprawisz, że definicja będzie tymczasowa (na przykład wyjdzie poza zakres po zakończeniu funkcji):

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

I tak, zachowuje kolejność pól wymienionych przy tworzeniu typu.

MadMan2064
źródło
Jeśli dodasz __iter__metodę z for k in self._attrs_: yield getattr(self, k), będzie ona obsługiwać rozpakowywanie jak krotka.
snapshoe
Jest to również bardzo łatwo dodać __len__, __getitem__oraz __setiem__metod wspierania dostaniem valus przez indeks, jak p[0]. Przy tych ostatnich fragmentach wydaje się to najbardziej kompletną i poprawną odpowiedzią (w każdym razie dla mnie).
snapshoe
__len__i __iter__są dobre. __getitem__i __setitem__naprawdę może być odwzorowany na self.__dict__.__setitem__iself.__dict__.__getitem__
MadMan2064
2

Krotki są z definicji niezmienne.

Możesz jednak utworzyć podklasę słownika, w której będziesz mieć dostęp do atrybutów za pomocą notacji kropkowej;

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
źródło
2

Jeśli chcesz zachować podobne zachowanie jak namedtuples, ale zmienne, spróbuj namedlist

Zauważ, że aby być mutowalnym, nie może to być krotka.

agomcas
źródło
Dzięki za link. Jak dotąd wygląda to najbliżej, ale muszę to ocenić bardziej szczegółowo. Przy okazji, jestem całkowicie świadomy, że krotki są niezmienne, dlatego szukam rozwiązania takiego jak namedtuple.
Alexander
0

Pod warunkiem, że wydajność nie ma większego znaczenia, można użyć głupiego hacka, takiego jak:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
źródło
1
Ta odpowiedź nie jest dobrze wyjaśniona. Wydaje się to zagmatwane, jeśli nie rozumiesz zmiennego charakteru list. --- W tym przykładzie ... aby zmienić przypisanie z, musisz mutable_z.z.pop(0)wtedy zadzwonić mutable_z.z.append(new_value). Jeśli zrobisz to źle, otrzymasz więcej niż 1 element, a Twój program będzie zachowywał się nieoczekiwanie.
byxor
1
@byxor to, albo może po prostu: mutable_z.z[0] = newValue. Jak stwierdzono, rzeczywiście jest to hack.
Srg
O tak, jestem zaskoczony, że przegapiłem bardziej oczywisty sposób ponownego przypisania.
byxor
Podoba mi się, prawdziwy hack.
WebOrCode