Adanya tuple bernama bisa berubah dengan Python?

121

Adakah yang bisa mengubah namedtuple atau menyediakan kelas alternatif agar berfungsi untuk objek yang bisa berubah?

Terutama untuk keterbacaan, saya ingin sesuatu yang mirip dengan nametuple yang melakukan ini:

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)

Itu harus memungkinkan untuk membuat acar objek yang dihasilkan. Dan sesuai dengan karakteristik bernama tuple, urutan output saat diwakili harus sesuai dengan urutan daftar parameter saat membuat objek.

Alexander
sumber
3
Lihat juga: stackoverflow.com/q/5131044 . Adakah alasan mengapa Anda tidak bisa begitu saja menggunakan kamus?
senshin
@senshin Terima kasih untuk tautannya. Saya lebih suka tidak menggunakan kamus karena alasan yang disebutkan di dalamnya. Tanggapan itu juga terkait dengan code.activestate.com/recipes/… , yang cukup dekat dengan apa yang saya cari.
Alexander
Tidak seperti namedtuples, tampaknya Anda tidak perlu mereferensikan atribut dengan indeks, yaitu jadi p[0]dan p[1]akan menjadi cara alternatif untuk mereferensikan xdan ymasing - masing, benar?
martineau
Idealnya, ya, dapat diindeks berdasarkan posisi seperti tupel biasa selain nama, dan dibongkar seperti tupel. Resep ActiveState ini sudah dekat, tapi saya yakin ini menggunakan kamus biasa, bukan OrderedDict. code.activestate.com/recipes/500261
Alexander
2
Sebuah nametuple bisa berubah disebut kelas.
gbtimmon

Jawaban:

132

Ada alternatif yang bisa berubah untuk collections.namedtuple- recordclass .

Ini memiliki API dan footprint memori yang sama namedtupledan mendukung penugasan (Seharusnya lebih cepat juga). Sebagai contoh:

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)

Untuk python 3.6 dan lebih tinggi recordclass(sejak 0.5) mendukung jenis petunjuk:

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)

Ada contoh yang lebih lengkap (ini juga termasuk perbandingan kinerja).

Sejak 0.9 recordclassperpustakaan menyediakan varian lain - recordclass.structclassfungsi pabrik. Ini dapat menghasilkan kelas, yang instansinya menempati lebih sedikit memori daripada __slots__instans berbasis. Ini dapat menjadi penting untuk instance dengan nilai atribut, yang tidak dimaksudkan untuk memiliki siklus referensi. Ini dapat membantu mengurangi penggunaan memori jika Anda perlu membuat jutaan instance. Berikut adalah contoh ilustrasi .

intellimath
sumber
4
Suka itu. 'Perpustakaan ini sebenarnya adalah "bukti konsep" untuk masalah alternatif yang "bisa berubah" dari nama tuple.`
Alexander
1
recordclasslebih lambat, membutuhkan lebih banyak memori, dan membutuhkan ekstensi-C dibandingkan dengan resep Antti Haapala dan namedlist.
GrantJ
recordclassadalah versi yang bisa berubah collection.namedtupleyang mewarisi api, jejak memori, tetapi tugas dukungan. namedlistsebenarnya adalah turunan dari kelas python dengan slot. Ini lebih berguna jika Anda tidak membutuhkan akses cepat ke bidangnya dengan indeks.
intellimath
Akses atribut recordclassmisalnya (python 3.5.2) sekitar 2-3% lebih lambat daripada untuknamedlist
intellimath
Saat menggunakan namedtupledan pembuatan kelas sederhana Point = namedtuple('Point', 'x y'), Jedi dapat melengkapi atribut secara otomatis, sementara ini bukan kasusnya recordclass. Jika saya menggunakan kode pembuatan yang lebih panjang (berdasarkan RecordClass), maka Jedi memahami Pointkelasnya, tetapi bukan konstruktor atau atributnya ... Apakah ada cara recordclassuntuk bekerja dengan baik dengan Jedi?
PhilMacKay
34

types.SimpleNamespace diperkenalkan dengan Python 3.3 dan mendukung persyaratan yang diminta.

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)
masa depan yang funky
sumber
1
Saya sudah mencari sesuatu seperti ini selama bertahun-tahun. Pengganti yang bagus untuk pustaka dict bertitik seperti dotmap
axwell
1
Ini membutuhkan lebih banyak suara positif. Ini persis seperti yang dicari OP, ada di pustaka standar, dan sangat mudah untuk digunakan. Terima kasih!
Tom Zych
3
-1 OP membuatnya sangat jelas dengan tesnya apa yang dia butuhkan dan SimpleNamespacegagal tes 6-10 (akses oleh indeks, pembongkaran berulang, iterasi, dikt yang dipesan, penggantian di tempat) dan 12, 13 (bidang, slot). Perhatikan bahwa dokumentasi (yang Anda tautkan dalam jawaban) secara khusus mengatakan " SimpleNamespacemungkin berguna sebagai pengganti class NS: pass. Namun, untuk penggunaan jenis rekaman terstruktur namedtuple()sebagai gantinya."
Ali
1
-1 juga, SimpleNamespacemembuat objek, bukan konstruktor kelas, dan tidak bisa menjadi pengganti nametuple. Perbandingan jenis tidak akan berfungsi, dan jejak memori akan jauh lebih tinggi.
RedGlyph
26

Sebagai alternatif yang sangat Pythonic untuk tugas ini, sejak Python-3.7, Anda dapat menggunakan dataclassesmodul yang tidak hanya berperilaku seperti mutable NamedTuplekarena mereka menggunakan definisi kelas normal, tetapi juga mendukung fitur kelas lainnya.

Dari PEP-0557:

Meskipun mereka menggunakan mekanisme yang sangat berbeda, Kelas Data dapat dianggap sebagai "tupel bernama yang dapat berubah dengan default". Karena Kelas Data menggunakan sintaks definisi kelas normal, Anda bebas menggunakan pewarisan, metaclass, docstrings, metode yang ditentukan pengguna, pabrik kelas, dan fitur kelas Python lainnya.

Dekorator kelas disediakan yang memeriksa definisi kelas untuk variabel dengan penjelasan jenis seperti yang didefinisikan dalam PEP 526 , "Sintaks untuk Penjelasan Variabel". Dalam dokumen ini, variabel seperti itu disebut bidang. Dengan menggunakan kolom ini, dekorator menambahkan definisi metode yang dihasilkan ke kelas untuk mendukung inisialisasi instance, repr, metode perbandingan, dan metode opsional lainnya seperti yang dijelaskan di bagian Spesifikasi . Kelas seperti itu disebut Kelas Data, tetapi sebenarnya tidak ada yang istimewa tentang kelas tersebut: dekorator menambahkan metode yang dihasilkan ke kelas dan mengembalikan kelas yang sama dengan yang diberikan.

Fitur ini diperkenalkan di PEP-0557 yang dapat Anda baca lebih lanjut di tautan dokumentasi yang disediakan.

Contoh:

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
sumber
1
Itu dibuat sangat jelas dengan tes di OP apa yang dibutuhkan dan dataclassgagal tes 6-10 (akses oleh indeks, pembongkaran berulang, iterasi, dikt yang dipesan, penggantian di tempat) dan 12, 13 (bidang, slot) dengan Python 3.7 .1.
Ali
1
meskipun ini mungkin tidak secara spesifik seperti yang dicari OP, itu pasti membantu saya :)
Martin CR
25

Terbaru namedlist 1,7 melewati semua tes Anda dengan baik Python 2.7 dan Python 3.5 pada 11 Jan 2016. Ini adalah implementasi python murni sedangkan recordclassadalah ekstensi C. Tentu saja, itu tergantung pada kebutuhan Anda apakah ekstensi C lebih disukai atau tidak.

Tes Anda (tetapi juga lihat catatan di bawah):

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 pada Python 2.7

1. Mutasi nilai bidang  
p: 10, 12

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

3. Representasi  
Titik (x = 10, y = 12) 

4. Ukuran  
ukuran p: 64 

5. Akses menurut nama bidang  
p: 10, 12

6. Akses berdasarkan indeks  
p: 10, 12

7. Pembongkaran berulang  
p: 10, 12

8. Iterasi  
p: [10, 12]

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

10. Penggantian di tempat (perbarui?)  
p: Titik (x = 100, y = 200)

11. Acar dan Unpickle  
Acar berhasil

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

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

Satu-satunya perbedaan dengan Python 3.5 adalah namedlistukurannya menjadi lebih kecil, ukurannya 56 (Python 2.7 melaporkan 64).

Perhatikan bahwa saya telah mengubah tes 10 Anda untuk penggantian di tempat. The namedlistmemiliki _replace()metode yang melakukan salinan dangkal, dan itu sangat masuk akal bagi saya karena namedtupledi perpustakaan standar berperilaku dengan cara yang sama. Mengubah semantik _replace()metode akan membingungkan. Menurut pendapat saya, _update()metode ini harus digunakan untuk pembaruan di tempat. Atau mungkin saya gagal memahami maksud dari tes 10 Anda?

Ali
sumber
Ada nuansa penting. Nilai namedlisttoko dalam contoh daftar. Masalahnya adalah bahwa cpython's listsebenarnya adalah array dinamis. Secara desain, itu mengalokasikan lebih banyak memori daripada yang diperlukan untuk membuat mutasi daftar lebih murah.
intellimath
1
@intellimath Namedlist adalah sedikit keliru. Ini sebenarnya tidak mewarisi dari listdan secara default menggunakan __slots__pengoptimalan. Ketika saya mengukur, penggunaan memori kurang dari recordclass: 96 byte vs 104 byte untuk enam bidang pada Python 2.7
GrantJ
@Bayu_joo recorclassmenggunakan lebih banyak memori karena ini adalah tupleobjek mirip dengan ukuran memori variabel.
intellimath
2
Suara negatif anonim tidak membantu siapa pun. Apa yang salah dengan jawabannya? Mengapa suara negatif itu?
Ali
Saya suka keamanan terhadap kesalahan ketik yang diberikannya sehubungan dengan itu types.SimpleNamespace. Sayangnya, pylint tidak menyukainya :-(
xverges
23

Sepertinya jawaban untuk pertanyaan ini adalah tidak.

Di bawah ini cukup dekat, tetapi secara teknis tidak bisa berubah. Ini membuat namedtuple()instance baru dengan nilai x yang diperbarui:

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

Di sisi lain, Anda dapat membuat kelas sederhana __slots__yang akan bekerja dengan baik untuk sering memperbarui atribut instance kelas:

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

Untuk menambah jawaban ini, menurut saya __slots__bagus digunakan di sini karena hemat memori ketika Anda membuat banyak instance kelas. Satu-satunya kekurangan adalah Anda tidak dapat membuat atribut kelas baru.

Berikut satu utas relevan yang menggambarkan efisiensi memori - Kamus vs Objek - mana yang lebih efisien dan mengapa?

Konten yang dikutip dalam jawaban utas ini adalah penjelasan yang sangat ringkas mengapa __slots__lebih hemat memori - slot Python

kennes
sumber
1
Dekat, tapi kikuk. Katakanlah saya ingin melakukan + = tugas, kemudian saya perlu melakukan: p._replace (x = px + 10) vs. px + = 10
Alexander
1
ya, ini tidak benar-benar mengubah tupel yang ada, ini membuat contoh baru
kennes
7

Berikut ini adalah solusi yang baik untuk Python 3: Kelas minimal yang menggunakan __slots__dan Sequencekelas dasar abstrak; tidak melakukan deteksi kesalahan mewah atau semacamnya, tetapi berfungsi, dan berperilaku sebagian besar seperti tupel yang bisa berubah (kecuali untuk pemeriksaan ketik).

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

Contoh:

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

Jika mau, Anda dapat memiliki metode untuk membuat kelas juga (meskipun menggunakan kelas eksplisit lebih transparan):

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

Contoh:

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

Dalam Python 2 Anda perlu menyesuaikannya sedikit - jika Anda mewarisi dari Sequence, kelas akan memiliki a__dict__ dan __slots__kemauan akan berhenti berfungsi.

Solusi dalam Python 2 adalah tidak mewarisi Sequence, tetapi object. Jika isinstance(Point, Sequence) == Truediinginkan, Anda perlu mendaftarkan NamedMutableSequencesebagai kelas dasar untuk Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
sumber
3

Mari terapkan ini dengan pembuatan tipe dinamis:

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)

Ini memeriksa atribut untuk melihat apakah mereka valid sebelum mengizinkan operasi dilanjutkan.

Jadi, apakah ini bisa dijadikan acar? Ya jika (dan hanya jika) Anda melakukan hal berikut:

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

Definisi tersebut harus ada dalam namespace Anda, dan harus ada cukup lama agar acar dapat menemukannya. Jadi jika Anda mendefinisikan ini sebagai paket Anda, itu harus berhasil.

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

Pickle akan gagal jika Anda melakukan hal berikut, atau membuat definisi sementara (keluar dari ruang lingkup saat fungsi berakhir, katakanlah):

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

Dan ya, itu mempertahankan urutan bidang yang terdaftar dalam pembuatan tipe.

MadMan2064
sumber
Jika Anda menambahkan __iter__metode dengan for k in self._attrs_: yield getattr(self, k), itu akan mendukung pembongkaran seperti tupel.
snapshoe
Ini juga cukup mudah untuk menambahkan __len__, __getitem__, dan __setiem__metode untuk mendukung mendapatkan valus dengan indeks, seperti p[0]. Dengan potongan terakhir ini, ini sepertinya jawaban yang paling lengkap dan benar (bagi saya).
snapshoe
__len__dan __iter__bagus. __getitem__dan __setitem__benar-benar dapat dipetakan ke self.__dict__.__setitem__danself.__dict__.__getitem__
MadMan2064
2

Tupel menurut definisi tidak dapat diubah.

Namun Anda dapat membuat subkelas kamus di mana Anda dapat mengakses atribut dengan notasi titik;

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

Jika Anda ingin perilaku serupa seperti namaiuple tetapi bisa berubah coba daftar nama

Perhatikan bahwa agar bisa berubah itu tidak bisa menjadi tupel.

agomcas
sumber
Terima kasih untuk tautannya. Sejauh ini sepertinya yang paling dekat, tetapi saya perlu mengevaluasinya secara lebih detail. Btw, saya benar-benar sadar tuple tidak dapat diubah, itulah sebabnya saya mencari solusi seperti namedtuple.
Alexander
0

Memberikan kinerja tidak terlalu penting, seseorang dapat menggunakan peretasan konyol seperti:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
sumber
1
Jawaban ini tidak dijelaskan dengan baik. Tampaknya membingungkan jika Anda tidak memahami sifat daftar yang bisa berubah. --- Dalam contoh ini ... untuk menetapkan kembali z, Anda harus menelepon mutable_z.z.pop(0)kemudian mutable_z.z.append(new_value). Jika Anda salah, Anda akan mendapatkan lebih dari 1 elemen dan program Anda akan berperilaku tidak terduga.
byxor
1
@byxor itu, atau Anda bisa saja: mutable_z.z[0] = newValue. Ini memang peretasan, seperti yang dinyatakan.
Srg
Oh ya, saya terkejut saya melewatkan cara yang lebih jelas untuk menetapkannya kembali.
byxor
Saya suka itu, retasan nyata.
WebOrCode