Python'da değişken isimli tuple var mı?


121

Herhangi biri, isimli tuple'ı değiştirebilir veya değiştirilebilir nesneler için çalışması için alternatif bir sınıf sağlayabilir mi?

Öncelikle okunabilirlik için, bunu yapan namedtuple'a benzer bir şey istiyorum:

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)

Ortaya çıkan nesneyi temizlemek mümkün olmalıdır. Ve adlandırılmış demetin özelliklerine göre, gösterildiğinde çıktının sıralaması, nesneyi oluştururken parametre listesinin sırasına uymalıdır.


3
Ayrıca bkz: stackoverflow.com/q/5131044 . Sözlük kullanmamanın bir nedeni var mı?
senshin

@senshin Bağlantı için teşekkürler. Ben sözlükte belirtilen sebepten dolayı kullanmamayı tercih ediyorum. Bu yanıt aynı zamanda peşinde olduğuma oldukça yakın olan code.activestate.com/recipes/… ile bağlantılıydı .
Alexander

S'den farklı olarak namedtuple, özniteliklere dizine göre atıfta bulunmanıza gerek yok gibi görünüyor, yani p[0]ve p[1]referans için alternatif yollar xve ysırasıyla, doğru mu?
martineau

İdeal olarak, evet, isme ek olarak düz bir demet gibi konuma göre dizine eklenebilir ve bir demet gibi paketten çıkarılabilir. Bu ActiveState tarifi yakındır, ancak OrderedDict yerine normal bir sözlük kullandığına inanıyorum. code.activestate.com/recipes/500261
Alexander

2
Değişken bir adlandırılmış çift, sınıf olarak adlandırılır.
gbtimmon

Yanıtlar:


133

Bir değişken alternatif yoktur collections.namedtuple- recordclass .

Aynı API ve bellek ayak izine sahiptir namedtupleve atamaları destekler (Ayrıca daha hızlı olmalıdır). Örneğin:

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)

Python 3.6 ve üstü için recordclass(0.5'ten beri) yazım ipuçlarını destekler:

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)

Daha eksiksiz bir örnek var (aynı zamanda performans karşılaştırmalarını da içeriyor).

0.9 recordclasskitaplığı başka bir varyant - recordclass.structclassfabrika işlevi sağladığından . Örnekleri __slots__tabanlı örneklerden daha az bellek kaplayan sınıflar üretebilir . Bu, referans döngüleri olması amaçlanmayan öznitelik değerlerine sahip örnekler için önemli olabilir. Milyonlarca örnek oluşturmanız gerekirse bellek kullanımını azaltmaya yardımcı olabilir. İşte açıklayıcı bir örnek .


4
Beğen onu. "Bu kütüphane aslında adlandırılmış tuple" değişken "alternatifi problemi için bir" kavram kanıtı "dır.
Alexander

1
recordclassdaha yavaş, daha bellek alır, ve benzeri gibi Cı-uzantıları gerektirir karşılaştırıldığında Antti Haapala tarifine ile namedlist.
GrantJ

recordclasscollection.namedtupleAPI'sini, bellek ayak izini devralan, ancak atamaları destekleyen değişken bir sürümüdür . namedlistaslında slotlu python sınıfının bir örneğidir. Alanlarına dizine göre hızlı erişime ihtiyacınız yoksa daha kullanışlıdır.
intellimath

recordclassÖrneğin öznitelik erişimi (python 3.5.2),namedlist
intellimath

Jedi, kullanırken namedtupleve basit sınıf oluştururken Point = namedtuple('Point', 'x y'), bu durum böyle olmasa da, öznitelikleri otomatik tamamlayabilir recordclass. Daha uzun yaratma kodunu kullanırsam (dayalı olarak RecordClass), o zaman Jedi Pointsınıfı anlar , ancak kurucusunu veya niteliklerini anlamaz ... recordclassJedi ile güzelce çalışmanın bir yolu var mı ?
PhilMacKay

34

types.SimpleNamespace , Python 3.3'te tanıtıldı ve istenen gereksinimleri destekler.

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
Yıllardır böyle bir şey arıyordum. Dotmap gibi noktalı bir dict kitaplığı için harika bir alternatif
axwell

1
Bunun daha fazla oylamaya ihtiyacı var. OP'nin tam olarak aradığı şey bu, standart kitaplıkta ve kullanımı daha kolay olamazdı. Teşekkürler!
Tom Zych

3
-1 OP, testlerinde neye ihtiyacı olduğunu ve SimpleNamespace6-10 (indekse göre erişim, yinelemeli paket açma, yineleme, sıralı dikte, yerinde değiştirme) ve 12, 13 (alanlar, yuvalar) testlerinde başarısız olduğunu çok net bir şekilde ortaya koydu . Dokümantasyonun (cevaba bağladığınız) özellikle " SimpleNamespacebunun yerine kullanılabilir class NS: pass. Ancak namedtuple()bunun yerine yapılandırılmış bir kayıt türü için kullanın"
Ali

1
-1 de SimpleNamespacebir sınıf kurucusu değil bir nesne oluşturur ve namedtuple'ın yerini alamaz. Tür karşılaştırması çalışmayacak ve bellek ayak izi çok daha yüksek olacaktır.
RedGlyph

26

Bu görev için oldukça Pythonic bir alternatif olarak, Python-3.7'den beri, normal sınıf tanımlarını kullandıklarından, diğer sınıf özelliklerini de destekledikleri için dataclassessadece değişken gibi davranmayan modülü NamedTuplekullanabilirsiniz.

PEP-0557'den:

Çok farklı bir mekanizma kullansalar da, Veri Sınıfları "öntanımlı değişken adlı çiftler" olarak düşünülebilir. Veri Sınıfları normal sınıf tanımı sözdizimini kullandığından, kalıtımı, meta sınıfları, docstrings'i, kullanıcı tanımlı yöntemleri, sınıf fabrikalarını ve diğer Python sınıfı özelliklerini kullanmakta özgürsünüz.

İçinde tanımlandığı gibi tür ek açıklamaları olan değişkenler için bir sınıf tanımını inceleyen bir sınıf dekoratörü sağlanır. PEP 526 , "Değişken Ek Açıklamaları için Sözdizimi" . Bu belgede, bu tür değişkenler alanlar olarak adlandırılır. Bu alanları kullanarak, dekoratör, örnek başlatma, repr, karşılaştırma yöntemleri ve isteğe bağlı olarak Spesifikasyon bölümünde açıklanan diğer yöntemleri desteklemek için oluşturulan yöntem tanımlarını sınıfa ekler . Böyle bir sınıfa Veri Sınıfı denir, ancak sınıfla ilgili özel bir şey yoktur: dekoratör, üretilen yöntemleri sınıfa ekler ve verildiği sınıfı döndürür.

Bu özellik, sağlanan dokümantasyon bağlantısında daha ayrıntılı olarak okuyabileceğiniz PEP-0557'de tanıtılmıştır .

Misal:

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
OP'de neyin gerekli olduğu ve dataclass6-10 (indeksle erişim, yinelemeli paket açma, yineleme, sıralı dikte, yerinde değiştirme) ve Python 3.7'deki 12, 13 (alanlar, yuvalar) testlerinde başarısız olan testler çok netleştirildi. 0,1.
Ali

1
Bu özellikle OP'nin aradığı şey olmasa da, kesinlikle bana yardımcı oldu :)
Martin CR

25

En son adlandırılmış liste 1.7, 11 Ocak 2016 itibariyle tüm testlerinizi hem Python 2.7 hem de Python 3.5 ile geçer . Bu saf bir python uygulaması iken recordclass, bir C uzantısıdır. Elbette, bir C uzantısının tercih edilip edilmeyeceği gereksinimlerinize bağlıdır.

Testleriniz (aynı zamanda aşağıdaki nota da bakın):

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

Python 2.7'de çıktı

1. Alan değerlerinin mutasyonu  
s: 10, 12

2. Dize  
p: Nokta (x = 10, y = 12)

3. Temsil  
Nokta (x = 10, y = 12) 

4. Sizeof  
p boyutu: 64 

5. Alan adına göre erişim  
s: 10, 12

6. Dizine göre erişim  
s: 10, 12

7. Yinelemeli ambalajdan çıkarma  
s: 10, 12

8. Yineleme  
s: [10, 12]

9. Sıralı Dikte  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Yerinde değiştirme (güncelleme?)  
p: Nokta (x = 100, y = 200)

11. Turşu ve Unpickle  
Başarıyla turşu

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

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

Python 3.5 ile tek fark, namedlistküçültülmüş boyutun 56 olmasıdır (Python 2.7 raporlar 64).

Yerinde değiştirme için 10 numaralı testinizi değiştirdiğimi unutmayın. ' Nin sığ bir kopya yapan namedlistbir _replace()yöntemi var ve bu benim için mükemmel bir anlam ifade ediyor çünkü namedtuplestandart kütüphanede aynı şekilde davranıyor. _replace()Yöntemin anlamını değiştirmek kafa karıştırıcı olacaktır. Bence _update()yöntem yerinde güncellemeler için kullanılmalıdır. Ya da belki 10 numaralı testinizin amacını anlayamadım?


Önemli bir nüans var. namedlistMağaza listesi örneğinde değer verir. Şey olduğunu cpython'ın listaslında bir dinamik dizidir. Tasarım gereği, listenin mutasyonunu daha ucuz hale getirmek için gerekenden daha fazla bellek ayırır.
intellimath

1
@intellimath namedlist biraz yanlış adlandırma. Aslında list, __slots__optimizasyondan devralmaz ve varsayılan olarak optimizasyonu kullanır . recordclass
Ölçtüğümde

@GrantJ Evet. değişken bellek boyutuna sahip benzer recorclassbir tuplenesne olduğundan daha fazla bellek kullanır .
intellimath

2
İsimsiz olumsuz oylar kimseye yardımcı olmuyor. Cevabın nesi yanlış? Neden olumsuz oy?
Ali

Sağladığı yazım hatalarına karşı güvenliği seviyorum types.SimpleNamespace. Maalesef pilint bundan hoşlanmıyor :-(
xverges

23

Görünüşe göre bu sorunun cevabı hayır.

Aşağıda oldukça yakındır, ancak teknik olarak değişken değildir. Bu, namedtuple()güncellenmiş bir x değerine sahip yeni bir örnek oluşturmaktır :

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

Öte yandan, __slots__sınıf örneği özniteliklerini sık sık güncellemek için iyi çalışması gereken basit bir sınıf oluşturabilirsiniz :

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

Bu yanıta eklemek için, __slots__burada iyi bir kullanım olduğunu düşünüyorum çünkü çok sayıda sınıf örneği oluşturduğunuzda bellek açısından verimli. Tek dezavantajı, yeni sınıf nitelikleri oluşturamamanızdır.

İşte bellek verimliliğini gösteren ilgili bir başlık - Sözlük vs Nesne - hangisi daha verimli ve neden?

Bu konunun cevabında alıntılanan içerik, neden __slots__daha verimli bellek olduğunu çok kısa ve öz bir açıklamadır - Python slotları


1
Yakın ama hantal. Diyelim ki bir + = ataması yapmak istedim, o zaman yapmam gerekecek: p._replace (x = px + 10) vs. px + = 10
Alexander

1
evet, mevcut demeti gerçekten değiştirmiyor, yeni bir örnek oluşturuyor
kennes

8

Aşağıdaki Python 3 için iyi bir çözümdür: Minimal bir sınıf kullanan __slots__ve Sequencesoyut temel sınıf; herhangi bir hata tespiti yapmaz, ancak çalışır ve çoğunlukla değiştirilebilir bir tuple gibi davranır (typecheck hariç).

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

Misal:

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

İsterseniz sınıfı oluşturmak için de bir yönteminiz olabilir (açık bir sınıf kullanmak daha şeffaf olsa da):

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

Misal:

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

Python 2'de hafifçe ayarlamanız gerekli - eğer devralan Sequence, sınıf bir olacak__dict__ ve __slots__işlenmesinden elde duracaktır.

Python 2'de çözüm devralan getirmemektir Sequenceama object. Eğer isinstance(Point, Sequence) == Trueistenirse, kayıt için gereken NamedMutableSequencebir temel sınıf olarak Sequence:

Sequence.register(NamedMutableSequence)

3

Bunu dinamik tür oluşturma ile uygulayalım:

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)

Bu, işlemin devam etmesine izin vermeden önce geçerli olup olmadıklarını görmek için öznitelikleri kontrol eder.

Yani bu turşulabilir mi? Evet, eğer (ve sadece) aşağıdakileri yaparsanız:

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

Tanım, ad alanınızda olmalı ve turşu'nun onu bulması için yeterince uzun olmalıdır. Yani bunu paketinizde olacak şekilde tanımlarsanız, çalışmalıdır.

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

Aşağıdakileri yaparsanız veya tanımı geçici hale getirirseniz Pickle başarısız olur (örneğin işlev bittiğinde kapsam dışına çıkar):

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

Ve evet, tür oluşturmada listelenen alanların sırasını korur.


İle bir __iter__yöntem eklerseniz for k in self._attrs_: yield getattr(self, k), bu bir demet gibi paket açmayı destekleyecektir.
snapshoe

Bu oldukça kolay eklemek de var __len__, __getitem__ve __setiem__benzeri, dizine göre elde valus desteklemek için yöntemler p[0]. Bu son bitlerle, bu en eksiksiz ve doğru cevap gibi görünüyor (yine de bana).
snapshoe

__len__ve __iter__iyidir. __getitem__ve __setitem__gerçekten eşlenebilir self.__dict__.__setitem__veself.__dict__.__getitem__
MadMan2064

2

Tuple'lar tanım gereği değişmezdir.

Bununla birlikte, özniteliklere noktalı gösterimle erişebileceğiniz bir sözlük alt sınıfı oluşturabilirsiniz;

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

Adlandırılmış tuples ile benzer davranış istiyorsanız, ancak değiştirilebilirse namedlist'i deneyin .

Değiştirilebilmesi için bir demet olamayacağını unutmayın .


Bağlantı için teşekkürler. Şimdiye kadarki en yakın gibi görünüyor ama daha detaylı değerlendirmem gerekiyor. Btw, tuple'ların değişmez olduğunun tamamen farkındayım, bu yüzden namedtuple gibi bir çözüm arıyorum.
Alexander

0

Sağlanan performans çok az önemliyse, aşağıdakiler gibi aptalca bir hack kullanılabilir:

from collection import namedtuple

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

1
Bu cevap çok iyi açıklanmadı. Listelerin değişken doğasını anlamıyorsanız kafa karıştırıcı görünüyor. --- Bu örnekte ... yeniden atama z, aramak zorunda mutable_z.z.pop(0)ardından mutable_z.z.append(new_value). Bunu yanlış anlarsanız, 1'den fazla elementle karşılaşırsınız ve programınız beklenmedik şekilde davranır.
byxor

1
@byxor öyle, ya da tekini: mutable_z.z[0] = newValue. Bu gerçekten de belirtildiği gibi bir hack.
Srg

Oh evet, onu yeniden atamanın daha açık bir yolunu kaçırdığıma şaşırdım.
byxor

Hoşuma gitti, gerçek hack.
WebOrCode
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.