Существование изменяемого именованного кортежа в Python?


121

Кто-нибудь может изменить namedtuple или предоставить альтернативный класс, чтобы он работал с изменяемыми объектами?

В первую очередь для удобства чтения я хотел бы что-то похожее на namedtuple, которое делает это:

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

@senshin Спасибо за ссылку. Я предпочитаю не пользоваться словарем по указанной в нем причине. Этот ответ также связан с code.activestate.com/recipes/… , что довольно близко к тому, что мне нужно.
Александр

В отличие от namedtuples, похоже, у вас нет необходимости иметь возможность ссылаться на атрибуты по индексу, т.е. так p[0]и p[1]будут альтернативные способы ссылки xи, yсоответственно, правильно?
Мартино

В идеале, да, индексируется не только по имени, но и по позиции, как простой кортеж, и распаковывается как кортеж. Этот рецепт ActiveState близок, но я считаю, что он использует обычный словарь вместо OrderedDict. code.activestate.com/recipes/500261
Александр

2
Изменяемый именованный кортеж называется классом.
gbtimmon

Ответы:


133

Есть изменчивая альтернатива collections.namedtuple- recordclass .

Он имеет тот же 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
Для этого нужно больше голосов. Это именно то, что искал OP, он находится в стандартной библиотеке, и его очень просто использовать. Спасибо!
Tom Zych

3
-1 ОП очень четко дал понять, что ему нужно, и SimpleNamespaceне прошел тесты 6-10 (доступ по индексу, итеративная распаковка, итерация, упорядоченный dict, замена на месте) и 12, 13 (поля, слоты). Обратите внимание, что в документации (которую вы связали в ответе) прямо говорится: « SimpleNamespaceМожет быть полезно в качестве замены class NS: pass. Однако namedtuple()вместо этого используйте структурированный тип записи» .
Али

1
-1 также SimpleNamespaceсоздает объект, а не конструктор класса, и не может быть заменой namedtuple. Сравнение типов не будет работать, а объем памяти будет намного выше.
RedGlyph

26

В качестве альтернативы Pythonic для этой задачи, начиная с 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
С помощью тестов в OP стало очень ясно, что необходимо и dataclassне проходит тесты 6-10 (доступ по индексу, итеративная распаковка, итерация, упорядоченный dict, замена на месте) и 12, 13 (поля, слоты) в Python 3.7. 0,1.
Али

1
хотя это может быть не совсем то, что искал OP, мне это определенно помогло :)
Мартин CR

25

Последний namedlist 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. Мутация значений поля  
п: 10, 12

2. Строка  
p: точка (x = 10, y = 12)

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

4. Размер  
размер p: 64 

5. Доступ по имени поля  
п: 10, 12

6. Доступ по индексу  
п: 10, 12

7. Итеративная распаковка  
п: 10, 12

8. Итерация  
p: [10, 12]

9. Заказной Dict  
p: OrderedDict ([('x', 10), ('y', 12)])

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

11. Маринованные и соленые.  
Маринованный успешно

12. Поля  
р: ('х', 'у')

13. Слоты  
р: ('х', 'у')

Единственное отличие от Python 3.5 в том, что namedlistон стал меньше, размер 56 (Python 2.7 сообщает 64).

Обратите внимание, что я заменил ваш test 10 на замену на месте. У namedlistнего есть _replace()метод, который делает неглубокую копию, и это имеет для меня смысл, потому что namedtupleв стандартной библиотеке ведет себя так же. Изменение семантики _replace()метода может сбить с толку. На мой взгляд, этот _update()метод следует использовать для обновлений на месте. Или, может быть, я не понял цель вашего теста 10?


Есть важный нюанс. Значения namedlistсохранения в экземпляре списка. Дело в том , что cpython«s listна самом деле динамический массив. По замыслу, он выделяет больше памяти, чем необходимо, чтобы удешевить изменение списка.
intellimath 02

1
@intellimath namedlist немного неверен. На самом деле он не наследует оптимизацию listи по умолчанию использует ее __slots__. Когда я измерил, использование памяти было меньше recordclass: 96 байтов против 104 байтов для шести полей на Python 2.7
GrantJ

@GrantJ Да. recorclassиспользует больше памяти, потому что это tuple-подобный объект с переменным размером памяти.
intellimath

2
Анонимные голоса против никому не помогают. Что не так с ответом? Почему голос против?
Али

Мне нравится защита от опечаток, которую он обеспечивает types.SimpleNamespace. К сожалению, pylint это не нравится :-(
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__это хорошее использование здесь, потому что оно эффективно с точки зрения памяти при создании большого количества экземпляров классов. Единственным недостатком является то, что вы не можете создавать новые атрибуты класса.

Вот один важный поток, который иллюстрирует эффективность памяти - Dictionary vs Object - что более эффективно и почему?

Цитируемый контент в ответе на этот поток является очень кратким объяснением того, почему __slots__более эффективен память - слоты Python


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

1
да, это на самом деле не изменение существующего кортежа, а создание нового экземпляра
Кеннес

8

Следующее - хорошее решение для 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, класс будет иметь__dict__ и __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

Определение должно находиться в вашем пространстве имен и существовать достаточно долго, чтобы pickle его нашел. Так что, если вы определите, что это находится в вашем пакете, это должно работать.

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 02

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

Если вы хотите, чтобы поведение было похоже на namedtuples, но изменяемое, попробуйте namedlist

Обратите внимание: чтобы быть изменяемым, он не может быть кортежем.


Спасибо за ссылку. Это похоже на самый близкий на данный момент, но мне нужно оценить его более подробно. Кстати, я полностью осознаю, что кортежи неизменяемы, поэтому я ищу такое решение, как namedtuple.
Александр

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). Если вы ошибетесь, у вас будет более одного элемента, и ваша программа будет вести себя неожиданно.
byxor

1
@byxor что, или вы могли бы просто: mutable_z.z[0] = newValue. Как уже говорилось, это действительно взлом.
Srg

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

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