2

I have a number of Python classes, Say, Class1, Class2, Class3 etc from a library/package. I want to extend all of the classes with some common functionalities. If I extend each class individually, we introduce a lot of redundancy and break the "Don't Repeat Yourself" acronym. So, my thought is to have a Base class and use it extend other classes. For example:

class Base:
    def __init__(self):
        # I want self.base_attr_1, self.base_attr_2 and so on...
    def base_method_1(self, *args, **kwargs):
        pass
    def base_method_2(self, *args, **kwargs):
        pass
    # and so on...

So we can extend Class1, Class2 and so on using maybe Multiple inheritances. Say,

class Class1(Class1, Base):
    pass
class Class2(Class2, Base):
    pass
# and so on...

So that at last when I create an object of Class1, Class2 etc., I can use the Base class attributes and methods. Like;

class_1 = Class1(*args, **kwargs)
print(class_1.base_attr_1)
print(class_1.base_attr_2)
class_1.base_method_1(*args, **kwargs)
class_2.base_method_2(*args, **kwargs)
# and so on..

Please explain how to implement the Class1, Class2 etc, to extend the Base class.

Any help is highly appreciated. Thank you.

2 Answers2

2

following your description, you would have two possibilities to handle your issue:

  1. metaclass
  2. decorator

If I was you, I would try something like this (decorator solution) :

from functools import wraps
def deco(cls):
    def test(x):
        return x**2

    d = {k:v for k,v in locals().items() if k != "cls"}

    @wraps(cls)
    def wrapper(*args, **kwargs):
        o = cls(*args, **kwargs)
        #o.test = test # setattr(o,"test", test) will be better solution,
                       # if you have more elements, which you'd like to add
        # generalized :
        # ============
        for k,v in d.items(): setattr(o,k,v)
        return o

    #Fast replacement of @wraps, but not exhaustive ! 
    #wrapper.__doc__ = cls.__doc__
    #wrapper.__name__ = cls.__name__
    return wrapper

@deco
class A(object):
    pass

a = A()
print(a.__dict__)
print(a.test(10))

Result:

{'test': <function test at 0x02843C70>}
100

As already spoken my solution with metaclass:

class MyMeta(type):
    def __new__(cls, clsname, bases, clsdict):

        def test1(self, x):
            return x ** 2

        def test2(self, x):
            return x * 10
        # filter the elements which aren't in the __new__ signature
        tmp = {k: v for k, v in locals().items() if k not in ("cls", "clsname", "bases", "clsdict")}
        for k, v in tmp.items(): clsdict[k] = v

        return type.__new__(cls, clsname, bases, clsdict)


class A(object):
    __metaclass__ = MyMeta
    pass


a = A()

print(a.__dict__)
print(A.__dict__)
print(a.test1(10))
print(a.test2("ok_?"))

Result:

{}
{'test1': <function test1 at 0x029F73F0>, '__module__': '__main__', 'test2': <function test2 at 0x02A14BB0>, '__metaclass__': <class '__main__.MyMeta'>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
100
ok_?ok_?ok_?ok_?ok_?ok_?ok_?ok_?ok_?ok_?

The difference here is that the test1 and test2 aren't in the instance, but in the class itself, which means that if you subclass these classes they will be in the structure of the new classes also.

Which will be the better solution depends from your case :)

baskettaz
  • 741
  • 3
  • 12
  • 1
    Just added a more generalized form, which will do for any number of parameters – baskettaz Mar 24 '22 at 09:51
  • Thank you, this solution works out of the box. Could you please show me how to implement the metaclass solution too? And if possible the other generalized decorator way which doesn't import `wraps` from `functools`? – kunarapu priyatham Mar 24 '22 at 12:14
  • Also, if I want to use previously existing attributes and methods of class `A` in the `test()` method implementation inside decorator, how can I use them? – kunarapu priyatham Mar 24 '22 at 12:51
  • Hi, just extended the solution to your previous question. What is the reason not to use the *wraps* from the *functools*? You can comment it out and the decorator solution will still work, but it is a good practice to keep it in. – baskettaz Mar 24 '22 at 13:39
  • No reason for not using `wraps` but just thought about how to do that without it. I have a previously implemented `A` class with some attributes (say a `val` attribute) and some methods. How can I use those previous attributes and methods of A in the `test` function along with `x` to implement the `test` functionality? I don't want to use the created object `a` to do it like `a.val` in the `test` function because I want to provide this a library where we don't have pre-existing objects. – kunarapu priyatham Mar 24 '22 at 23:16
  • Thank you so much for the metaclass solution. But, `a.test1(10)` and `a.test2("ok?")` are not working with me. I'm getting the error - `AttributeError: 'A' object has no attribute 'test1'`. I don't know how it is working for you. Could you please directly copy your metaclass code and try checking it? – kunarapu priyatham Mar 24 '22 at 23:16
1

When you have an inheritance setup like this:

class MyClass1(Base, ExternalClass1):

Calling super().__init__() will call Base.__init__() (first inherited class probably) and hence only initialize attributes in Base. You can explicitly call the init functions of all ancestors on the same inheritance "level" like this:

class MyClass1(Base, ExternalClass1):
    def __init__(self) -> None:
        Base.__init__(self)
        ExternalClass1.__init__(self)

That will initialize attributes from both Base and ExternalClass1.

Example:

class ExternalClass1():
    def __init__(self) -> None:
        self.external_attr_1 = "external attr"
    def external_func1(self):
        print("external_func1")


class Base():
    def __init__(self) -> None:
        self.base_attr_1 = "base attr"
    def base_method_1(self):
        print("base_method_1")


class MyClass1(Base, ExternalClass1):
    def __init__(self) -> None:
        Base.__init__(self)
        ExternalClass1.__init__(self)
        self.my_attr_1 = "my class attr"
        self.external_func1()
        self.base_method_1()
        self.my_class_1_func()
        print(self.external_attr_1)
        print(self.base_attr_1)
        print(self.my_attr_1)
    def my_class_1_func(self):
        print("my_class_1_func")

cl1 = MyClass1()

Outputs:

external_func1
base_method_1
my_class_1_func
external attr
base attr
my class attr

Note however that self is now shared between all 3 classes, which can cause clashes. Another pattern is of course to go for composition, and have the external class be a property of MyClass1. This is both safer and introduces less coupling.

EDIT: If you need the same class-name, then you can reference it with the module-name - but I don't think it is very good practice.

import external
...
class MyClass1(Base, external.MyClass1):
    def __init__(self) -> None:
        Base.__init__(self)
        external.MyClass1.__init__(self)
Jeppe
  • 1,830
  • 3
  • 24
  • 33
  • Thank you so much @Jeppe for your effort. If we change the `ExternalClass1` and `MyClass1` to the same names, I'm getting maximum recursion depth error. As I want to extend from a class from an existing library with the same class name, it seems to not work me. – kunarapu priyatham Mar 24 '22 at 11:13
  • @kunarapupriyatham I've edited my post with a solution (see the bottom). – Jeppe Mar 24 '22 at 11:59
  • This is same as: `from external import *` and `class MyClass1(Base, MyClass1)`. This also goes to recursion depth error. – kunarapu priyatham Mar 24 '22 at 12:09
  • When you write it like that, it will take the `MyClass1` which is "closest", which is the one you're defining in this module - hence extending itself (infinitely). You need to tell it that it is the one in the other module by specifying the external module-name as well. – Jeppe Mar 24 '22 at 12:11
  • But the issue is, I want to extend all other Classes too. So, it will introduce a redundant `external.` in all the extensions of all classes which I think is not a good practice too. – kunarapu priyatham Mar 24 '22 at 12:17
  • 1
    @kunarapupriyatham Well, it is not possible to do what you ask. `MyClass1` may exist after executing `from external import *`, but you override it with the one you're defining. So either pick a new name, or make it explicit which `MyClass1` you're extending. Adding `external.` to make imports explicit is FAR better practice than what you're proposing. – Jeppe Mar 24 '22 at 12:20
  • Okay, @Jeppe. I'll see for its implementation possibility. – kunarapu priyatham Mar 24 '22 at 12:24