Існування мутантного імені кортежу в Python?


121

Хтось може змінити nametuple або надати альтернативний клас, щоб він працював для змінних об'єктів?

В першу чергу для читабельності, я хотів би щось схоже на nametuple, що робить це:

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)

Має бути можливість підсолити отриманий предмет. І відповідно до характеристик названого кортежу, впорядкування виводу при поданні має відповідати порядку списку параметрів при побудові об'єкта.


3
Дивіться також: stackoverflow.com/q/5131044 . Чи є причина, що ви не можете просто використовувати словник?
сеншин

@senshin Дякую за посилання. Я вважаю за краще не використовувати словник з тієї причини, яка в ньому вказана. Ця відповідь також пов'язана з code.activestate.com/recipes/… , що досить близько до того, що я хочу.
Олександр

На відміну від namedtuples, здається, вам не потрібно мати можливість посилатись на атрибути за індексом, тобто так, p[0]і це p[1]були б альтернативні способи посилання xта yвідповідно правильні?
мартіно

В ідеалі, так, індексується за положенням, як звичайний кортеж на додаток до імені, і розпаковується як кортеж. Цей рецепт ActiveState близький, але я вважаю, що він використовує звичайний словник замість OrdersDict. code.activestate.com/recipes/500261
Олександр

2
Змінюваний ім'яtutu називається класом.
gbtimmon

Відповіді:


132

Існує змінна альтернатива collections.namedtuple- клас запису .

Він має той самий інтерфейс API та пам'яті, що namedtupleі він підтримує завдання (він також повинен бути швидшим). Наприклад:

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 і вище recordclass(з 0.5) типи підтримки:

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)

Є більш повний приклад (він також включає порівняння продуктивності).

Оскільки 0.9 recordclassбібліотека забезпечує ще один варіант - recordclass.structclassзаводську функцію. Він може створювати класи, екземпляри яких займають менше пам'яті, ніж __slots__екземпляри на основі. Це може бути важливим для примірників зі значеннями атрибутів, у яких не має бути еталонних циклів. Це може допомогти зменшити використання пам'яті, якщо вам потрібно створити мільйони екземплярів. Ось наочний приклад .


4
Люблю це. "Ця бібліотека насправді є" доказом концепції "для проблеми" змінної "альтернативи названого кортежу".
Олександр,

1
recordclassповільніше, займає більше пам’яті та потребує розширення C, порівняно з рецептом Антті Хаапали та namedlist.
GrantJ

recordclassце змінна версія, collection.namedtupleяка успадковує його api, слід пам'яті, але підтримують завдання. namedlistнасправді екземпляр класу python зі слотами. Це корисніше, якщо вам не потрібен швидкий доступ до його полів за індексом.
intellimath

recordclassНаприклад, доступ до атрибутів (python 3.5.2) приблизно на 2-3% повільніший, ніж дляnamedlist
intellimath

Використовуючи namedtupleта просте створення класів Point = namedtuple('Point', 'x y'), Jedi може автозаповнювати атрибути, хоча це не так recordclass. Якщо я використовую довший код створення (на основі RecordClass), то джедай розуміє Pointклас, але не його конструктор чи атрибути ... Чи є спосіб, щоб recordclassдобре працювати з джедаями?
PhilMacKay

34

types.SimpleNamespace було введено в Python 3.3 і підтримує запитувані вимоги.

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
Я щось подібне шукав роками. Велика заміна для пунктирною бібліотеки Dict як dotmap
Axwell

1
Для цього потрібно більше коштів. Це саме те, що шукав ОП, він знаходиться в стандартній бібліотеці, і це не може бути простішим у використанні. Дякую!
Том Зич

3
-1 ОП чітко дала зрозуміти, що йому потрібно, і SimpleNamespaceпровалить тести 6-10 (доступ за індексом, ітеративне розпакування, ітерація, впорядкований dict, заміщення на місці) та 12, 13 (поля, слоти). Зауважте, що в документації (з якою ви пов’язані у відповіді) конкретно сказано, що " SimpleNamespaceможе бути корисною заміною class NS: pass. Однак namedtuple()замість цього використовується структурований тип запису ".
Алі

1
-1 також SimpleNamespaceстворює об'єкт, а не конструктор класів, і не може бути заміною для nametuple. Порівняння типів не працюватиме, а слід пам’яті буде набагато вище.
RedGlyph

26

Будучи дуже пітонічною альтернативою для цього завдання, оскільки Python-3.7 ви можете використовувати dataclassesмодуль, який не тільки веде себе як мутаційний, NamedTupleоскільки використовує звичайні визначення класів, але також підтримує функції інших класів.

Від PEP-0557:

Хоча вони використовують зовсім інший механізм, Класи даних можуть розглядатися як "змінні назви з парними елементами". Оскільки в Класах даних використовується звичайний синтаксис визначення класу, ви можете користуватися успадкуванням, метакласами, документами, визначеними користувачем методами, фабриками класів та іншими функціями класу Python.

Надається декоратор класу, який перевіряє визначення класу для змінних з анотаціями типів, визначених у PEP 526 , "Синтаксис змінних анотацій". У цьому документі такі змінні називаються полями. Використовуючи ці поля, декоратор додає згенеровані визначення методу до класу для підтримки ініціалізації екземпляра, повторної копії, методів порівняння та необов'язково інших методів, як описано в розділі Специфікація . Такий клас називається класом даних, але насправді немає нічого особливого в класі: декоратор додає в клас згенеровані методи і повертає той самий клас, який був заданий.

Ця функція введена в PEP-0557, що ви можете прочитати про неї детальніше на наданому посиланні на документацію.

Приклад:

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

Демонстрація:

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
З тестами в ОП було зрозуміло, що потрібно, і dataclassтести 6 - 10 (доступ за індексом, ітераційне розпакування, ітерація, впорядкований dict, заміщення на місці) та 12, 13 (поля, слоти) на Python 3.7 .1.
Алі

1
хоча це не може конкретно те, що шукала ОП, це, безумовно, допомогло мені :)
Martin CR

25

Останній названий список 1.7 проходить усі ваші тести з Python 2.7 та Python 3.5 станом на 11 січня 2016 року. Це є чистою реалізацією python, тоді як recordclassце розширення C. Звичайно, від ваших вимог залежить, чи є кращим розширення C чи ні.

Ваші тести (але також див. Примітку нижче):

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

1. Мутація значень поля  
p: 10, 12

2. Рядок  
p: Точка (x = 10, y = 12)

3. Представництво  
Точка (x = 10, y = 12) 

4. Розмір  
розмір р: 64 

5. Доступ за назвою поля  
p: 10, 12

6. Доступ за індексом  
p: 10, 12

7. Ітераційне розпакування  
p: 10, 12

8. Ітерація  
p: [10, 12]

9. Упорядкований Дікт  
p: OrdersDict ([('x', 10), ('y', 12)])

10. Замінити заміну (оновлення?)  
p: Точка (x = 100, y = 200)

11. Соління і соління  
Маринований успішно

12. Поля  
p: ('x', 'y')

13. Слоти  
p: ('x', 'y')

Єдина різниця з Python 3.5 полягає в тому, що їх namedlistстало менше, розмір - 56 (Python 2.7 повідомляє 64).

Зауважте, що я змінив ваш тест 10 для заміни на місці. namedlistЄ _replace()метод , який робить неповну копію, і це має сенс для мене , тому що namedtupleв стандартній бібліотеці поводиться точно так само. Зміна семантики _replace()методу була б заплутаною. На мою думку, цей _update()метод слід використовувати для оновлень на місці. А може, я не зрозумів наміру вашого тесту 10?


Є важливий нюанс. Збереження namedlistзначень у екземплярі списку. Річ у тім, що cpython's list- це насправді динамічний масив. За задумом він виділяє більше пам’яті, ніж потрібно, щоб зробити мутацію списку дешевшою.
інтеллімат

1
@intellimath позначений список трохи помилковий. Він фактично не успадковує listі за замовчуванням використовує __slots__оптимізацію. Коли я вимірював, використання пам'яті було менше recordclass: 96 байт проти 104 байт для шести полів на Python 2.7
GrantJ

@GrantJ Так. recorclassвикористовує більше пам'яті, оскільки це tupleподібний об'єкт із змінним розміром пам'яті.
intellimath

2
Анонімні голоси не допомагають нікому. Що не так у відповіді? Чому потік?
Алі

Я люблю безпеку від помилок, які вона забезпечує стосовно types.SimpleNamespace. На жаль, пілінт не подобається :-(
xverges

23

Здається, відповідь на це питання - ні.

Нижче досить близько, але технічно це не змінюється. Це створює новий namedtuple()екземпляр із оновленим значенням x:

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

З іншого боку, ви можете створити простий клас, використовуючи __slots__який повинен добре працювати для часто оновлюваних атрибутів екземплярів класу:

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

Щоб додати цю відповідь, я думаю, що __slots__тут корисно використовувати, оскільки це ефективність пам'яті, коли ви створюєте безліч екземплярів класу. Єдиним недоліком є ​​те, що ви не можете створити нові атрибути класу.

Ось один відповідний потік, який ілюструє ефективність пам’яті - Словник проти об’єкта - який є більш ефективним і чому?

Цитований вміст у відповіді на цю тему - це дуже коротке пояснення, чому __slots__ефективніша пам’ять - слоти Python


1
Близький, але незграбний. Скажімо, я хотів виконати завдання + =, мені тоді потрібно було б зробити: p._replace (x = px + 10) vs. px + = 10
Олександр

1
так, це насправді не змінює існуючий кортеж, він створює новий екземпляр
kennes

7

Далі є хорошим рішенням для Python 3: Мінімальний клас з використанням __slots__та Sequenceабстрактний базовий клас; не робить фантазійного виявлення помилок чи подібного, але працює, і поводиться здебільшого як змінний кортеж (за винятком перевірки набору тексту).

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

Приклад:

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

Якщо ви хочете, ви також можете створити метод створення класу (хоча використання явного класу є більш прозорим):

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

Приклад:

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

У Python 2 вам потрібно його трохи відрегулювати - якщо ви успадковуєте з Sequence, клас матиме a__dict__ і the__slots__ перестане працювати.

Рішення в Python 2 полягає в тому, щоб не успадковувати Sequence, але object. При isinstance(Point, Sequence) == Trueбажанні вам потрібно зареєструвати NamedMutableSequenceбазовий клас для Sequence:

Sequence.register(NamedMutableSequence)

3

Давайте реалізуємо це за допомогою створення динамічного типу:

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)

Це перевіряє атрибути, щоб перевірити, чи є вони дійсними, перш ніж дозволити операцію продовжуватися.

Так це підбирається? Так, якщо (і лише якщо) ви зробите наступне:

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

Визначення повинно бути у вашому просторі імен і повинно існувати досить довго, щоб маринувати його. Тож якщо ви визначите, що це у вашому пакеті, воно повинно працювати.

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

Pickle не вдасться, якщо ви виконаєте наступне, або зробите визначення тимчасовим (виходить із сфери застосування, скажімо, функція):

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

І так, він зберігає порядок полів, перелічених у створенні типу.


Якщо ви додасте __iter__метод за допомогою for k in self._attrs_: yield getattr(self, k), він буде підтримувати розпакування, як кортеж.
знімок

Це також досить легко додати __len__, __getitem__та __setiem__методи, які підтримують отримання валуса за індексом, наприклад p[0]. З цими останніми бітами це здається найбільш повною і правильною відповіддю (для мене все одно).
знімок

__len__і __iter__хороші. __getitem__і __setitem__насправді можна відобразити на карті self.__dict__.__setitem__таself.__dict__.__getitem__
MadMan2064

2

Кортежі за визначенням незмінні.

Однак ви можете створити підклас словника, де ви можете отримати доступ до атрибутів за допомогою крапкових позначень;

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

Якщо ви хочете подібну поведінку, як іменіtutuples, але змінні, спробуйте найменування

Зауважте, що для того, щоб бути зміненим, це не може бути кортеж.


Дякуємо за посилання Це виглядає як найближче поки що, але мені потрібно це більш детально оцінити. До речі, я абсолютно усвідомлюю, що кортежі незмінні, тому я шукаю таке рішення, як nametuple.
Олександр

0

За умови, що продуктивність маловажлива, можна скористатися нерозумним злом, як-от:

from collection import namedtuple

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

1
Ця відповідь не дуже добре пояснена. Це виглядає заплутано, якщо ви не розумієте змінний характер списків. --- У цьому прикладі ... щоб повторно призначити z, вам потрібно зателефонувати mutable_z.z.pop(0)тоді mutable_z.z.append(new_value). Якщо ви помилитесь, ви отримаєте більше 1 елемента, і ваша програма поводитиметься несподівано.
byxor

1
@byxor що, або ви могли б просто: mutable_z.z[0] = newValue. Це справді хак, як було зазначено.
Srg

О так, я здивований, що пропустив більш очевидний спосіб перепризначити його.
byxor

Мені це подобається, справжній хак.
WebOrCode
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.