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


может ли кто-нибудь изменить 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)

должна быть возможность замариновать полученный объект. И в соответствии с характеристиками именованного кортежа порядок вывода при представлении должен соответствовать порядку списка параметров при построении объект.

ответ: Спасибо всем, кто представил свои предложения. Я считаю, что recordclass ссылка на @intellimath является лучшим решением (Также см. здесь).

класс записи 0.4 изменяемый вариант коллекций.namedtuple, который поддерживает заданий

recordclass-это лицензированная библиотека python MIT. Он реализует тип memoryslots и фабрика функция recordclass для создания Запись-Как классы.

memoryslots-это кортеж-подобный тип, который поддерживает операции назначения. recordclass-это заводская функция, которая создает "изменяемый" аналог коллекция.namedtuple. Эта библиотека на самом деле является " доказательством концепции" для проблемы "изменчивой" альтернативы namedtuple.

Я также провел некоторые тесты против всех предложений. Не все эти функции были запрошены, поэтому сравнение не совсем справедливо. Тесты здесь только для того, чтобы указать на удобство использования каждого класса.

# Option 1 (p1): @kennes913
# Option 2 (p2): @MadMan2064
# Option 3 (p3): @intellimath   
# Option 4 (p4): @Roland Smith
# Option 5 (p5): @agomcas
# Option 6 (p6): @Antti Haapala


# TEST:                             p1     p2     p3     p4     p5      p6 
# 1.  Mutation of field values   |  x   |   x  |   x  |   x  |   x  |   x  |
# 2.  String                     |      |   x  |   x  |   x  |      |   x  |
# 3.  Representation             |      |   x  |   x  |   x  |      |   x  |
# 4.  Sizeof                     |  x   |   x  |   x  |   ?  |  ??  |   x  |
# 5.  Access by name of field    |  x   |   x  |   x  |   x  |   x  |   x  |
# 6.  Access by index.           |      |      |   x  |      |      |      |
# 7.  Iterative unpacking.       |      |   x  |   x  |      |      |   x  |
# 8.  Iteration                  |      |   x  |   x  |      |      |   x  |
# 9.  Ordered Dict               |      |      |   x  |      |      |      |
# 10. Inplace replacement        |      |      |   x  |      |      |      |
# 11. Pickle and Unpickle        |      |      |   x  |      |      |      |
# 12. Fields*                    |      |      | yes  |      |  yes |      |
# 13. Slots*                     |  yes |      |      |      |  yes |      |

# *Note that I'm not very familiar with slots and fields, so please excuse 
# my ignorance in reporting their results.  I have included them for completeness.

# Class/Object creation.
p1 = Point1(x=1, y=2)

Point2 = namedgroup("Point2", ["x", "y"])
p2 = Point2(x=1, y=2)

Point3 = recordclass('Point3', 'x y')   # ***
p3 = Point3(x=1, y=2)

p4 = AttrDict()
p4.x = 1
p4.y = 2

p5 = namedlist('Point5', 'x y')

Point6 = namedgroup('Point6', ['x', 'y'])
p6 = Point6(x=1, y=2)

point_objects = [p1, p2, p3, p4, p5, p6]

# 1. Mutation of field values.
for n, p in enumerate(point_objects):
    try:
        p.x *= 10
        p.y += 10
        print('p{0}: {1}, {2}'.format(n + 1, p.x, p.y))
    except Exception as e:
        print('p{0}: Mutation not supported. {1}'.format(n + 1, e))

p1: 10, 12
p2: 10, 12
p3: 10, 12
p4: 10, 12
p5: 10, 12
p6: 10, 12


# 2. String.
for n, p in enumerate(point_objects):
    print('p{0}: {1}'.format(n + 1, p))
p1: <__main__.Point1 instance at 0x10c72dc68>
p2: Point2(x=10, y=12)
p3: Point3(x=10, y=12)
p4: {'y': 12, 'x': 10}
p5: <class '__main__.Point5'>
p6: Point6(x=10, y=12)


# 3. Representation.
[('p{0}'.format(n + 1), p) for n, p in enumerate(point_objects)]

[('p1', <__main__.Point1 instance at 0x10c72dc68>),
 ('p2', Point2(x=10, y=12)),
 ('p3', Point3(x=10, y=12)),
 ('p4', {'x': 10, 'y': 12}),
 ('p5', __main__.Point5),
 ('p6', Point6(x=10, y=12))]


# 4. Sizeof.
for n, p in enumerate(point_objects):
    print("size of p{0}:".format(n + 1), sys.getsizeof(p))

size of p1: 72
size of p2: 64
size of p3: 72
size of p4: 280
size of p5: 904
size of p6: 64


# 5. Access by name of field.
for n, p in enumerate(point_objects):
    print('p{0}: {1}, {2}'.format(n + 1, p.x, p.y))

p1: 10, 12
p2: 10, 12
p3: 10, 12
p4: 10, 12
p5: 10, 12
p6: 10, 12


# 6. Access by index.
for n, p in enumerate(point_objects):
    try:
        print('p{0}: {1}, {2}'.format(n + 1, p[0], p[1]))
    except:
        print('p{0}: Unable to access by index.'.format(n+1))

p1: Unable to access by index.
p2: Unable to access by index.
p3: 10, 12
p4: Unable to access by index.
p5: Unable to access by index.
p6: Unable to access by index.


# 7. Iterative unpacking.
for n, p in enumerate(point_objects):
    try:
        x, y = p
        print('p{0}: {1}, {2}'.format(n + 1, x, y))
    except:
        print('p{0}: Unable to unpack.'.format(n + 1))

p1: Unable to unpack.
p2: 10, 12
p3: 10, 12
p4: y, x
p5: Unable to unpack.
p6: 10, 12


# 8. Iteration
for n, p in enumerate(point_objects):
    try:
        print('p{0}: {1}'.format(n + 1, [v for v in p]))
    except:
        print('p{0}: Unable to iterate.'.format(n + 1))

p1: Unable to iterate.
p2: [10, 12]
p3: [10, 12]
p4: ['y', 'x']
p5: Unable to iterate.
p6: [10, 12]
In [95]:


# 9. Ordered Dict
for n, p in enumerate(point_objects):
    try:
        print('p{0}: {1}'.format(n + 1, p._asdict()))
    except:
        print('p{0}: Unable to create Ordered Dict.'.format(n + 1))

p1: Unable to create Ordered Dict.
p2: Unable to create Ordered Dict.
p3: OrderedDict([('x', 10), ('y', 12)])
p4: Unable to create Ordered Dict.
p5: Unable to create Ordered Dict.
p6: Unable to create Ordered Dict.


# 10. Inplace replacement
for n, p in enumerate(point_objects):
    try:
        p_ = p._replace(x=100, y=200)
        print('p{0}: {1} - {2}'.format(n + 1, 'Success' if p is p_ else 'Failure', p))
    except:
        print('p{0}: Unable to replace inplace.'.format(n + 1))

p1: Unable to replace inplace.
p2: Unable to replace inplace.
p3: Success - Point3(x=100, y=200)
p4: Unable to replace inplace.
p5: Unable to replace inplace.
p6: Unable to replace inplace.


# 11. Pickle and Unpickle.
for n, p in enumerate(point_objects):
    try:
        pickled = pickle.dumps(p)
        unpickled = pickle.loads(pickled)
        if p != unpickled:
            raise ValueError((p, unpickled))
        print('p{0}: {1}'.format(n + 1, 'Pickled successfully', ))
    except Exception as e:
        print('p{0}: {1}; {2}'.format(n + 1, 'Pickle failure', e))

p1: Pickle failure; (<__main__.Point1 instance at 0x10c72dc68>, <__main__.Point1 instance at 0x10ca631b8>)
p2: Pickle failure; (Point2(x=10, y=12), Point2(x=10, y=12))
p3: Pickled successfully
p4: Pickle failure; '__getstate__'
p5: Pickle failure; Can't pickle <class '__main__.Point5'>: it's not found as __main__.Point5
p6: Pickle failure; (Point6(x=10, y=12), Point6(x=10, y=12))


# 12. Fields.
for n, p in enumerate(point_objects):
    try:
        print('p{0}: {1}'.format(n + 1, p._fields))
    except Exception as e:
        print('p{0}: {1}; {2}'.format(n + 1, 'Unable to access fields.', e))

p1: Unable to access fields.; Point1 instance has no attribute '_fields'
p2: Unable to access fields.; 'Point2' object has no attribute '_fields'
p3: ('x', 'y')
p4: Unable to access fields.; '_fields'
p5: ('x', 'y')
p6: Unable to access fields.; 'Point6' object has no attribute '_fields'


# 13. Slots.
for n, p in enumerate(point_objects):
    try:
        print('p{0}: {1}'.format(n + 1, p.__slots__))
    except Exception as e:
        print('p{0}: {1}; {2}'.format(n + 1, 'Unable to access slots', e))

p1: ['x', 'y']
p2: Unable to access slots; 'Point2' object has no attribute '__slots__'
p3: ()
p4: Unable to access slots; '__slots__'
p5: ('x', 'y')
p6: Unable to access slots; 'Point6' object has no attribute '__slots__'
10 72

10 ответов:

существует изменяемая альтернатива 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)

есть более полное пример (Он также включает в себя сравнение производительности).

похоже, что ответа на этот вопрос нет.

ниже довольно близко, но это технически не изменчиво. Это создание нового 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 слоты

последние 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. Mutation of field values  
p: 10, 12

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

3. Representation  
Point(x=10, y=12) 

4. Sizeof  
size of p: 64 

5. Access by name of field  
p: 10, 12

6. Access by index  
p: 10, 12

7. Iterative unpacking  
p: 10, 12

8. Iteration  
p: [10, 12]

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

10. Inplace replacement (update?)  
p: Point(x=100, y=200)

11. Pickle and Unpickle  
Pickled successfully

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

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

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

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

типы.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)

следующее хорошее решение для 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)

как очень подходящие для Python вариантом для этой задачи, начиная с версии Python-3.7, вы можете использовать dataclasses модуль, который не только ведет себя как Мутабельный NamedTuple поскольку они используют обычные определения классов, они также поддерживают другие функции классов.

от PEP-0557:

хотя они используют совсем другой механизм, классы данных можно рассматривать как"изменяемые namedtuples с настройками по умолчанию". Поскольку классы данных используют обычный синтаксис определения класса, вы можете использовать его бесплатно наследование, метаклассы, docstrings, пользовательские методы, фабрики классов и другие функции класса 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)

давайте реализуем это с помощью динамического создания типа:

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"])

рассол не получится, если вы сделайте следующее или сделайте определение временным (выходит из области действия, когда функция заканчивается, скажем):

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

и да, он сохраняет порядок полей, перечисленных в создании типа.

Кортежи по определению неизменяемы.

однако вы можете создать подкласс словаря, где вы можете получить доступ к атрибутам с точечной нотацией;

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}

Если вы хотите, чтобы подобное поведение как namedtuples но Мутабельный попробовать namedlist

обратите внимание, что для того, чтобы быть изменяемы это не может кортежа.

при условии, что производительность имеет мало значения, можно было бы использовать глупый хак, как:

from collection import namedtuple

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