¿Existe una tupla con nombre mutable en Python?

121

¿Alguien puede modificar namedtuple o proporcionar una clase alternativa para que funcione para objetos mutables?

Principalmente por legibilidad, me gustaría algo similar a namedtuple que haga esto:

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)

Debe ser posible encurtir el objeto resultante. Y según las características de la tupla con nombre, el orden de la salida cuando se representa debe coincidir con el orden de la lista de parámetros al construir el objeto.

Alejandro
fuente
3
Consulte también: stackoverflow.com/q/5131044 . ¿Hay alguna razón por la que no pueda usar un diccionario?
senshin
@senshin Gracias por el enlace. Prefiero no usar un diccionario por el motivo señalado en él. Esa respuesta también se vinculó a code.activestate.com/recipes/… , que se acerca bastante a lo que busco.
Alexander
A diferencia de namedtuples, parece que no tiene necesidad de poder hacer referencia a los atributos por índice, es decir, entonces p[0]y p[1]serían formas alternativas de referencia xy y, respectivamente, ¿correcto?
Martineau
Idealmente, sí, indexable por posición como una tupla simple además de por nombre, y se descomprime como una tupla. Esta receta de ActiveState está cerca, pero creo que usa un diccionario normal en lugar de un OrderedDict. code.activestate.com/recipes/500261
Alexander
2
Una tupla con nombre mutable se llama clase.
gbtimmon

Respuestas:

132

Existe una alternativa mutable a collections.namedtuple- recordclass .

Tiene la misma API y huella de memoria que namedtupley admite asignaciones (también debería ser más rápido). Por ejemplo:

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)

Para Python 3.6 y superior recordclass(desde 0.5), admite sugerencias de tipo:

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)

Hay un ejemplo más completo (también incluye comparaciones de desempeño).

Dado que la recordclassbiblioteca 0.9 proporciona otra variante: recordclass.structclassfunción de fábrica. Puede producir clases, cuyas instancias ocupan menos memoria que las __slots__instancias basadas en. Esto puede ser importante para las instancias con valores de atributo, que no han tenido la intención de tener ciclos de referencia. Puede ayudar a reducir el uso de memoria si necesita crear millones de instancias. A continuación se muestra un ejemplo ilustrativo .

intellimath
fuente
4
Gusta. 'Esta biblioteca es en realidad una "prueba de concepto" para el problema de la alternativa "mutable" de la tupla con nombre.'
Alexander
1
recordclasses más lento, requiere más memoria y requiere extensiones C en comparación con la receta de Antti Haapala y namedlist.
GrantJ
recordclasses una versión mutable collection.namedtupleque hereda su api, huella de memoria, pero asignaciones de soporte. namedlistes en realidad una instancia de la clase Python con ranuras. Es más útil si no necesita un acceso rápido a sus campos por índice.
intellimath
El acceso a los atributos, por recordclassejemplo (Python 3.5.2), es aproximadamente un 2-3% más lento que paranamedlist
intellimath
Cuando se usa namedtupleuna creación de clase simple Point = namedtuple('Point', 'x y'), Jedi puede autocompletar atributos, mientras que este no es el caso recordclass. Si utilizo el código de creación más largo (basado en RecordClass), entonces Jedi entiende la Pointclase, pero no su constructor o atributos ... ¿Hay alguna forma recordclassde trabajar bien con Jedi?
PhilMacKay
34

types.SimpleNamespace se introdujo en Python 3.3 y es compatible con los requisitos solicitados.

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)
futuro funky
fuente
1
He estado buscando algo como esto durante años. Gran reemplazo para una biblioteca dict punteada como dotmap
axwell
1
Esto necesita más votos a favor. Es exactamente lo que estaba buscando el OP, está en la biblioteca estándar y no podría ser más sencillo de usar. ¡Gracias!
Tom Zych
3
-1 El OP dejó muy claro con sus pruebas lo que necesita y SimpleNamespacefalla las pruebas 6-10 (acceso por índice, desempaquetado iterativo, iteración, dictado ordenado, reemplazo in situ) y 12, 13 (campos, ranuras). Tenga en cuenta que la documentación (que vinculó en la respuesta) dice específicamente " SimpleNamespacepuede ser útil como reemplazo de class NS: pass. Sin embargo, para un tipo de registro estructurado, utilice namedtuple()en su lugar".
Ali
1
-1 también SimpleNamespacecrea un objeto, no un constructor de clase, y no puede reemplazar a namedtuple. La comparación de tipos no funcionará y la huella de memoria será mucho mayor.
RedGlyph
26

Como una alternativa muy Pythonic para esta tarea, desde Python-3.7, puede usar un dataclassesmódulo que no solo se comporta como un mutable NamedTupleporque usan definiciones de clases normales sino que también admiten otras características de clases.

Desde PEP-0557:

Aunque utilizan un mecanismo muy diferente, las clases de datos se pueden considerar como "tuplas con nombre mutables con valores predeterminados". Debido a que las clases de datos usan la sintaxis de definición de clase normal, usted es libre de usar herencia, metaclases, cadenas de documentación, métodos definidos por el usuario, fábricas de clases y otras características de clases de Python.

Se proporciona un decorador de clases que inspecciona una definición de clase para variables con anotaciones de tipo como se define en PEP 526 , "Sintaxis para anotaciones de variables". En este documento, estas variables se denominan campos. Usando estos campos, el decorador agrega definiciones de métodos generados a la clase para admitir la inicialización de la instancia, una repr, métodos de comparación y, opcionalmente, otros métodos como se describe en la sección Especificación . Tal clase se llama Clase de datos, pero realmente no hay nada especial en la clase: el decorador agrega métodos generados a la clase y devuelve la misma clase que se le dio.

Esta función se introduce en PEP-0557 y puede leerla con más detalles en el enlace de documentación proporcionado.

Ejemplo:

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

Manifestación:

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)
Kasravnd
fuente
1
Se dejó muy claro con las pruebas en el OP lo que se necesita y dataclassfalla las pruebas 6-10 (acceso por índice, desempaquetado iterativo, iteración, dictado ordenado, reemplazo in situ) y 12, 13 (campos, ranuras) en Python 3.7 .1.
Ali
1
aunque esto puede no ser específicamente lo que estaba buscando el OP, ciertamente me ayudó :)
Martin CR
25

La última lista de nombres 1.7 pasa todas sus pruebas con Python 2.7 y Python 3.5 a partir del 11 de enero de 2016. Es una implementación de Python pura, mientras que recordclasses una extensión de C. Por supuesto, depende de sus requisitos si se prefiere una extensión C o no.

Tus pruebas (pero también consulta la nota a continuación):

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

Salida en Python 2.7

1. Mutación de los valores de campo  
p: 10, 12

2. Cadena  
p: Punto (x = 10, y = 12)

3. Representación  
Punto (x = 10, y = 12) 

4. Tamaño de  
tamaño de p: 64 

5. Acceso por nombre del campo  
p: 10, 12

6. Acceso por índice  
p: 10, 12

7. Desembalaje iterativo  
p: 10, 12

8. Iteración  
p: [10, 12]

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

10. Reemplazo in situ (¿actualizar?)  
p: Punto (x = 100, y = 200)

11. Encurtir y despejar  
Encurtido con éxito

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

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

La única diferencia con Python 3.5 es que se namedlistha vuelto más pequeño, el tamaño es 56 (Python 2.7 informa 64).

Tenga en cuenta que cambié su prueba 10 para reemplazo en el lugar. El namedlisttiene un _replace()método que hace una copia superficial, y que tiene mucho sentido para mí, porque el namedtuplede la biblioteca estándar se comporta de la misma manera. Cambiar la semántica del _replace()método sería confuso. En mi opinión, el _update()método debería usarse para actualizaciones in situ. ¿O tal vez no entendí la intención de tu prueba 10?

Ali
fuente
Hay un matiz importante. Los namedlistvalores de la tienda en la instancia de lista. El caso es que cpython's listen realidad es una matriz dinámica. Por diseño, asigna más memoria de la necesaria para que la mutación de la lista sea más barata.
intellimath
1
@intellimath namedlist es un nombre poco apropiado. En realidad, no hereda listy utiliza de forma predeterminada la __slots__optimización. Cuando medí, el uso de la memoria fue inferior a recordclass: 96 bytes frente a 104 bytes para seis campos en Python 2.7
GrantJ
@GrantJ Sí. recorclassusa más memoria porque es un tupleobjeto similar con un tamaño de memoria variable.
intellimath
2
Los votos negativos anónimos no ayudan a nadie. ¿Qué hay de malo en la respuesta? ¿Por qué el voto negativo?
Ali
Me encanta la seguridad contra los errores tipográficos que ofrece types.SimpleNamespace. Desafortunadamente, a pylint no le gusta :-(
xverges
23

Parece que la respuesta a esta pregunta es no.

A continuación se muestra bastante cerca, pero técnicamente no es mutable. Esto está creando una nueva namedtuple()instancia con un valor x actualizado:

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

Por otro lado, puede crear una clase simple usando __slots__que debería funcionar bien para actualizar con frecuencia los atributos de instancia de clase:

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

Para agregar a esta respuesta, creo que __slots__es un buen uso aquí porque es eficiente en la memoria cuando crea muchas instancias de clase. El único inconveniente es que no puede crear nuevos atributos de clase.

Aquí hay un hilo relevante que ilustra la eficiencia de la memoria, Diccionario vs Objeto, ¿cuál es más eficiente y por qué?

El contenido citado en la respuesta de este hilo es una explicación muy sucinta de por qué __slots__es más eficiente la memoria: ranuras de Python

Kennes
fuente
1
Cercano, pero torpe. Digamos que quería hacer una tarea + =, luego tendría que hacer: p._replace (x = px + 10) vs px + = 10
Alexander
1
sí, realmente no está cambiando la tupla existente, está creando una nueva instancia
Kennes
7

La siguiente es una buena solución para Python 3: Una clase mínimo usando __slots__y Sequenceclase base abstracta; no realiza una elegante detección de errores o algo así, pero funciona y se comporta principalmente como una tupla mutable (excepto para la comprobación de tipo).

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

Ejemplo:

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

Si lo desea, también puede tener un método para crear la clase (aunque usar una clase explícita es más transparente):

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

Ejemplo:

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

En Python 2, debe ajustarlo ligeramente; si hereda Sequence, la clase tendrá un__dict__ y __slots__dejará de funcionar.

La solución en Python 2 es no heredar de Sequence, pero object. Si isinstance(Point, Sequence) == Truelo desea, debe registrar NamedMutableSequencecomo clase base para Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
fuente
3

Implementemos esto con la creación dinámica de tipos:

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)

Esto verifica los atributos para ver si son válidos antes de permitir que continúe la operación.

Entonces, ¿es esto encurtido? Sí, si (y solo si) hace lo siguiente:

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

La definición tiene que estar en su espacio de nombres y debe existir el tiempo suficiente para que pickle la encuentre. Entonces, si define esto para que esté en su paquete, debería funcionar.

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

Pickle fallará si hace lo siguiente, o hace que la definición sea temporal (sale del alcance cuando la función finaliza, digamos):

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

Y sí, conserva el orden de los campos enumerados en la creación del tipo.

MadMan2064
fuente
Si agrega un __iter__método con for k in self._attrs_: yield getattr(self, k), admitirá el desempaquetado como una tupla.
zapato
También es bastante fácil añadir __len__, __getitem__y __setiem__métodos para apoyar conseguir Valus por el índice, al igual que p[0]. Con estos últimos bits, esta parece la respuesta más completa y correcta (para mí de todos modos).
zapato
__len__y __iter__son buenos. __getitem__y __setitem__realmente se puede asignar a self.__dict__.__setitem__yself.__dict__.__getitem__
MadMan2064
2

Las tuplas son por definición inmutables.

Sin embargo, puede crear una subclase de diccionario en la que pueda acceder a los atributos con notación de puntos;

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}
Roland Smith
fuente
2

Si desea un comportamiento similar al de namedtuples pero mutable, intente namedlist

Tenga en cuenta que para ser mutable no puede ser una tupla.

agomcas
fuente
Gracias por el enlace. Esto parece el más cercano hasta ahora, pero necesito evaluarlo con más detalle. Por cierto, soy totalmente consciente de que las tuplas son inmutables, por eso estoy buscando una solución como namedtuple.
Alexander
0

Siempre que el rendimiento sea de poca importancia, uno podría usar un truco tonto como:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
fuente
1
Esta respuesta no está muy bien explicada. Parece confuso si no comprende la naturaleza mutable de las listas. --- En este ejemplo ... para reasignar z, debes llamar mutable_z.z.pop(0)entonces mutable_z.z.append(new_value). Si se equivoca, terminará con más de 1 elemento y su programa se comportará inesperadamente.
byxor
1
@byxor que, o lo que podría: mutable_z.z[0] = newValue. De hecho, es un truco, como se dijo.
Srg
Oh, sí, me sorprende haber perdido la forma más obvia de reasignarlo.
byxor
Me gusta, truco real.
WebOrCode