2012-06-04 1 views
13

Bien, 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 Fotografía debe ser exclusiva del nombre de archivo de la foto.¿Cómo puedo memorizar una creación de instancias de clase en Python?

El problema es que cuando un usuario le dice a mi aplicación que cargue un archivo, necesito poder identificar cuándo ya están cargados y usar la instancia existente para ese nombre de archivo, en lugar de crear instancias duplicadas en el mismo nombre de archivo .

Para mí, esto parece una buena situación para usar la memoria, y hay muchos ejemplos de eso, pero en este caso no solo estoy recordando una función normal, necesito estar recordando __init__(). Esto plantea un problema, porque cuando se llama a __init__(), ya es demasiado tarde ya que ya se creó una nueva instancia.

En mi investigación encontré el método __new__() de Python, y pude escribir un ejemplo trivial de trabajo, pero se vino abajo cuando traté de usarlo en mis objetos del mundo real, y no estoy seguro de por qué (lo único que se me ocurre 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 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() 

cual la salida siguiente:

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 crearon dos instancias para los dos identificadores únicos proporcionados, y las instancias Flub claramente solo tienen dos enumeradas.

Pero cuando traté de adoptar este enfoque con los objetos que estaba usando, tengo todo tipo de errores sin sentido acerca de cómo __init__() tomó sólo 0 argumentos, no 2. Así que cambiaría algunas cosas y entonces se diría yo que __init__() necesitaba un argumento. Totalmente extraño.

Después de un tiempo de luchar con él, básicamente, sólo di por vencido y se trasladó toda la magia negro __new__() en una llamada métodoestático get, de tal manera que yo podría llamar Photograph.get(filename) y sólo se llamaría Photograph(filename) si el nombre de archivo no estuviera ya en Photograph.instances .

¿Alguien sabe dónde me equivoqué aquí? ¿Hay alguna forma mejor de hacer esto?

Otra forma de pensar es que es similar a un singleton, excepto que no es singleton global, solo singleton-por-nombre de archivo.

Here's my real-world code using the staticmethod get si quiere verlo todo junto.

+1

He editado la pregunta para eliminar las cosas que dijiste. – robru

Respuesta

11

veamos dos puntos sobre su pregunta.

Usando memoize

Puede utilizar memoization, pero usted debe decorar la clase , no el método __init__. Supongamos que tenemos esta memoizator:

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 sólo tiene que 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á por debajo. Tenga en cuenta que los mismos parámetros dan el mismo identificador del objeto devuelto:

1 3072319660 
2 3072319692 
3 3072319724 
2 3072319692 
4 3072319756 

De todos modos, yo preferiría crear una función para generar los objetos y memoize ella. Parece más limpio para mí, pero puede ser un poco irrelevante cosas que me molestan:

class Test(object): 
    def __init__(self, somevalue): 
     self.somevalue = somevalue 

@memoize 
def get_test_from_value(somevalue): 
    return Test(somevalue) 

El uso de __new__:

O, por supuesto, puede anular __new__. Hace algunos días publiqué an answer about the ins, outs and best practices of overriding __new__ que puede ser útil. 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 encargue de nunca volver a crear un objeto para el mismo parámetro. Por supuesto, sin embargo, esto es principalmente una opinión mía, no una regla.

+0

Gracias. No me di cuenta de que podía poner el decorador directamente en la clase en lugar de ponerlo en los métodos. Esa era la información clave que me faltaba. Su decorador de memoise no es * bastante * lo que necesito porque las cadenas no son simples como son los números (y por lo tanto los 'id' no son únicos de una cadena idéntica a otra), pero para mis necesidades simplificadas pude simplemente usa el primer argumento directamente como la clave. – robru

+0

@Robru seguramente mi memoria es solo un código rápido que uso en ejemplos, no le presto demasiada atención :) – brandizzi

+0

Por supuesto, después de una hora de perfeccionar tu decorador de memoizes para que funcione con mi configuración particular de clases, se me ocurre que esta solución no funcionará realmente porque tengo varios métodos y funciones que iteran sobre el dict de 'ClassName.instances' para realizar operaciones en todas las instancias cargadas, y esta técnica de memorización en particular mezcla todas las diferentes instancias de diferentes clases en una sola dicción Parece que voy a tener que ir con '__new__' después de todo. – robru

2

Los parámetros a __new__ también se pasan a __init__, por lo que:

def __init__(self, flubid): 
    ... 

tiene que aceptar el argumento flubid allí, incluso si no lo utiliza en __init__

Esta es la relevante comentario tomado de typeobject.c in 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. 

*/ 
+0

Lo que dices tiene sentido, pero ¿cómo funciona mi ejemplo trivial sin definir '__init__' en absoluto? ¿No debería también darme errores sobre el número incorrecto de argumentos pasados? – robru

+0

@Robru, actualicé mi respuesta con la explicación dada en 'typeobject.c' –

+0

Oh, ok. Gracias. – robru

3

La solución que terminé usando es la siguiente:

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 decorar la clasecon esto, no __init__. Aunque Brandizzi me proporcionó esa información clave, su decorador de ejemplo no funcionó como lo deseaba.

me encontré con este concepto bastante sutil, pero básicamente cuando se está utilizando decoradores en Python, es necesario comprender que la cosa que se decorado (si se trata de un método o una clase) es en realidad reemplazado por el decorador propio . Así, por ejemplo, cuando yo iba a tratar de acceder Photograph.instances o Camera.generate_id() (un métodoestático), en realidad no podía acceder a ellos porque Photograph en realidad no se refieren a la clase de fotografía original, se refiere a la funciónmemoized(del ejemplo de Brandizzi)

Para evitar esto, tuve que crear una clase de decorador que realmente tomara todos los atributos y métodos estáticos de la clase decorada y los expuso como propios. Casi como una subclase, excepto que la clase de decorador no sabe de antemano qué clases va a decorar, por lo que tiene que copiar los atributos después del hecho.

El resultado final es que cualquier instancia de la clase memoize se 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

Cuestiones relacionadas