In Python, how do you change an instantiated objec

2019-03-13 06:17发布

问题:

Let's say you have an object that was instantiated from a class inside a module. Now, you reload that module. The next thing you'd like to do is make that reload affect that class.

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

Do you have to make a new ClassChange object, copy myObject into that, and delete the old myObject? Or is there a simpler way?

Edit: The run() method seems like a static class style method but that was only for the sake of brevity. I'd like the run() method to operate on data inside the object, so a static module function wouldn't do...

回答1:

To update all instances of a class, it is necessary to keep track somewhere about those instances -- typically via weak references (weak value dict is handiest and general) so the "keeping track" functionality won't stop unneeded instances from going away, of course!

You'd normally want to keep such a container in the class object, but, in this case, since you'll be reloading the module, getting the old class object is not trivial; it's simpler to work at module level.

So, let's say that an "upgradable module" needs to define, at its start, a weak value dict (and an auxiliary "next key to use" int) with, say, conventional names:

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

Each class in the module must have, typically in __init__, a call _register(self) to register new instances.

Now the "reload function" can get the roster of all instances of all classes in this module by getting a copy of _objs before it reloads the module.

If all that's needed is to change the code, then life is reasonably easy:

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)

Alas, one typically does want to give the new class a chance to upgrade an object of the old class to itself more finely, e.g. by a suitable class method. That's not too hard either:

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)

For example, say the new version of class Zap has renamed an attribute from foo to bar. This could be the code of the new 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

This is NOT all -- there's a LOT more to say on the subject -- but, it IS the gist, and the answer is WAY long enough already (and I, exhausted enough;-).



回答2:

You have to make a new object. There's no way to magically update the existing objects.

Read the reload builtin documentation - it is very clear. Here's the last paragraph:

If a module instantiates instances of a class, reloading the module that defines the class does not affect the method definitions of the instances — they continue to use the old class definition. The same is true for derived classes.

There are other caveats in the documentation, so you really should read it, and consider alternatives. Maybe you want to start a new question with why you want to use reload and ask for other ways of achieving the same thing.



回答3:

My approach to this is the following:

  1. Look through all imported modules and reload only those with a new .py file (as compared to the existing .pyc file)
  2. For every function and class method that is reloaded, set old_function.__code__ = new_function.__code__.
  3. For every reloaded class, use gc.get_referrers to list instances of the class and set their __class__ attribute to the new version.

Advantages to this approach are:

  • Usually no need to reload modules in any particular order
  • Usually only need to reload the modules with changed code and no more
  • Don't need to modify classes to keep track of their instances

You can read about the technique (and its limitations) here: http://luke-campagnola.blogspot.com/2010/12/easy-automated-reloading-in-python.html

And you can download the code here: http://luke.campagnola.me/code/downloads/reload.py



回答4:

You have to get the new class from the fresh module and assign it back to the instance.

If you could trigger this operation anytime you use an instance with this 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)


回答5:

The following code does what you want, but please don't use it (at least not until you're very sure you're doing the right thing), I'm posting it for explanation purposes only.

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)

When in your code you instanciate myObject, you get an instance of ClassChange. This instance has an instance method called run. The object keeps this instance method (for the reason explained by nosklo) even when reloading, because reloading only reloads the class ClassChange.

In my code above, run is a class method. Class methods are always bound to and operate on the class, not the instance (which is why their first argument is usually called cls, not self). Wenn ClassChange is reloaded, so is this class method.

You can see that I also pass the instance as an argument to work with the correct (same) instance of ClassChange. You can see that because the same object id is printed in both cases.



回答6:

I'm not sure if this is the best way to do it, or meshes with what you want to do... but this may work for you. If you want to change the behavior of a method, for all objects of a certain type... just use a function variable. For example:


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

You can now have a class that is responsible for reloading modules, and after reloading, setting Foo.behavior to something new. I tried out this code. It works fine :-).

Does this work for you?



回答7:

There are tricks to make what you want possible.

Someone already mentioned that you can have a class that keeps a list of its instances, and then changing the class of each instance to the new one upon reload.

However, that is not efficient. A better method is to change the old class so that it is the same as the new class.