Scrivi due versioni di classe con due diverse super classi senza violare DRY?

7

Ho una classe chiamata LimitedDict che è un dizionario che limita il numero di voci che può contenere cancellando vecchie voci quando ne viene aggiunta una nuova. Attualmente eredita dalla classe dict di Python. Ora ho bisogno della stessa identica classe, solo che voglio ereditare da defaultdict in modo che abbia valori predefiniti. Come posso farlo senza copiare e incollare il codice?

Ho pensato di renderlo un wrapper, ma questo crea un sacco di codice boilerplate.

    
posta sinθ 18.03.2015 - 16:41
fonte

4 risposte

30

La derivazione di una classe "LimitedDict" da dict con il comportamento descritto probabilmente viola il principio di sostituzione di Liskov . La maggior parte del codice che usa i dizionari si aspetterà che tutti i dati che inserisci in un dt rimarranno lì e non svaniranno all'improvviso. Quindi non puoi usare facilmente un LimitedDict come sostituto di un dict nella maggior parte dei luoghi.

Ecco perché non è una buona idea derivare una classe del genere da dict , meglio implementarla con utilizzando un dict. È il vecchio mantra: quando sei in dubbio, preferisci la composizione all'eredità. L'attributo "storage" interno della tua classe potrebbe essere uno standard dict o un defaultdict , e puoi implementare alcuni meccanismi per scegliere tra questi due (ad esempio iniettando l'oggetto dict tramite il costruttore), che risolve immediatamente il tuo problema.

Potresti chiamare questo "fare un wrapper", e sì, dovrai aggiungere un po 'di codice boilerplate, ma per la mia esperienza questo è un piccolo prezzo per i mal di testa che ti salveranno da quando in seguito dovrai mantenere quel codice.

    
risposta data 18.03.2015 - 17:28
fonte
5

In questo caso specifico , non devi affatto sottoclasse defaultdict , perché defaultdict non è molto più di una sottoclasse dict con un aggiunto metodo %%_de% .

Puoi semplicemente sottoclasse __missing__ e aggiungi quel metodo alla sottoclasse:

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

In una situazione più generica puoi passare all'utilizzo dell'ereditarietà multipla; sposta i tuoi metodi aggiuntivi o il tuo comportamento personalizzato su una classe mix-in e eredita sia da LimitedDict sia da dict e il tuo 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
    
risposta data 18.03.2015 - 17:31
fonte
3

Stai cercando qualcosa come un mixin. Una miscelazione è come una sottoclasse che puoi applicare a più superclassi, per creare una nuova classe combinata. Poichè Python supporta l'ereditarietà multipla, questo è abbastanza facile da fare:

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

Output:

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

che ci dice che le classi combinate sono architettate in modo tale che le modifiche di Mixin si applichino a entrambe le superclassi A e B come se le aveste copiate e incollate.

    
risposta data 18.03.2015 - 17:35
fonte
0

Sono d'accordo con Doc Brown sulla questione della sostituzione di Liskov, ma se non si eredita da Ditt, come saprai che stai supportando la giusta interfaccia astratta? Posso suggerire di usare MutableMapping dal modulo delle collezioni?

La docstring per MutableMapping della sorgente ha il seguente aspetto:

"""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__.

"""

Ciò significa che quei metodi magici sono tutto ciò che devi implementare per ottenere la stessa funzionalità dict. Potrebbe sembrare qualcosa del genere (testato nominalmente in Python 2 e 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)

E non ha testato molto, ma l'ho fatto e si comporta come previsto:

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)

Funziona sia in Python 2.6 e 3.3, e puoi usare ad es. iteritems anche in 2.

Inoltre, setdefault è disponibile, quindi non hai davvero bisogno di defaultdict , neanche. Ecco l'aiuto su come funziona:

>>> 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
    
risposta data 18.03.2015 - 23:31
fonte

Leggi altre domande sui tag