5

I have a class where I add some attributes dynamically and at some point I want to restore the class to it's pristine condition without the added attributes.

The situation:

class Foo(object):
  pass

Foo.x = 1
# <insert python magic here>
o = Foo() # o should not have any of the previously added attributes
print o.x # Should raise exception

My initial thought was to create a copy of the original class:

class _Foo(object):
  pass

Foo = _Foo
Foo.x = 1
Foo = _Foo # Clear added attributes
o = Foo()
print o.x # Should raise exception

But since Foo is just a reference to _Foo any attributes get added to the original _Foo as well. I also tried

Foo = copy.deepcopy(_Foo)

in case that would help but apparently it does not.

clarification:

The user should not need to care about how the class is implemented. It should, therefore, have the same features of a "normally defined" class, i.e. introspection, built-in help, subclassing, etc. This pretty much rules out anything based on __getattr__

pafcu
  • 7,808
  • 12
  • 42
  • 55
  • Might I ask why you feel this is useful? – Ignacio Vazquez-Abrams Oct 10 '10 at 07:49
  • Because I have a class where I dynamically add methods with a default prefix (e.g. 'm_' in front of all the added stuff) when a module is imported. The user might want to use some other prefix so I need to "re-initialize" the class. – pafcu Oct 10 '10 at 07:53
  • 2
    That sounds like a very broken idea. – Glenn Maynard Oct 10 '10 at 08:03
  • @Glenn Maynard: That sounds like a horribly unconstructive comment that in no way adds to the discussion. – pafcu Oct 10 '10 at 08:05
  • 8
    It's self-evidently broken, and whining that I didn't feel the need to explain something which seems self-evident is horribly unconstructive and in no way adds to the discussion. Maybe you shouldn't whine at the person who just answered your question. – Glenn Maynard Oct 10 '10 at 08:07
  • If you actually want to create a new class with a special set of methods that you fully have at some point: That's trivial with `type`. If you want a class that looks up it's methods in a set of methods that you can change at any point, then you must use `__getattr__` ... which is it? – Jochen Ritzel Oct 10 '10 at 13:38
  • @THC4k: I don't understand what you mean. I can change attributes at any point by using setattr/delattr. Why would I need to use `__getattr__`? – pafcu Oct 10 '10 at 13:44

10 Answers10

5

I agree with Glenn that this is a horribly broken idea. Anyways, here how you'd do it with a decorator. Thanks to Glenn's post as well for reminding me that you can delete items from a class's dictionary, just not directly. Here's the code.

def resetable(cls):
    cls._resetable_cache_ = cls.__dict__.copy()
    return cls

def reset(cls):
    cache = cls._resetable_cache_ # raises AttributeError on class without decorator
    for key in [key for key in cls.__dict__ if key not in cache]:
        delattr(cls, key)
    for key, value in cache.items():  # reset the items to original values
        try:
            setattr(cls, key, value)
        except AttributeError:
            pass

I'm torn on whether to reset the values by catching attempts to update non-assignable attributes with a try as I've shown or building a list of such attributes. I'll leave it up to you.

And here's a use:

@resetable   # use resetable on a class that you want to do this with
class Foo(object):
    pass

Foo.x = 1
print Foo.x
reset(Foo)
o = Foo() 
print o.x # raises AttributeError as expected
aaronasterling
  • 68,820
  • 20
  • 127
  • 125
3

You have to record the original state and restore it explicitly. If the value existed before you changed it, restore that value; otherwise delete the value you set.

class Foo(object):
  pass

try:
    original_value = getattr(Foo, 'x')
    originally_existed = True
except AttributeError:
    originally_existed = False

Foo.x = 1

if originally_existed:
    Foo.x = original_value
else:
    del Foo.x

o = Foo() # o should not have any of the previously added attributes
print o.x # Should raise exception

You probably don't want to be doing this. There are valid cases for monkey patching, but you generally don't want to try to monkey unpatch. For example, if two independent bits of code monkey patch the same class, one of them trying to reverse the action without being aware of the other is likely to break things. For an example of a case where this is actually useful, see https://stackoverflow.com/questions/3829742#3829849.

Community
  • 1
  • 1
Glenn Maynard
  • 55,829
  • 10
  • 121
  • 131
  • I agree. You should not modify external classes. This is a class that modifies itself. The end-user should not change any methods themselves. The addition of methods is more or less transparent. The possibility to change prefix is needed to future-proof the class and should in general not be needed (the methods are added based on external data). The alternative is to not set up the class with a default prefix, but force the user to each time specify it explicitly. This results in more code for the user to write, more possibility for errors, and in general more annoying behaviour. – pafcu Oct 10 '10 at 08:12
  • You should either arrange to have the configuration available before the module is loaded, so you can set the correct method names to begin with, or handle the method names dynamically with a `__getattr__` method. However, if you really want to set methods, then remove them and set them again--then this is how you do it. If you know for sure that the methods won't exist, then you can simply delete them and not worry about storing the original state. – Glenn Maynard Oct 10 '10 at 08:16
  • Or, I suppose, you could ignore the answer entirely. (Welcome to *Standard Overflow*.) – Glenn Maynard Oct 10 '10 at 08:41
  • @pafcu: Having an external variable for each and every added attribute to track is a horrible solution, will not scale well, and requires knowing the names of all the original attributes plus all the names of the added attributes. – Ethan Furman Jan 25 '13 at 16:12
3

You can use inspect and maintain an original list of members and than delete all members that are not in the original list

import inspect
orig_members = []
for name, ref in inspect.getmembers(o):
  orig_members.append(name)
...

Now, when you need to restore back to original

for name, ref in inspect.getmembers(o):
  if name in orig_members:
    pass
  else:
    #delete ref here
aaronasterling
  • 68,820
  • 20
  • 127
  • 125
molicule
  • 5,481
  • 3
  • 29
  • 40
1

The simplest way I found was this:

def foo_maker():
    class Foo(object):
        pass
    return Foo
Foo = foo_maker()
Foo.x = 1
Foo = foo_maker() # Foo is now clean again
o = Foo() # Does not have any of the previously added attributes
print o.x # Raises exception

edit: As pointed out in comments, does not actually reset class but has the same effect in practice.

pafcu
  • 7,808
  • 12
  • 42
  • 55
  • that's not restoring the class to it's original value. That's an entirely new class with the same name. – aaronasterling Oct 11 '10 at 07:37
  • @AaronMcSmooth: Sure, but it has all the properties I wanted it to have. To an external user there is no difference. – pafcu Oct 11 '10 at 07:48
  • Create an instance of a modified `Foo` class. Replace that `Foo` class with a 'clean' version. The old instance still has the new attributes. It's not the same thing at all. Not even in the ballpark. – aaronasterling Oct 11 '10 at 07:53
  • @AaronMcSmooth: I really don't care about that case. This happens to work in my case because I don't need to change old instances (no old instance in the example either), but I agree that it is not a correct general solution which is why I will not accept this solution as the "correct" answer to my question. I merely added this solution here as an alternative. – pafcu Oct 11 '10 at 07:56
0

I don't fully understand why you need this, but I'll have a go. Ordinary inheritance probably won't do because you want to 'reset' to the old state. How about a proxy pattern?

class FooProxy(object):
    def __init__(self, f):
        self.f = foo
        self.magic = {}

    def set_magic(self, k, v):
        self.magic[k] = v

    def get_magic(self, k):
        return self.magic.get(k)

    def __getattr__(self, k):
        return getattr(self.f, k)

    def __setattr__(self, k, v):
        setattr(self.f, k, v)

    f = Foo() 
    p = FooProxy(f)
    p.set_magic('m_bla', 123)

use f for ordinary, 'original' access, use p for proxied access, it should behave mostly like Foo. Re-proxy f with new configuration if you need to

Ivo van der Wijk
  • 16,341
  • 4
  • 43
  • 57
0

I don't know if you can accept an additional module file for class, if you can:

my_class.py

class Foo(object):
  pass

You main script:

import my_class  
Foo = my_class.Foo
Foo.x = 1
p = Foo()
print p.x # Printing '1'

# Some code....

reload(my_class) # reload to reset
Foo = my_class.Foo
o = Foo()
print p.x # Printing '1'
print o.__class__ == p.__class__ # Printing 'False'
print o.x # Raising exception

I am not sure if there is any side-effect. It seems to do what OP wants, though this is really unusal.

livibetter
  • 19,832
  • 3
  • 42
  • 42
  • I strongly recommend avoiding the use of `reload` in production code. You're not restoring the same object, but loading a new one over the old name. Any references to and instances made of the class before reloading will cause breakage. – Glenn Maynard Oct 10 '10 at 08:14
0

In your second example you're making a reference to the class rather than an instance.

Foo = _Foo # Reference

If you instead made an instance copy, what you want to do is exactly the way it will work. You can modify the instance all you want and 'revert' it by creating a new instance.

Foo = _Foo()

#!/usr/bin/python

class FooClass(object): pass

FooInstance = FooClass() # Create an instance

FooInstance.x = 100 # Modify the instance

print dir(FooClass) # Verify FooClass doesn't have an 'x' attribute

FooInstance = FooClass() # Creates a new instance

print FooInstance.x # Exception

synthesizerpatel
  • 27,321
  • 5
  • 74
  • 91
  • No, that will not do since I lose e.g. the ability to do help(Foo) at the python prompt, can't subclass from Foo, etc. – pafcu Oct 10 '10 at 08:20
  • Actually you can do both of those things. help(FooInstance) will present you with FooClass's help. As far as subclassing.. thats a new requirement not mentioned in the question. What you want is to read up on metaprogramming with python. Try these. http://www2.lib.uchicago.edu/~keith/courses/python/class/5/ and http://tim.oreilly.com/pub/a/python/2003/04/17/metaclasses.html – synthesizerpatel Oct 10 '10 at 08:55
0

I don't understand what you are trying to do, but keep in mind that you don't have to add attributes to the class in order to make it look like you added attributes to the class.

You can give the class a __getattr__ method that will be invoked for any missing attribute. Where it gets the value from is up to you:

class MyTrickyClass(object):
    self.magic_prefix = "m_"
    self.other_attribute_source = SomeOtherObject()

    def __getattr__(self, name):
        if name.startswith(self.magic_prefix):
            stripped = name[len(self.magic_prefix):]
            return getattr(self.other_attribute_source, stripped)
        raise AttributeError

m = MyTrickyClass()
assert hasattr(m, "m_other")
MyTrickyClass.magic_prefix = "f_"
assert hasattr(m, "f_other")
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • I guess I should have been more clear that I want the whole process to be as transparent as possible, i.e. a user should not need to know how the class is implemented. Going the __getattr__ route results in many problems, such as lack of introspection that would confuse users. – pafcu Oct 10 '10 at 13:06
0

If all the stuff you added starts with a given distinctive prefix, you could search the object's __dict__ for members with that prefix, and delete them, when it's time to restore.

Russell Borogove
  • 18,516
  • 4
  • 43
  • 50
-1

To create a deep copy of a class you can use the new.classobj function

class Foo:
    pass

import new, copy
FooSaved = new.classobj(Foo.__name__, Foo.__bases__, copy.deepcopy(Foo.__dict__))

# ...play with original class Foo...

# revert changes
Foo = FooSaved

UPD: module new is deprecated. Instead you should use types.ClassType with the same args

Maxim Razin
  • 9,114
  • 7
  • 34
  • 33
  • those are old style classes and your suggestions will only work for such classes. It's not just the module that's deprecated, it's the whole damn scheme you're proposing. – aaronasterling Oct 10 '10 at 09:30