2012-03-02 11 views
55

This article ha uno snippet che mostra l'utilizzo di __bases__ per modificare dinamicamente la gerarchia di ereditarietà di alcuni codici Python, aggiungendo una classe a un insieme di classi esistenti di classi da cui eredita. Ok, questo è difficile da leggere, il codice è probabilmente più chiara:Come modificare dinamicamente la classe base delle istanze in fase di runtime?

class Friendly: 
    def hello(self): 
     print 'Hello' 

class Person: pass 

p = Person() 
Person.__bases__ = (Friendly,) 
p.hello() # prints "Hello" 

Cioè, Person non eredita dal Friendly a livello sorgente, ma questo rapporto eredità si aggiunge dinamicamente a runtime modifica dell'attributo __bases__ della classe Person. Tuttavia, se si cambia Friendly e Person di essere nuove classi di stile (ereditando da oggetto), si ottiene il seguente errore:

TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object' 

Un po 'di Googling su questo sembra indicate some incompatibilities tra new-style e classi di stile vecchi per quanto riguarda la modifica della gerarchia di ereditarietà in fase di runtime. Nello specifico: "New-style class objects don't support assignment to their bases attribute".

La mia domanda, è possibile fare l'esempio Friendly/Person sopra usando le classi di nuovo stile in Python 2.7+, possibilmente usando l'attributo __mro__?

Disclaimer: Mi rendo perfettamente conto che si tratta di un codice oscuro. Mi rendo perfettamente conto che nei trucchi del codice di produzione reale come questo tendono a delimitare illeggibili, questo è puramente un esperimento mentale, e per i funzies imparare qualcosa su come Python si occupa di problemi legati all'ereditarietà multipla.

+0

È anche piacevole per gli studenti leggere questo se non hanno familiarità con metaclass, type(), ...: http://www.slideshare.net/gwiener/metaclasses-in-python :) – loolooyyyy

+0

Ecco il mio uso Astuccio. Sto importando una libreria che ha una classe B che eredita dalla classe A. – FutureNerd

+2

Ecco il mio caso di utilizzo effettivo. Sto importando una libreria che ha la classe B che eredita dalla classe A. Voglio creare New_A ereditando da A, con new_A_method(). Ora voglio creare New_B ereditando da ... beh, da B come se B erediti da New_A, in modo che i metodi di B, i metodi di A e new_A_method() siano tutti disponibili per le istanze di New_B. Come posso fare questo senza applicare patch per scimmie alla classe A esistente? – FutureNerd

risposta

29

Ok, ancora una volta, questo non è qualcosa che dovresti normalmente fare, questo è solo a scopo informativo.

Dove Python cerca un metodo su un oggetto istanza è determinata dal attributo della classe __mro__ che definisce l'oggetto (la M etodoR isoluzioneO attributo rder). Quindi, se potessimo modificare lo __mro__ di Person, otterremmo il comportamento desiderato. Qualcosa di simile:

setattr(Person, '__mro__', (Person, Friendly, object)) 

Il problema è che __mro__ è un attributo di sola lettura, e quindi setattr non funzionerà. Forse se sei un guru di Python c'è un modo per aggirare questo, ma chiaramente mi manca lo status di guru come non riesco a pensare a uno.

Una possibile soluzione è quella di ridefinire semplicemente la classe:

def modify_Person_to_be_friendly(): 
    # so that we're modifying the global identifier 'Person' 
    global Person 

    # now just redefine the class using type(), specifying that the new 
    # class should inherit from Friendly and have all attributes from 
    # our old Person class 
    Person = type('Person', (Friendly,), dict(Person.__dict__)) 

def main(): 
    modify_Person_to_be_friendly() 
    p = Person() 
    p.hello() # works! 

Quello che non fa è modificare alcun creati in precedenza Person casi ad avere il metodo hello().Per esempio (solo modificando main()):

def main(): 
    oldperson = Person() 
    ModifyPersonToBeFriendly() 
    p = Person() 
    p.hello() 
    # works! But: 
    oldperson.hello() 
    # does not 

Se i dettagli della chiamata type non sono chiare, allora continuate a leggere e-satis' excellent answer on 'What is a metaclass in Python?'.

+0

-1: perché forzare la nuova classe ad ereditare da Frielndly solo quando si poteva altrettanto bene preservare l'originale '__mro__' chiamando:' type ('Person', (Friendly) + Person .__ mro__, dict (Person .__ dict__)) '(e meglio ancora, aggiungi salvaguardie in modo che Friendly non finisca due volte lì dentro). ci sono altri problemi qui - per quanto riguarda dove la classe "Persona" è effettivamente definita e usata: la tua funzione lo cambia solo sul modulo corrente - gli altri moduli che eseguono Person non saranno sicuri - sarà meglio eseguire un monkeypatch sul modulo Persona è definito. (e anche lì ci sono problemi) – jsbueno

+6

'-1' Hai perso totalmente il motivo dell'eccezione in primo luogo. Puoi tranquillamente modificare 'Person .__ class __.__ bases__' in Python 2 e 3, a patto che' Person' non erediti da 'object' ** direttamente **. Vedi le risposte di akaRem e Sam Gulve qui sotto. Questa soluzione alternativa funziona solo attorno al proprio fraintendimento del problema. –

7

Non posso garantire le conseguenze, ma questo codice fa quello che vuoi su py2.7.2.

class Friendly(object): 
    def hello(self): 
     print 'Hello' 

class Person(object): pass 

# we can't change the original classes, so we replace them 
class newFriendly: pass 
newFriendly.__dict__ = dict(Friendly.__dict__) 
Friendly = newFriendly 
class newPerson: pass 
newPerson.__dict__ = dict(Person.__dict__) 
Person = newPerson 

p = Person() 
Person.__bases__ = (Friendly,) 
p.hello() # prints "Hello" 

Sappiamo che questo è possibile. Freddo. Ma non lo useremo mai!

13

ho lottato con questo, e stato incuriosito dalla soluzione, ma Python 3 prende via da noi:

AttributeError: attribute '__dict__' of 'type' objects is not writable 

Io in realtà hanno un bisogno legittimo di un decoratore che sostituisce il (singolo) superclasse della classe decorata. Richiederebbe una descrizione troppo lunga da includere qui (ho provato, ma non sono riuscito a ottenere una lunghezza abbastanza lunga e una complessità limitata) è venuto fuori nel contesto dell'uso da parte di molte applicazioni Python di un server aziendale basato su Python in cui diverse applicazioni richiedevano variazioni leggermente diverse di alcuni dei codici.)

La discussione su questa pagina e altre simili suggeriva che il problema dell'assegnazione a __bases__ si verifica solo per le classi senza superclasse definite (ovvero, la cui unica superclasse è oggetto). Sono stato in grado di risolvere questo problema (sia per Python 2.7 e 3.2) definendo le classi i cui superclasse avevo bisogno di sostituire come sottoclassi di una classe banale:

## T is used so that the other classes are not direct subclasses of object, 
## since classes whose base is object don't allow assignment to their __bases__ attribute. 

class T: pass 

class A(T): 
    def __init__(self): 
     print('Creating instance of {}'.format(self.__class__.__name__)) 

## ordinary inheritance 
class B(A): pass 

## dynamically specified inheritance 
class C(T): pass 

A()     # -> Creating instance of A 
B()     # -> Creating instance of B 
C.__bases__ = (A,) 
C()     # -> Creating instance of C 

## attempt at dynamically specified inheritance starting with a direct subclass 
## of object doesn't work 
class D: pass 

D.__bases__ = (A,) 
D() 

## Result is: 
##  TypeError: __bases__ assignment: 'A' deallocator differs from 'object' 
3

destro del pipistrello, tutti gli avvertimenti di scherzi con la gerarchia di classi dinamicamente sono in vigore.

Ma se deve essere fatto, a quanto pare, c'è un trucco che aggira lo "deallocator differs from 'object" issue when modifying the __bases__ attribute per le nuove classi di stile.

È possibile definire un oggetto di classe

class Object(object): pass 

che deriva una classe da built-in metaclasse type. Ecco, ora le tue nuove classi di stile possono modificare lo __bases__ senza alcun problema.

Nei miei test questo ha funzionato molto bene in quanto tutte le istanze esistenti (prima di cambiarne l'ereditarietà) e le sue classi derivate hanno avvertito l'effetto della modifica incluso il loro mro aggiornato.

1

avevo bisogno di una soluzione per questo che:

  • funziona sia con Python 2 (> = 2.7) e Python 3 (> = 3.2).
  • Consente di modificare le basi di classe dopo aver importato dinamicamente una dipendenza.
  • Consente di modificare le basi di classe dal codice di test dell'unità.
  • Funziona con tipi con metaclasse personalizzato.
  • Permette comunque a unittest.mock.patch di funzionare come previsto.

Ecco cosa mi è venuta:

def ensure_class_bases_begin_with(namespace, class_name, base_class): 
    """ Ensure the named class's bases start with the base class. 

     :param namespace: The namespace containing the class name. 
     :param class_name: The name of the class to alter. 
     :param base_class: The type to be the first base class for the 
      newly created type. 
     :return: ``None``. 

     Call this function after ensuring `base_class` is 
     available, before using the class named by `class_name`. 

     """ 
    existing_class = namespace[class_name] 
    assert isinstance(existing_class, type) 

    bases = list(existing_class.__bases__) 
    if base_class is bases[0]: 
     # Already bound to a type with the right bases. 
     return 
    bases.insert(0, base_class) 

    new_class_namespace = existing_class.__dict__.copy() 
    # Type creation will assign the correct ‘__dict__’ attribute. 
    del new_class_namespace['__dict__'] 

    metaclass = existing_class.__metaclass__ 
    new_class = metaclass(class_name, tuple(bases), new_class_namespace) 

    namespace[class_name] = new_class 

Usato come questo all'interno dell'applicazione:

# foo.py 

# Type `Bar` is not available at first, so can't inherit from it yet. 
class Foo(object): 
    __metaclass__ = type 

    def __init__(self): 
     self.frob = "spam" 

    def __unicode__(self): return "Foo" 

# … later … 
import bar 
ensure_class_bases_begin_with(
     namespace=globals(), 
     class_name=str('Foo'), # `str` type differs on Python 2 vs. 3. 
     base_class=bar.Bar) 

Usa come questo da codice di unit test:

# test_foo.py 

""" Unit test for `foo` module. """ 

import unittest 
import mock 

import foo 
import bar 

ensure_class_bases_begin_with(
     namespace=foo.__dict__, 
     class_name=str('Foo'), # `str` type differs on Python 2 vs. 3. 
     base_class=bar.Bar) 


class Foo_TestCase(unittest.TestCase): 
    """ Test cases for `Foo` class. """ 

    def setUp(self): 
     patcher_unicode = mock.patch.object(
       foo.Foo, '__unicode__') 
     patcher_unicode.start() 
     self.addCleanup(patcher_unicode.stop) 

     self.test_instance = foo.Foo() 

     patcher_frob = mock.patch.object(
       self.test_instance, 'frob') 
     patcher_frob.start() 
     self.addCleanup(patcher_frob.stop) 

    def test_instantiate(self): 
     """ Should create an instance of `Foo`. """ 
     instance = foo.Foo() 
0

Le risposte sopra sono valide se è necessario modificare una classe esistente in fase di runtime. Tuttavia, se stai solo cercando di creare una nuova classe che eredita da qualche altra classe, c'è una soluzione molto più pulita. Ho avuto questa idea da https://stackoverflow.com/a/21060094/3533440, ma penso che l'esempio seguente mostri meglio un caso d'uso legittimo.

def make_default(Map, default_default=None): 
    """Returns a class which behaves identically to the given 
    Map class, except it gives a default value for unknown keys.""" 
    class DefaultMap(Map): 
     def __init__(self, default=default_default, **kwargs): 
      self._default = default 
      super().__init__(**kwargs) 

     def __missing__(self, key): 
      return self._default 

    return DefaultMap 

DefaultDict = make_default(dict, default_default='wug') 

d = DefaultDict(a=1, b=2) 
assert d['a'] is 1 
assert d['b'] is 2 
assert d['c'] is 'wug' 

mi corregga se sbaglio, ma questa strategia sembra molto leggibile per me, e vorrei utilizzarlo nel codice di produzione. Questo è molto simile ai funtori in OCaml.

+1

Non è proprio sicuro di cosa abbia a che fare con la domanda poiché la domanda riguardava davvero il cambiamento dinamico delle classi base in fase di runtime. In ogni caso, qual è il vantaggio di questo solo ereditando da '' 'Map''' direttamente e sovrascrivendo i metodi secondo necessità (proprio come lo standard' '' collections.defaultdict''')? Così come '' 'make_default''' può sempre restituire un solo tipo di cosa, quindi perché non solo rendere' '' DefaultMap''' l'identificatore di primo livello piuttosto che dover chiamare '' 'make_default''' per ottenere il lezione da istanziare? –

+0

Grazie per il feedback! (1) Sono atterrato qui mentre provavo a fare un'eredità dinamica, quindi ho pensato di poter aiutare qualcuno che seguisse il mio stesso percorso. (2) Credo che si possa usare 'make_default' per creare una versione predefinita di un altro tipo di classe simile a un dizionario (' Mappa'). Non puoi ereditare direttamente da 'Map' perché' Map' non è definito fino al runtime. In questo caso, si vorrebbe ereditare direttamente da 'dict', ma immaginiamo che potrebbe esserci un caso in cui non si saprebbe quale classe del dizionario ereditare da fino al runtime. – fredcallaway

Problemi correlati