Существование измененного имени кортежа в 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
Мое чтение темы может быть поверхностным, но разве основные классы данных не предоставляют это сейчас?