¿Escribe dos versiones de la clase con dos súper clases diferentes sin violar DRY?

7

Tengo una clase llamada LimitedDict , que es un diccionario que limita la cantidad de entradas que puede contener al eliminar entradas antiguas cuando se agrega una nueva. Actualmente se hereda de la clase dict de Python. Ahora necesito exactamente la misma clase, solo quiero que se herede de defaultdict para que tenga valores predeterminados. ¿Cómo puedo hacer esto sin solo copiar y pegar el código?

He pensado en convertirlo en un envoltorio, pero eso crea una gran cantidad de código repetitivo.

    
pregunta sinθ 18.03.2015 - 16:41

4 respuestas

30

Derivar una clase "LimitedDict" de dict con el comportamiento descrito probablemente violará el principio de sustitución de Liskov . La mayoría de los códigos que usan diccionarios esperarán que todos los datos que coloques en un dict se quedarán allí y no se desvanecerán repentinamente. Por lo tanto, no puede usar fácilmente un LimitedDict como reemplazo de dict en la mayoría de los lugares.

Por eso no es una buena idea derivar tal clase de dict , implementarla mejor utilizando un dict. Es el viejo mantra: en caso de duda, favorecer la composición sobre la herencia. El atributo de "almacenamiento" interno de su clase podría ser un dict estándar o un defaultdict , y puede implementar algunos mecanismos para elegir entre esos dos (por ejemplo, inyectando el objeto dict a través del constructor), que resuelve tu problema inmediatamente.

Puede llamar a esto "hacer una envoltura", y sí, tendrá que agregar un poco de código repetitivo, pero según mi experiencia, este es un pequeño precio por los dolores de cabeza de los que se ahorrará cuando más tarde tenga que mantener ese código.

    
respondido por el Doc Brown 18.03.2015 - 17:28
5

En este caso específico , no necesita subclase defaultdict , porque defaultdict no es mucho más que una subclase dict con un __missing__ method .

Puede simplemente subclase LimitedDict y agregar ese método a la subclase:

class DefaultLimitedDict(LimitedDict):
    def __init__(self, factory, *args, **kw):
        self.default_factory = factory
        super().__init__(*args, **kw)

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        self[key] = new_value = self.default_factory()
        return new_value

En una situación más genérica, podría pasar a utilizar la herencia múltiple; mueva sus métodos adicionales o comportamiento personalizado a una clase de mix-in y herede de dict o defaultdict y su mix-in:

class LimitedDict(DictLimiter, dict):
    # methods will first be looked up on DictLimiter, then dict

class DefaultLimitedDict(DictLimiter, defaultdict):
    # methods will first be looked up on DictLimiter, then dict
    
respondido por el Martijn Pieters 18.03.2015 - 17:31
3

Estás buscando algo como un mixin. Una mezcla es como una subclase que puede aplicar a varias superclases, para crear una nueva clase combinada. Ya que Python admite herencia múltiple, esto es bastante fácil de hacer:

class A(object):
  def foo(self):
    print("A::foo")
  def bar(self):
    print("A::bar")

# a variation of A
class B(A):
  def foo(self):
    print("B::foo")

# a mixin that can be applied to any A -- that is A or B
class Mixin(A):
  def foo(self):
    print("before foo")
    super(Mixin, self).foo()
  def bar(self):
    super(Mixin, self).bar()
    print("after bar")

# Mixin + A
# actually, that's the same as Mixin itself
class MixedA(Mixin, A):
  pass

# Mixin + B
class MixedB(Mixin, B):
  pass

a = MixedA()
a.foo()
a.bar()

b = MixedB()
b.foo()
b.bar()

Salida:

before foo
A::foo
A::bar
after bar
before foo
B::foo
A::bar
after bar

que nos dice que las clases combinadas están diseñadas de forma que los Mixin cambios se aplican tanto a las superclases A como a las B como si las hubieras copiado.

    
respondido por el amon 18.03.2015 - 17:35
0

Estoy de acuerdo con el Doc Brown en el tema de la sustitución de Liskov, pero si no hereda de dict, ¿cómo sabrá que respalda la interfaz abstracta correcta? ¿Puedo sugerir usar MutableMapping del módulo de colecciones?

La cadena de documentos para MutableMapping de la fuente se ve así:

"""A MutableMapping is a generic container for associating
key/value pairs.

This class provides concrete generic implementations of all
methods except for __getitem__, __setitem__, __delitem__,
__iter__, and __len__.

"""

Lo que significa que esos métodos mágicos son todo lo que necesitas implementar para obtener la misma funcionalidad de dictado. Puede parecer algo como esto (probado nominalmente en Python 2 y 3):

import collections

class LimitedDict(collections.MutableMapping):

    def __init__(self, maxlen=10):
        self.maxlen = maxlen
        self.latest = collections.deque(maxlen=maxlen)
        self.data = {}
    def __setitem__(self, key, value):
        '''ld[i] = y'''
        item_in = key in self.data
        if len(self.latest) >= self.maxlen and not item_in:
            self.latest.pop()
        elif item_in:
            self.latest.remove(key)
        self.latest.appendleft(key)
        self.data[key] = value
    def __delitem__(self, key):
        '''del ld[i]'''
        del self.data[key]
        self.latest.remove(key) 
    def __getitem__(self, key):
        return self.data[key] 
    def __iter__(self):
        for key in self.latest:
            yield key 
    def __len__(self):
        return len(self.latest)

Y no probé mucho, pero hice esto y me comporto como se esperaba:

def main():
    ld = LimitedDict(3)
    ld['foo'] = 1
    print(ld['foo'])
    ld['bar'] = 2
    print(ld['bar'])
    del ld['bar']
    assert 'bar' not in ld 
    ld['bar'] = 3
    ld['foo'] = 4
    ld['baz'] = 5
    ld['quux'] = 6
    assert 'bar' not in ld
    for key, value in ld.items():
        print(key, value)
    ld.update(foo=7, bar=8, baz=9)
    try:
        del ld['quux']
    except KeyError:
        raised = True
    assert raised
    assert not isinstance(ld, dict)

Esto se ejecuta en Python 2.6 y 3.3, y puedes usar, por ejemplo, iteritems en 2 también.

También, setdefault está disponible, por lo que tampoco necesitas defaultdict . Aquí está la ayuda sobre cómo funciona:

>>> help(dict.setdefault)
Help on method_descriptor:

setdefault(...)
    D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D
    
respondido por el Aaron Hall 18.03.2015 - 23:31

Lea otras preguntas en las etiquetas