Domanda In Python, come si modifica un oggetto istanziato dopo una ricarica?


Diciamo che hai un oggetto che è stato istanziato da una classe all'interno di un modulo. Ora ricarichi quel modulo. La prossima cosa che vorresti fare è far sì che la ricarica influenzi quella classe.

mymodule.py
---
class ClassChange():
    def run(self):
        print 'one'

myexperiment.py
---
import mymodule
from mymodule import ClassChange  # why is this necessary?
myObject = ClassChange()
myObject.run()
>>> one
### later, i changed this file, so that it says print 'two'

reload(mymodule)
# trick to change myObject needed here
myObject.run()
>>> two

Devi creare un nuovo oggetto ClassChange, copiare myObject in quello ed eliminare il vecchio myObject? O c'è un modo più semplice?

Modifica: il metodo run () sembra un metodo di stile di classe statico, ma solo per brevità. Mi piacerebbe che il metodo run () operi sui dati all'interno dell'oggetto, quindi una funzione del modulo statico non farebbe ...


13
2017-07-03 20:03


origine


risposte:


Per aggiornare tutte le istanze di una classe, è necessario tenere traccia di alcune di queste istanze, in genere tramite riferimenti deboli (il valore debole è dettato dalla mano e generale), quindi la funzionalità "tenere traccia" non impedirà alle istanze non necessarie di andare via, ovviamente!

Normalmente si desidera mantenere un tale contenitore nell'oggetto di classe, ma, in questo caso, dal momento che si ricarica il modulo, ottenere il vecchio oggetto di classe non è banale; è più semplice lavorare a livello di modulo.

Quindi, diciamo che un "modulo aggiornabile" deve definire, all'inizio, un valore debole dict (e un ausiliario "next key to use" int) con, ad esempio, nomi convenzionali:

import weakref
class _List(list): pass   # a weakly-referenceable sequence
_objs = weakref.WeakValueDictionary()
_nextkey = 0
def _register(obj):
  _objs[_nextkey] = List((obj, type(obj).__name__))
  _nextkey += 1

Ogni classe nel modulo deve avere, in genere in __init__, una chiamata _register(self) per registrare nuove istanze.

Ora la "funzione di ricarica" ​​può ottenere il roster di tutte le istanze di tutte le classi in questo modulo ottenendo una copia di _objs prima che ricarichi il modulo.

Se tutto ciò che serve è cambiare il codice, quindi la vita è ragionevolmente facile:

def reload_all(amodule):
    objs = getattr(amodule, '_objs', None)
    reload(amodule)
    if not objs: return  # not an upgraable-module, or no objects
    newobjs = getattr(amodule, '_objs', None)
    for obj, classname in objs.values():
        newclass = getattr(amodule, classname)
        obj.__class__ = newclass
        if newobjs: newobjs._register(obj)

Purtroppo, in genere si vuole dare alla nuova classe la possibilità di aggiornare un oggetto della vecchia classe su se stesso più finemente, ad es. con un metodo di classe adatto. Anche questo non è troppo difficile:

def reload_all(amodule):
    objs = getattr(amodule, '_objs', None)
    reload(amodule)
    if not objs: return  # not an upgraable-module, or no objects
    newobjs = getattr(amodule, '_objs', None)
    for obj, classname in objs:
        newclass = getattr(amodule, classname)
        upgrade = getattr(newclass, '_upgrade', None)
        if upgrade:
            upgrade(obj)
        else:
            obj.__class__ = newclass
        if newobjs: newobjs._register(obj)

Ad esempio, supponiamo che la nuova versione della classe Zap abbia cambiato il nome di un attributo da foo a bar. Questo potrebbe essere il codice del nuovo Zap:

class Zap(object):
    def __init__(self):
        _register(self)
        self.bar = 23

    @classmethod
    def _upgrade(cls, obj):
        obj.bar = obj.foo
        del obj.foo
        obj.__class__ = cls

Questo NON è tutto - c'è molto di più da dire sull'argomento - ma, è l'essenza, e la risposta è già abbastanza lunga (e io, abbastanza esausto ;-).


13
2017-07-03 21:59



Devi creare un nuovo oggetto. Non c'è modo di aggiornare magicamente gli oggetti esistenti.

Leggi il reload documentazione integrata - è molto chiaro. Ecco l'ultimo paragrafo:

Se un modulo crea istanze di una classe, ricaricare il modulo che definisce la classe non influisce sulle definizioni dei metodi delle istanze - continuano a utilizzare la vecchia definizione di classe. Lo stesso vale per le classi derivate.

Ci sono altri avvertimenti nella documentazione, quindi dovresti leggerlo e prendere in considerazione le alternative. Forse vuoi iniziare una nuova domanda con perché tu vuoi usare reload e chiedere altri modi per ottenere la stessa cosa.


6
2017-07-03 20:04



Il mio approccio a questo è il seguente:

  1. Guarda tutti i moduli importati e ricarica solo quelli con un nuovo file .py (rispetto al file .pyc esistente)
  2. Per ogni funzione e metodo di classe che viene ricaricato, impostare old_function .__ code__ = new_function .__ code__.
  3. Per ogni classe ricaricata, utilizzare gc.get_referrers per elencare le istanze della classe e impostare l'attributo __class__ sulla nuova versione.

I vantaggi di questo approccio sono:

  • Di solito non è necessario ricaricare i moduli in un ordine particolare
  • Di solito è sufficiente ricaricare i moduli con codice modificato e non di più
  • Non è necessario modificare le classi per tenere traccia delle loro istanze

Puoi leggere la tecnica (e le sue limitazioni) qui: http://luke-campagnola.blogspot.com/2010/12/easy-automated-reloading-in-python.html

E puoi scaricare il codice qui: http://luke.campagnola.me/code/downloads/reload.py


4
2018-04-03 21:58



Devi ottenere la nuova classe dal nuovo modulo e assegnarla nuovamente all'istanza.

Se puoi attivare questa operazione ogni volta che usi un'istanza con questo mixin:

import sys

class ObjDebug(object):
  def __getattribute__(self,k):
    ga=object.__getattribute__
    sa=object.__setattr__
    cls=ga(self,'__class__')
    modname=cls.__module__ 
    mod=__import__(modname)
    del sys.modules[modname]
    reload(mod)
    sa(self,'__class__',getattr(mod,cls.__name__))
    return ga(self,k)

3
2018-04-05 17:09



Il seguente codice fa quello che vuoi, ma per favore non usarlo (almeno non finché non sarai sicuro di fare la cosa giusta), La sto postando solo a scopo esplicativo.

mymodule.py:

class ClassChange():
    @classmethod
    def run(cls,instance):
        print 'one',id(instance)

myexperiment.py:

import mymodule

myObject = mymodule.ClassChange()
mymodule.ClassChange.run(myObject)

# change mymodule.py here

reload(mymodule)
mymodule.ClassChange.run(myObject)

quando nel tuo codice ti instanciate myObject, ottieni un'istanza di ClassChange. Questa istanza ha un metodo di istanza chiamato run. L'oggetto mantiene questo metodo di istanza (per la ragione spiegata da nosklo) anche durante il ricaricamento, poiché ricaricare solo ricarica il classe  ClassChange.

In il mio codice sopra, run è un metodo di classe. I metodi di classe sono sempre vincolati e operano sulla classe, non sull'istanza (motivo per cui viene solitamente chiamato il loro primo argomento clsno self). wenn ClassChange è ricaricato, così è questo metodo di classe.

Puoi vedere che anche io passo il esempio come argomento per lavorare con la corretta (stessa) istanza di ClassChange. Lo puoi vedere perché lo stesso ID oggetto è stampato in entrambi i casi.


2
2017-07-03 20:24



Non sono sicuro se questo è il modo migliore per farlo, o si integra con quello che vuoi fare ... ma questo potrebbe funzionare per te. Se vuoi cambiare il comportamento di un metodo, per tutti gli oggetti di un certo tipo ... basta usare una variabile di funzione. Per esempio:


def default_behavior(the_object):
  print "one"

def some_other_behavior(the_object):
  print "two"

class Foo(object):
  # Class variable: a function that has the behavior
  # (Takes an instance of a Foo as argument)
  behavior = default_behavior

  def __init__(self):
    print "Foo initialized"

  def method_that_changes_behavior(self):
    Foo.behavior(self)

if __name__ == "__main__":
  foo = Foo()
  foo.method_that_changes_behavior() # prints "one"
  Foo.behavior = some_other_behavior
  foo.method_that_changes_behavior() # prints "two"

  # OUTPUT
  # Foo initialized
  # one
  # two

Ora puoi avere una classe che è responsabile del ricaricamento dei moduli e, dopo aver ricaricato, l'impostazione Foo.behavior a qualcosa di nuovo. Ho provato questo codice. Funziona benissimo :-).

Per te funziona?


1
2017-07-03 20:26



Ci sono trucchi per rendere ciò che vuoi possibile.

Qualcuno ha già detto che è possibile avere una classe che mantiene un elenco delle sue istanze e quindi cambiare la classe di ogni istanza in quella nuova dopo averla ricaricata.

Tuttavia, non è efficiente. Un metodo migliore consiste nel cambiare la vecchia classe in modo che sia uguale alla nuova classe.


1
2017-07-04 01:38