qué - ¿Cómo puedo memorizar una instanciación de clase en Python?
qué es una clase en python (3)
La solución que terminé usando es esta:
class memoize(object):
def __init__(self, cls):
self.cls = cls
self.__dict__.update(cls.__dict__)
# This bit allows staticmethods to work as you would expect.
for attr, val in cls.__dict__.items():
if type(val) is staticmethod:
self.__dict__[attr] = val.__func__
def __call__(self, *args):
key = ''//''.join(map(str, args))
if key not in self.cls.instances:
self.cls.instances[key] = self.cls(*args)
return self.cls.instances[key]
Y luego __init__
la clase con esto, no __init__
. Aunque Brandizzi me proporcionó esa información clave, su decorador de ejemplo no funcionó como se desea.
Encontré este concepto bastante sutil, pero básicamente cuando usas decoradores en Python, debes entender que lo que se decora (ya sea un método o una clase) es reemplazado por el decorador. Entonces, por ejemplo, cuando intenté acceder a Photograph.instances
o Camera.generate_id()
(un método estático), no pude acceder a ellos porque Photograph
no hace referencia a la clase de Photograph original, se refiere a la función memoized
. (Del ejemplo de Brandizzi).
Para solucionar esto, tuve que crear una clase de decorador que en realidad tomara todos los atributos y métodos estáticos de la clase decorada y los expusiera como propios. Casi como una subclase, excepto que la clase decoradora no sabe de antemano qué clases estará decorando, por lo que tiene que copiar los atributos después del hecho.
El resultado final es que cualquier instancia de la clase memoize
convierte en una envoltura casi transparente alrededor de la clase real que ha decorado, con la excepción de que intentar crear una instancia (pero realmente llamarla) le proporcionará copias en caché cuando estén disponibles. .
Ok, aquí está el escenario del mundo real: estoy escribiendo una aplicación, y tengo una clase que representa un cierto tipo de archivos (en mi caso, esto es fotografías pero ese detalle es irrelevante para el problema). Cada instancia de la clase de fotografía debe ser única para el nombre de archivo de la foto.
El problema es que cuando un usuario le dice a mi aplicación que cargue un archivo, debo poder identificar cuándo ya están cargados los archivos y usar la instancia existente para ese nombre de archivo, en lugar de crear instancias duplicadas en el mismo nombre de archivo.
Para mí, esta es una buena situación para usar la memorización, y hay muchos ejemplos de eso, pero en este caso no solo estoy memorizando una función ordinaria, necesito memorizar __init__()
. Esto plantea un problema, porque cuando se __init__()
ya es demasiado tarde ya que se ha creado una nueva instancia.
En mi investigación encontré el __new__()
Python, y en realidad pude escribir un ejemplo trivial funcional, pero se derrumbó cuando intenté usarlo en mis objetos del mundo real, y no estoy seguro de por qué (la única Lo que sí puedo pensar es que mis objetos del mundo real eran subclases de otros objetos que realmente no puedo controlar, por lo que hubo algunas incompatibilidades con este enfoque. Esto es lo que yo tenía:
class Flub(object):
instances = {}
def __new__(cls, flubid):
try:
self = Flub.instances[flubid]
except KeyError:
self = Flub.instances[flubid] = super(Flub, cls).__new__(cls)
print ''making a new one!''
self.flubid = flubid
print id(self)
return self
@staticmethod
def destroy_all():
for flub in Flub.instances.values():
print ''killing'', flub
a = Flub(''foo'')
b = Flub(''foo'')
c = Flub(''bar'')
print a
print b
print c
print a is b, b is c
Flub.destroy_all()
Que la salida de este:
making a new one!
139958663753808
139958663753808
making a new one!
139958663753872
<__main__.Flub object at 0x7f4aaa6fb050>
<__main__.Flub object at 0x7f4aaa6fb050>
<__main__.Flub object at 0x7f4aaa6fb090>
True False
killing <__main__.Flub object at 0x7f4aaa6fb050>
killing <__main__.Flub object at 0x7f4aaa6fb090>
¡Es perfecto! Solo se hicieron dos instancias para los dos identificadores únicos dados, y las instancias de Flub claramente solo tienen dos enumeradas.
Pero cuando intenté adoptar este enfoque con los objetos que estaba usando, recibí todo tipo de errores sin sentido acerca de cómo __init__()
tomó solo 0 argumentos, no 2. Así que __init__()
algunas cosas y luego me dijo que __init__()
necesitaba un argumento. Totalmente extraño.
Después de un tiempo de pelear con él, básicamente me di por vencido y moví toda la magia negra __new__()
en un método estático llamado get
, de modo que pudiera llamar Photograph.get(filename)
y solo llamaría Photograph(filename)
si el nombre de archivo no estaba ya en Photograph.instances
.
¿Alguien sabe dónde me equivoqué aquí? ¿Hay alguna manera mejor de hacer esto?
Otra forma de verlo es que es similar a un singleton, excepto que no es globalmente singleton, solo singleton por nombre de archivo.
Aquí está mi código del mundo real usando el método static si quieres verlo todo junto.
Los parámetros para __new__
también se pasan a __init__
, así que:
def __init__(self, flubid):
...
flubid
aceptar el argumento flubid
allí, incluso si no lo usa en __init__
Aquí está el comentario relevante tomado de typeobject.c en Python2.7.3
/* You may wonder why object.__new__() only complains about arguments
when object.__init__() is not overridden, and vice versa.
Consider the use cases:
1. When neither is overridden, we want to hear complaints about
excess (i.e., any) arguments, since their presence could
indicate there''s a bug.
2. When defining an Immutable type, we are likely to override only
__new__(), since __init__() is called too late to initialize an
Immutable object. Since __new__() defines the signature for the
type, it would be a pain to have to override __init__() just to
stop it from complaining about excess arguments.
3. When defining a Mutable type, we are likely to override only
__init__(). So here the converse reasoning applies: we don''t
want to have to override __new__() just to stop it from
complaining.
4. When __init__() is overridden, and the subclass __init__() calls
object.__init__(), the latter should complain about excess
arguments; ditto for __new__().
Use cases 2 and 3 make it unattractive to unconditionally check for
excess arguments. The best solution that addresses all four use
cases is as follows: __init__() complains about excess arguments
unless __new__() is overridden and __init__() is not overridden
(IOW, if __init__() is overridden or __new__() is not overridden);
symmetrically, __new__() complains about excess arguments unless
__init__() is overridden and __new__() is not overridden
(IOW, if __new__() is overridden or __init__() is not overridden).
However, for backwards compatibility, this breaks too much code.
Therefore, in 2.6, we''ll *warn* about excess arguments when both
methods are overridden; for all other cases we''ll use the above
rules.
*/
Veamos dos puntos acerca de su pregunta.
Utilizando memoize
Puedes usar la memoria, pero debes decorar la clase , no el método __init__
. Supongamos que tenemos este memoizador:
def get_id_tuple(f, args, kwargs, mark=object()):
"""
Some quick''n''dirty way to generate a unique key for an specific call.
"""
l = [id(f)]
for arg in args:
l.append(id(arg))
l.append(id(mark))
for k, v in kwargs:
l.append(k)
l.append(id(v))
return tuple(l)
_memoized = {}
def memoize(f):
"""
Some basic memoizer
"""
def memoized(*args, **kwargs):
key = get_id_tuple(f, args, kwargs)
if key not in _memoized:
_memoized[key] = f(*args, **kwargs)
return _memoized[key]
return memoized
Ahora solo necesitas decorar la clase:
@memoize
class Test(object):
def __init__(self, somevalue):
self.somevalue = somevalue
Veamos una prueba?
tests = [Test(1), Test(2), Test(3), Test(2), Test(4)]
for test in tests:
print test.somevalue, id(test)
La salida está abajo. Tenga en cuenta que los mismos parámetros producen el mismo id del objeto devuelto:
1 3072319660
2 3072319692
3 3072319724
2 3072319692
4 3072319756
De todos modos, preferiría crear una función para generar los objetos y memorizarlos. Me parece más limpio, pero puede ser algún motivo irrelevante para las mascotas:
class Test(object):
def __init__(self, somevalue):
self.somevalue = somevalue
@memoize
def get_test_from_value(somevalue):
return Test(somevalue)
Usando __new__
:
O, por supuesto, puede anular __new__
. Hace algunos días __new__
una respuesta sobre las __new__
y las mejores prácticas para anular __new__
que pueden ser útiles. Básicamente, dice que siempre pase *args, **kwargs
a su método __new__
.
Yo, por mi parte, preferiría memorizar una función que crea los objetos, o incluso escribir una función específica que se encargaría de nunca recrear un objeto en el mismo parámetro. Por supuesto, sin embargo, esto es principalmente una opinión mía, no una regla.