-1

Description

My question is very similar to the question raised here, with one difference: I do not want to directly edit or replace anything in the original class instance.

So, basically, imagine that we have a class defined as

class Class1(object):
    """
    This is Class1.

    """

    def __init__(self, name):
        self.name = name

    def a(self):
        "Method a"
        return("This is the Class1 a()-method!")

    def b(self):
        "Method b"
        return("This is the Class1 b()-method!")

    def bb(self):
        "Method bb"
        return(self.b())

Now what I want to do is create a function that the user can call, where it provides me with an instance of Class1 (or any derived subclass). Then, I return an object that behaves exactly the same as the provided instance, except that the b()-method has been replaced with

def b(self):
    return('This is the new Class1 b()-method!')

Everything else about this object must be exactly the same as it was in the provided instance, such that the new object can be used in any place the old one could. It is basically as if the definition of Class1 (or any used subclass) already used this definition of b() to begin with.

Attempts

So, I have already tried a few things, but every one of them has at least one problem that I do not like. Note that I left out any checks to see if a provided object is indeed an instance of the Class1 class or any derived subclasses, as it adds nothing to my descriptions.


I tried using the solution that was given here, which gave me the following:

from types import MethodType


# Define handy decorator for overriding instance methods
def override_method(instance):
    def do_override(method):
        setattr(instance, method.__name__, MethodType(method, instance))

    return(do_override)


# Define function that returns new Class1 object
def get_new_Class1_obj(class1_obj):
    # Override b-method
    @override_method(class1_obj)
    def b(self):
        return('This is the new Class1 b()-method!')

    # Return it
    return(class1_obj)

This solution basically does everything I want, except that it directly replaces the b()-method in the provided class1_obj with the new definition. Therefore, once the get_new_Class1_obj()-function has been called, the original class1_obj can no longer be used in its original state. This is a problem, as for the application I have, I actually require the original b()-method to still be available (I simply did not use such a thing in this example to keep it a bit simple).


Another way I tried doing this involved using a class factory (I have tried several different versions of this, with below probably the closest I have to getting what I want):

# Define function that returns new Class1 object
def get_new_Class1_obj(class1_obj):
    # Define list with all methods that are overridden
    override_attrs = ['__init__', '__getattribute__', '__dir__', 'b']

    # Make a subclass of the class used for class1_obj
    # This is required for functions like 'isinstance'
    class new_Class1(class1_obj.__class__, object):
        # Empty __init__ as no initialization is required
        def __init__(self):
            pass

        # Make sure only overridden attributes are used from this class
        def __getattribute__(self, name):
            if name in override_attrs:
                return(super().__getattribute__(name))
            else:
                return(getattr(class1_obj, name))

        # Use the __dir__ of class1_obj
        def __dir__(self):
            return(dir(class1_obj))

        # Override b-method
        def b(self):
            return("This is the new Class1 b()-method!")

    # Initialize new_Class1
    new_class1_obj = new_Class1()

    # Return it
    return(new_class1_obj)

This is also very close to what I want (even though it is annoying to keep updating the override_attrs list) and I am now able to use the original class1_obj within the new_class1_obj if I want. However, the problem here is that the bb()-method of the new_class1_obj will not work properly, as it will use the b()-method of class1_obj and not the one of new_class1_obj. As far as I know, there is no way to enforce this without knowing that the method bb() exists in a form like this. As it would be possible that somebody subclasses Class1 and introduces a c()-method that calls b(), this solution would fail to work properly (while that would work properly with the first solution).

Not subclassing Class1 here would get rid of some annoyances, but it would also mean that functions such as isinstance do not work properly anymore, while not fixing the problem with the bb()-method.

Solution?

So, I currently cannot come up with a solution for this (I have been trying for a few days now). I am considering using my first attempt solution, but instead of immediately replacing the b()-method, I first assign b() to something like _old_b() or _b() (obviously making sure it does not exist already) and then replace b(). I do not really like that solution, as it still feels way too hacky and dirty to me.

So, does anybody have an idea for this? In my mind, it sounds like a very simple problem: I have an instance and I want to update one of its instance methods with a new one, without modifying the original instance. But, it seems that is not so simple after all.

Example

A full example use of this would be:

# Define original Class1 class
class Class1(object):
    """
    This is Class1.

    """

    def __init__(self, name):
        self.name = name

    def a(self):
        "Method a"
        return("This is the Class1 a()-method!")

    def b(self):
        "Method b"
        return("This is the Class1 b()-method!")

    def bb(self):
        "Method bb"
        return(self.b())


# Define new b()-method
def b(self):
    # Return both old and new b() output
    return(class1_obj.b(), "This is the new Class1 b()-method!")


# Define function that returns new Class1 object
def get_new_Class1_obj(class1_obj):
    <code involving getting new_class1_obj>
# Use with expected outputs
>>> class1_obj = Class1('TEST')
>>> new_class1_obj = get_new_Class1_obj(class1_obj)
>>> class1_obj is new_class1_obj
False
>>> class1_obj.name
'TEST'
>>> class1_obj.a()
"This is the Class1 a()-method!"
>>> class1_obj.b()
"This is the Class1 b()-method!"
>>> class1_obj.bb()
"This is the Class1 b()-method!"
>>> new_class1_obj.name
'TEST'
>>> new_class1_obj.a()
"This is the Class1 a()-method!"
>>> new_class1_obj.b()
("This is the Class1 b()-method!", "This is the new Class1 b()-method!")
>>> new_class1_obj.bb()
("This is the Class1 b()-method!", "This is the new Class1 b()-method!")
>>> class1_obj.name = 'TEST2'
>>> class1_obj.name
'TEST2'
>>> new_class1_obj.name
'TEST2'

1313e
  • 1,112
  • 9
  • 17
  • In what circumstances/how is the original `b()` function for `class1_obj` called? Perhaps that would give us an idea on how to proceed – codelessbugging May 01 '19 at 09:45
  • @CodelessBugging Well, if for example the user wants to use both `class1_obj` and `new_class1_obj` simultaneously. However, I also require that the original `b()`-method can be called from `new_class1_obj`. EDIT: I have added an example. – 1313e May 01 '19 at 09:56
  • Your problem seems to stem from the `bb()` method which still needs to call the old `b()` method whether in the original instance or the new instance, but any outside caller on the new instance would be redirected to the new `b()`. It seems that you should model this directly in the classes. – quamrana May 01 '19 at 10:26
  • @quamrana No, I want that the `bb()`-method in `new_class1_obj` uses the new `b()`-method, and not the old one. Only methods defined by `new_class1_obj` itself are allowed to use the old `b()`-method. – 1313e May 01 '19 at 10:28
  • _I have an instance and I want to update one of its instance methods with a new one, without modifying the original instance._ There is a contradiction. Updating of instance methods means modifying the instance. – sanyassh May 01 '19 at 10:31
  • You need to be clear. Which instances (old and new) can use the old and new `b()` methods? – quamrana May 01 '19 at 10:32
  • @quamrana It is not instance specific, but method specific. Only those methods that were replaced/overridden are allowed to use the old `b()`-method. All methods can use the new one. – 1313e May 01 '19 at 10:34
  • I'm still confused. You need to provide an example usage: Instantiate the original and use it. Create a new changed instance. Use both the original again and new instance to see the differences between how the original started out and how the original behaves now and the new instance behaves. – quamrana May 01 '19 at 10:39
  • @quamrana I have added an example with expected outputs. – 1313e May 01 '19 at 10:59

3 Answers3

0

I'm not absolutely sure of what you want, but can this below be suitable? (file test.py tested ok with python 3.7.3)

class Class1(object):
    """
    This is Class1.

    """

    def __init__(self, name):
        self.name = name

    def a(self):
        "Method a"
        return("This is the Class1 a()-method!")

    def b(self):
        "Method b"
        return("This is the Class1 b()-method!")

    def bb(self):
        "Method bb"
        return(self.b())

def get_new_Class1_obj(class1_obj):

    new_Class1_obj = Class1(class1_obj.name)

    def b():
        return(class1_obj.b(), "This is the new Class1 b()-method!")

    new_Class1_obj.b = b
    return new_Class1_obj

if __name__ == "__main__":
    class1_obj = Class1('TEST')
    new_class1_obj = get_new_Class1_obj(class1_obj)
    print("are insts same? ",class1_obj is new_class1_obj)
    print("class1_obj.name?",class1_obj.name)
    print("class1_obj.a():",class1_obj.a())
    print("class1_obj.b():",class1_obj.b())
    print("class1_obj.bb():",class1_obj.bb())
    print("new_class1_obj.name?",new_class1_obj.name)
    print("new_class1_obj.a():",new_class1_obj.a())
    print("new_class1_obj.b():",new_class1_obj.b())
    print("new_class1_obj.bb():",new_class1_obj.bb())

This code will return:

$ python test.py
are insts same?  False
class1_obj.name? TEST
class1_obj.a(): This is the Class1 a()-method!
class1_obj.b(): This is the Class1 b()-method!
class1_obj.bb(): This is the Class1 b()-method!
new_class1_obj.name? TEST
new_class1_obj.a(): This is the Class1 a()-method!
new_class1_obj.b(): ('This is the Class1 b()-method!', 'This is the new Class1 b()-method!')
new_class1_obj.bb(): ('This is the Class1 b()-method!', 'This is the new Class1 b()-method!')

Is this that you want?

I reviewed my answer, it seems to me this time to be close to your example above. What do you think about it?

Robert
  • 1
  • 1
  • It is close to what I want, but this requires me to know that the `bb()`-method exists. As I don't have this knowledge at all times (as shown in my post), I sadly cannot do it this way. – 1313e May 01 '19 at 10:49
  • I reviewed my answer, it seems to me this time to be close to your example above. What do you think about it? – Robert May 01 '19 at 12:40
  • Note: the updated answer of @quamrana, above, is better than mine if you need a deepcopy – Robert May 01 '19 at 13:17
  • Thanks for your answer, but this requires to reinitialize the instance all the way, which I don't want. The old and new instances should still be linked to each other. – 1313e May 02 '19 at 01:13
0

I don't know if this is what you want, but the code below has the same outputs as your example:

import copy

class ComplexObject:   # Equivalent of old Class1, but with no methods
    def __init__(self, name):
        self.name = name
        self.other_member = 'other, but could be intracomm'    

class Class1(object):
    def __init__(self, fwd):
        self.fwd = fwd  # All the interesting data is in this instance

    def get_name(self):
        return self.fwd.name  # Need to go through fwd to get at data
    def a(self):
        "Method a"
        return("This is the Class1 a()-method!")

    def b(self):
        "Method b"
        return("This is the Class1 b()-method!")

    def bb(self):
        "Method bb"
        return(self.b())

# Define function that returns new Class1 object
def get_new_Class1_obj(class1_obj):
    def b(self):
            # Return both old and new b() output
            return(class1_obj.b(), "This is the new Class1 b()-method!")
    new_instance = copy.copy(class1_obj)
    def b_wrapper():
        return b(new_instance)
    new_instance.b = b_wrapper

    return new_instance

complex=ComplexObject('TEST')
class1_obj = Class1(complex)
new_class1_obj = get_new_Class1_obj(class1_obj)
print("are insts same? ",class1_obj is new_class1_obj)
print("class1_obj.name",class1_obj.get_name())
print("class1_obj.a():",class1_obj.a())
print("class1_obj.b():",class1_obj.b())
print("class1_obj.bb():",class1_obj.bb())
print("new_class1_obj.name",new_class1_obj.get_name())
print("new_class1_obj.a():",new_class1_obj.a())
print("new_class1_obj.b():",new_class1_obj.b())
print("new_class1_obj.bb():",new_class1_obj.bb())
#Change some of the interesting data
class1_obj.fwd.name='FOO'
print("class1_obj.name",class1_obj.get_name())
print("new_class1_obj.name",new_class1_obj.get_name())

Output:

are insts same?  False
class1_obj.name TEST
class1_obj.a(): This is the Class1 a()-method!
class1_obj.b(): This is the Class1 b()-method!
class1_obj.bb(): This is the Class1 b()-method!
new_class1_obj.name TEST
new_class1_obj.a(): This is the Class1 a()-method!
new_class1_obj.b(): ('This is the Class1 b()-method!', 'This is the new Class1 b()-method!')
new_class1_obj.bb(): ('This is the Class1 b()-method!', 'This is the new Class1 b()-method!')
class1_obj.name FOO
new_class1_obj.name FOO

You can see that changes in the interesting data affects both classes.

quamrana
  • 37,849
  • 12
  • 53
  • 71
  • Also very close, but this will not use all attributes that `class1_obj` has. For example, attempting to call `new_class1_obj.name` will not work, as it only exists in `class1_obj`. I will actually add that to the example. – 1313e May 01 '19 at 11:29
  • Interesting idea, but the problem here is that making a deep copy of the instance I'm actually going to use here (`mpi4py.MPI.Intracomm`), is not possible. Making a copy would also mean reinitializing everything, meaning that changes to attributes in the old instance is not updated in the new one. You understand what situation I find myself in? :) – 1313e May 02 '19 at 01:16
  • So maybe you just need a `copy()`. – quamrana May 02 '19 at 11:04
  • `copy()` does not work, as I want the two instances to be linked together. All information in `class1_obj` should be accessible in `new_class1_obj`. This includes changes made. – 1313e May 03 '19 at 02:25
  • Updated with a new idea of moving all the interesting data into a different object. – quamrana May 03 '19 at 07:55
  • I have no control over what `Class1` looks like, so that would not be possible. If I could modify that class, this problem would be significantly easier to solve. – 1313e May 03 '19 at 08:03
0

Possible solution

After looking at the answer given by quamrana, I came up with a possible solution to this, but I would like some feedback on it:

from types import MethodType


# Define original Class1 class
class Class1(object):
    """
    This is Class1.

    """

    def __init__(self, name):
        self.name = name

    def a(self):
        "Method a"
        return("This is the Class1 a()-method!")

    def b(self):
        "Method b"
        return("This is the Class1 b()-method!")

    def bb(self):
        "Method bb"
        return(self.b())


# Define function that returns new Class1 object
def get_new_Class1_obj(class1_obj):
    # Obtain list of all properties that class1_obj has that are not methods
    props = [prop for prop in dir(class1_obj)
             if not isinstance(getattr(class1_obj, prop), MethodType)]

    # Make a subclass of the class used for class1_obj
    class new_Class1(class1_obj.__class__, object):
        # Empty __init__ as no initialization is required
        def __init__(self):
            pass

        # If requested attribute is not a method, use class1_obj for that
        def __getattribute__(self, name):
            if name in props:
                return(getattr(class1_obj, name))
            else:
                return(super().__getattribute__(name))

        # If requested attribute is not a method, use class1_obj for that
        def __setattr__(self, name, value):
            if name in props:
                setattr(class1_obj, name, value)
            else:
                super().__setattr__(name, value)

        # Use the __dir__ of class1_obj
        def __dir__(self):
            return(dir(class1_obj))

        # Define new b()-method
        def b(self):
            # Return both old and new b() output
            return(class1_obj.b(), "This is the new Class1 b()-method!")

    # Initialize new_Class1
    new_class1_obj = new_Class1()

    # Return it
    return(new_class1_obj)
# Do testing
if __name__ == '__main__':
    # Initialize instances
    class1_obj = Class1('TEST')
    new_class1_obj = get_new_Class1_obj(class1_obj)

    # Check that instances are not the same
    print(class1_obj is new_class1_obj)

    # Check outputs of class1_obj
    print(class1_obj.name)
    print(class1_obj.a())
    print(class1_obj.b())
    print(class1_obj.bb())

    # Check outputs of new_class1_obj
    print(new_class1_obj.name)
    print(new_class1_obj.a())
    print(new_class1_obj.b())
    print(new_class1_obj.bb())

    # Check that non-method changes in class1_obj affect new_class1_obj
    class1_obj.name = 'TEST2'
    print(class1_obj.name)
    print(new_class1_obj.name)

    # Check that non-method changes in new_class1_obj affect class1_obj
    new_class1_obj.name = 'TEST3'
    print(class1_obj.name)
    print(new_class1_obj.name)

All outputs here are the outputs I want. I am still not sure if I want non-method changes in new_class1_obj to affect class1_obj, but I can easily remove that by simply not overriding __setattr__(). The above also makes sure that adding a new attribute to new_class1_obj does not affect class1_obj. I could add determining props to __getattribute__ if I do want the opposite to work (adding a new attribute to class1_obj affects new_class1_obj).

1313e
  • 1,112
  • 9
  • 17
  • 1
    Yes, that's quite a good idea: You make your new class follow ALL the non-methods of the old class, even new attributes that may be added later. – quamrana May 07 '19 at 07:34