Ahmad
Ahmad

Reputation: 9658

How to preserve the value of class properties

class A:
    p = 1
    def __init__(self, p=None, **kwargs):
        self.p = p

class B(A):
    p = 2

a = A()
print(a.p)
b = B()
print(b.p)

As a more sensible example consider:

class Mamal:
    can_fly = False

class Bat(Mamal):
    can_fly = True

In the examples above, I would like 1 and 2 be printed. However, it prints None for both, though I know why. What is the solution to preserve the default value of classes?

One solution I can think of is:

class A:
    p = 1
    def __init__(self, p=None, **kwargs):
        if p: self.p = p
        if q: self.q = q
        ...

and if I have many attributes I should do that for all of them!? another minor problem is that the user can't pass None to the class init.

Another solution could be like:

class A:
    p = 1
    def __init__(self, p=1, **kwargs):
        self.p = p
        self.q = q
        ...

However again if one instantiate b like:

b = B()

the value of b.p would be also 1, while I expect it to keep 2.

I use overriding classes attributes much, but I just don't know how to preserve them from being overwritten by default values of the same or parent class.

Yet, another solution is combination of the above, like:

class A:
    p = 1
    def __init__(self, p=1, **kwargs):
        if p != 1: self.p = p
        ...

or using dataclass

from dataclasses import dataclass
@dataclass
class A:
    p :int = 1

@dataclass
class B(A):
    p:int = 2

Just would like to know what is usual approach and consequences.

Upvotes: 1

Views: 479

Answers (3)

Daniil Fajnberg
Daniil Fajnberg

Reputation: 18438

UPDATE:

If you really absolutely need both your class and your instances to have this attribute, and also want to use the class attribute as the default for an instance, I would say the correct way is like this:

_sentinel = object()


class A:
    p = 1

    def __init__(self, p=_sentinel):
        if p is not _sentinel:
            self.p = p


class B(A):
    p = 2


a = A()
print(a.p)  # prints 1
b = B()
print(b.p)  # prints 2
b2 = B(p=None)
print(b2.p)  # prints None

The sentinel object is for when you do want to be able to pass None to the constructor for whatever reason. Since we compare identity in the __init__ method, it is (practically) guaranteed that if any value is passed, it will be assigned to the instance attribute, even if that value is None.

Original answer:

The problem seems to stem from a misunderstanding of how (class-)attribute work in Python.

When you do this:

class A:
    p = 1

You define a class attribute. Instances of that class will automatically have that same attribute upon initialization, unless you overwrite it, which is exactly what you do here:

    def __init__(self, p=None, **kwargs):
        self.p = p

This overwrites the instance's attribute .p with the value p it receives in the __init__ method. In this case, since you defined a default value None and called the constructor without passing an argument, that is what was assigned to the instance's attribute.

If you want, you can simply omit the self.p assignment in the constructor. Then your instances will have the class' default upon initialization.

EDIT:

Depending on how you want to handle it, you can simply assign the value after initialization. But I doubt that is what you want. You probably don't need class attributes at all. Instead you may just want to define the default values in your __init__ method signature and assign them there.

If you really need that class attribute as well, you can do what you did, but more precisely by testing for if p is not None:.

Upvotes: 2

antont
antont

Reputation: 2756

You can use class properties from the class:

class A:
    p = 1

class B(A):
    p = 2

a = A()
print(a.p)
b = B()
print(b.p)

prints 1 and 2, like you wanted.

It is clearer to access them from the class directly, though:

print(A.p)
print(B.p)

You can set the instance one, without changing what is associated in the class.

class B(A):
    def change(self, x):
        self.p = x

b.change(3)
print(B.p) #2
print(b.p) #3

Upvotes: -1

Dj0ulo
Dj0ulo

Reputation: 470

I would set the default value of the p argument to the value that you want:

class A:
    def __init__(self, p=1, **kwargs):
        self.p = p

class B(A):
    def __init__(self, p=2, **kwargs):
        super().__init__(p, **kwargs)

a = A()
print(a.p)
b = B()
print(b.p)

Then from the constructor of B you can call the one from A by using super().__init__

Upvotes: 1

Related Questions