star.lit
star.lit

Reputation: 167

python - Accessing superclass's class attribute in super().__init__()

I have a class Parent with many instance properties, and I always pass a dict to initialize an instance. Like this:

info = {
    "name" : "Bob",
    "col" : 5,
    "line" : 10,
    "type" : "Alien"
}

p = Parent(info)

And in __init__ method I don't want to write this.property_name = value for each property cuz the code will be very long. For instance:

class Parent(object):

    def __init__(self, kwargs):
        this.name = kwargs.get("name")
        this.col = kwargs.get("col")
        this.line = kwargs.get("line")
        this.type = kwargs.get("type")

So I want to use a function to iterate the dict to set these instance properties. This is the function I wrote:

def initialize_properties(instance, names, kwargs):
    for name in names:
        setattr(instance, name, kwargs.get(name))

It seems that I need to store the property name list names somewhere, and I decide to store it as a class attribute, because I want my class to be human friendly (when somebody reads the class definition he knows what instance properties this class has). So I changed my class definition as follows:

class Parent(object):
    props = ("name", "col", "line", "type")

    def __init__(self, kwargs):
        initialize_properties(self, self.props, kwargs)

This works fine when inheritance is NOT considered. The problem occurs when I subclass Parent:

class Child(Parent):
    props = ("foo", "bar")

    def __init__(self, kwargs):
        super().__init__(kwargs)
        initialize_properties(self, self.props, kwargs)

I want instances of Child to inherit all the instance properties in superclass Parent, with some child-specific instance properties as well.(This is why we use inheritance, isn't it?) So I overwrite the class attribute props to define child-specific properties.

But it doesn't work.

info = {
    "name" : "Bob",
    "col" : 5,
    "line" : 10,
    "type" : "Alien",
    "foo" : 5,
    "bar" : 10
}

c = Child(info)

Instance c only has c.foo and c.bar defined and set, while c.name is not defined.

After some digging I found that when Parent.__init__(self, kwargs) is called through the super() function, the self argument passed is of class Child, so self.props evaluates to Child.props.

If I want to set the instance properties in Parent.props, I have to explicitly use Parent.props in Parent.__init__(self, kwargs), which is:

class Parent(object):
    props = ("name", "col", "line", "type")

    def __init__(self, kwargs):
        initialize_properties(self, Parent.props, kwargs)

This will solve the problem, but I think it's not very "clean" because you have to hard-code the class name Parent.

So my question is: Is there any way to detect the current class and access its class attributes, when you are calling a chain of super().__init__() to initialize an subclass instance?

Upvotes: 5

Views: 4074

Answers (3)

littlezz
littlezz

Reputation: 1

To answer you question

Is there any way to detect the current class and access its class attributes, when you are calling a chain of super().init()

In my opinion, no way.

However, you can use decorator to simplify your code

def add_props(names, cls=None):
    if cls is None:
        return lambda cls:add(names, cls)
    for c in cls.__bases__:
        names.extend(c.props)
    names = list(set(names))
    cls.props = names
    return cls


class A:
    props = ['a','b']


@add_props(names=['c'])
class B(A):
    pass

output:

In [69]: B.props
Out[69]: ['a', 'b', 'c']

But, it must said that if you want human friendly, the best way i think is not using props, just write code in your __init__ method.

class Root:
    def __init__(self, a=None, b=None):
        self.a = a
        self.b = b


class Child(Root):
    def __init__(self, c=None, d=None, **kwargs):
        self.c = c
        self.d = d
        super().__init__(**kwargs)

You can also have convenience to pass dict as you like

data = {'a': 1}
c = Child(**data)

And it is not only friendly to human but also friendly to most editors.

Upvotes: 0

user2390182
user2390182

Reputation: 73450

You could implement a method that collects the props from all supeclasses and use it in the __init__:

class Parent(object):
    props = ("name", "col", "line", "type")

    def __init__(self, **kwargs):
        def __init__(self, kwargs):
        initialize_properties(self, self.get_props(), kwargs)

    def get_props(self):
        return sum((getattr(k, 'props', ()) for k in self.__class__.mro()), ())

Now, you can simply define subclasses the way you wanted. You wouldn't even have to override the constructor:

class Child(Parent):
    props = ("foo", "bar")

That being said, you claim you want your classes human friendly. If a human reads your child class, they will be glad to read:

class Child(Parent):
    props = 'foo', 'bar', 'name', 'col', 'line', 'type'

and have all the class' props available in one place.

Upvotes: 3

falsetru
falsetru

Reputation: 368894

self.props first references instance attribute, then class attribute, and parents' class attribute, ...

By defining Child.props, self.props refers Child.props class attribute, stopping there, not searching in parent class.

How about make the Child.props also include parents' props?

class Child(Parent):
    props = Parent.props + ("foo", "bar")   # <---

    def __init__(self, kwargs):
        super().__init__(kwargs)
        initialize_properties(self, self.props, kwargs)

Upvotes: 1

Related Questions