Home > Software engineering >  Create child class instance from parent class instance
Create child class instance from parent class instance

Time:10-29

TL;DR: Python; I have Parent, Child classes. I have an instance of Parent class, parent. Can I make a Child class instance whose super() is parent?

Somewhat specific use case (workaround available) is as follows: I'd like to make an instance of Logger class (from Python logging module), with _log method overloaded. Methods like logger.info or logger.error call this method with a level specified as either INFO or ERROR etc., I'd like to replace this one method, touch nothing else, and make it all work seamlessly.

Here's some things that don't work (well):

  • I can't just inherit from logging.Logger instance and overload this one method and constructor, because Logger instances tend to be created via a factory method, logging.getLogger(name). So I can't just overload the constructor of the wrapper like:
class WrappedLogger(logging.Logger):
    def __init__(self, ...):
        super().__init__(...)

    def _log(self, ...):

and expect it to all work OK.

  • I could make a wrapper class, which provides the methods I'd like to call on the resulting instance, like .info or .error - but then I have to manually come up with all the cases. It also doesn't work well when the _log method is buried a few calls down the stack - there is basically no way to guarantee that any use of the wrapped class will call my desired _log method
  • I can make a little kludge like so:
class WrappedLogger(logging.Logger):
    def __init__(self, parent):
        self._parent = parent

    def _log(...): # overload

    def __getattr__(self, method_name):
        return getattr(self._parent, method_name)

now whenever I have an instance of this class, and call, say, wrapped.info(...), it will retrieve the parent method of info, call it, which will then call self._log which in turn points to my wrapped instance. But this feels very ugly.

  • Similarly, I could take a regular instance of Logger and manually swap out the method; this is maybe a bit less "clever", and less ugly than the above, but similarly underwhelming.

This question has been asked a few times, but in slightly different contexts, where other solutions were proposed. Rather than looking for a workaround, I'm interested in whether there is a native way of constructing a child class instance with the parent instance specified.

Related questions:

CodePudding user response:

If your goal is to supply a custom logger class that is used by getLogger, you can "register" the custom class with the logging manager.

So, let's define a custom logger class

from logging import Logger

class MyLogger(Logger):
    def _log(self, level, msg, *args, **kwargs) -> None:
        print("my logger wants to log:", msg)
        super()._log(level, msg, *args, **kwargs)

Then we tell the global logging manager to use this class instead.

from logging import setLoggerClass

setLoggerClass(MyLogger)

Thank you @Daniil Fajnberg, for pointing out, that setLoggerClass exists.

Now getLogger will instantiate your custom class.

from logging import getLogger

logger = getLogger(__file__)

logger.error("Dummy Error")

This will log the error as normal and also print "my logger wants to log: ...".

Note: The _log method you are overloading is undocumented. Maybe there is a better way to achieve what you want.

CodePudding user response:

If i am understanding correctly, what @Bennet wants is - he has some custom logger classes derived from Logger(Logger acts as interface) like Logger1, Logger2 etc(which implementation gets chosen will vary at runtime). On top of each of this he wants to add some functionality which modifies only the _log function of each of these implementations.

IMO there shouldn't be any direct way to do it, since what you are attempting is trying to modify(not extend) the behaviour of an existing class which is not recommended for OOP paradigm. The hacky way is clever (found it cool).

def __getattr__(self, method_name):
        return getattr(self._parent,  method_name) 

(I don't think you can do the same in Java) P.S. Wanted to comment this but i am poor in SO it seems :)

CodePudding user response:

From the way you keep re-phrasing your more general question, it seems you misunderstand how object creation works. You are asking for a

way of constructing a child class instance with the parent instance specified.

There is no such concept as a "parent instance". Inheritance refers to classes, not objects. This means you need to define yourself, what that term is supposed to mean. How would you define a "parent instance"? What should it be and when and how should it be created?

Just to demonstrate that there is no mechanism for creating "parent instances", when a child class instance is created, consider this:

class Foo:
    instances = []

    def __new__(cls):
        print(f"{cls.__name__}.__new__()")
        instance = super().__new__(cls)
        Foo.instances.append(instance)
        return instance

class Bar(Foo):
    pass

bar = Bar()

assert len(Foo.instances) == 1
assert Foo.instances[0] is bar
assert type(bar) is Bar
assert isinstance(bar, Foo)

The output is Bar.__new__() and obviously the assertions are passed. This goes to show that when we create an instance of Bar it delegates construction further up the MRO chain (because it doesn't implement its own __new__ method), which results in a call to Foo.__new__. It creates the object (by calling object.__new__) and puts it into its instances list. Foo does not also create another instance of class Foo.

You also seem to misunderstand, what calling the super class does, so I suggest checking out the documentation. In short, it is just an elegant tool to access a related class (again: not an instance).

So, again, your question is ill defined.

If you mean (as @Barmar suggested) that you want a way to copy all the attributes of an instance of Foo over to an instance of Bar, that is another story. In that case, you still need to be careful to define, what exactly you mean by "all attributes".

Typically this would refer to the instances __dict__. But do you also want its __slots__ copied? What about methods? Do you want them copied, too? And do you want to just replace everything on the Bar instance or only update those attributes set on the Foo instance?

I hope you see, what I am getting at. I guess the simplest way is just update the instances __dict__ with values from the other one:

...

class Bar(Foo):
    def update_from(self, obj):
        self.__dict__.update(obj.__dict__)

foo = Foo()
foo.n = 1
foo.text = "Hi"

bar = Bar()
bar.update_from(foo)

print(bar.n, bar.text)  # output: `1 Hi`

And you could of course do that in the __init__ method of Bar, if you wanted. If the initialization of Foo is deterministic and instances keep the initial arguments laying around somewhere, you could instead just call the inherited super().__init__ from Bar.__init__ and pass those initial arguments to it from the instance. Something like this:

class Foo:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.z = x   y

class Bar(Foo):
    def __init__(self, foo_obj):
        super().__init__(foo_obj.x, foo_obj.y)

foo = Foo(2, 3)
bar = Bar(foo)
print(bar.z)  # output: `5`

I hope this makes things clearer for you.

  • Related