Существование измененного имени кортежа в 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)

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

Ответ 1

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

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

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

Ответ 2

Кажется, что ответ на этот вопрос - нет.

Ниже довольно близко, но это не технически изменчиво. Это создает новый экземпляр 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__ здесь полезен, потому что он эффективен при создании большого количества экземпляров класса. Единственным недостатком является то, что вы не можете создавать новые атрибуты класса.

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

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

Ответ 3

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

Ответ 4

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)

Ответ 5

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

От PEP-0557:

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

Предусмотрен декоратор класса, который проверяет определение класса для переменных с аннотациями типов, как определено в PEP 526, "Синтаксис для аннотаций переменных". В этом документе такие переменные называются полями. Используя эти поля, декоратор добавляет сгенерированные определения методов в класс для поддержки инициализации экземпляра, repr, методов сравнения и, при необходимости, других методов, как описано в разделе " Спецификация ". Такой класс называется классом данных, но в нем нет ничего особенного: декоратор добавляет сгенерированные методы к классу и возвращает тот же класс, который был ему предоставлен.

Эта функция представлена в 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)

Ответ 6

Ниже приведено хорошее решение для Python 3: минимальный класс с использованием базового класса __slots__ и Sequence; не делает причудливого обнаружения ошибок или такого, но он работает и ведет себя в основном как изменяемый кортеж (кроме typecheck).

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)

Ответ 7

Позвольте реализовать это с созданием динамического типа:

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

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

Ответ 8

Если вы хотите подобное поведение, как namedtuples, но mutable try namedlist

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

Ответ 9

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

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

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}

Ответ 10

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

from collection import namedtuple

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

Ответ 11

Мое чтение темы может быть поверхностным, но разве основные классы данных не предоставляют это сейчас?