0

Well consider the following minimal example:

class base:
    def __init__(self, mass):
        self.mass = mass
    def price()
        return self.mass * 10


class deriv(base):
    def __init__(self, payload, *args, **kwargs):
        super().__init__(mass=0, *args, **kwargs)
        #mass of super is shadowed anyways, so setting it to 0
        self.payload = payload

    @property
    def mass(self):
        return self.payload #would be a more complex calculation
    #NO setter, since it's illogical due to it being complex

t = deriv(1000)

Basically the derived class "uniqueness" is that the mass is no longer a simple value that can be set & changed. But instead it is based on a complex function based on several other, new, properties.

Now above code "looks" good to me: mass is shadowed in the derived class by the property. In the base class initializer the derived class does not yet fully exist, so writing there should write into the base class properties (a shadowed object). Yet for the price function it should look into the derived class' property.

So I tried to execute this:

line 3, in __init__
    self.mass = mass
AttributeError: can't set attribute

Ok it looks as if during __init__ of the baseclass python is already aware of the properties from the derived class. That is "strange", especially in the light of another (silly) derived class, which does work:

class deriv2(base):
    def __init__(self, mass, *args, **kwargs):
        super().__init__(mass=0, *args, **kwargs)
        self.mass = mass*2

So how would I make this work? I rather change as little to both the interface of the object (still want to have a mass property in for deriv) as well as as little for base (deriv is the "weird" one, so that one is responsible for making sure he works).

paul23
  • 8,799
  • 12
  • 66
  • 149

3 Answers3

1

You are committing a design error.

The Liskov Substitution Principle says that subclasses should support all of the operations of their base class. Code which works with the base class should transparently work with the derived class; moreover, you shouldn't be able to tell that you're operating on a subclass.

So what happens when I write some code which tries to set the mass?

def set_mass(item):
    item.mass = 123

If item is an instance of base, calling set_mass will work without a hitch. The mass of the item will be 123 after running the function. It's perfectly reasonable to expect this function to work on any object which satisfies isinstance(item, base).

However, if item is an instance of deriv, you can't set the mass. This function will throw an exception. deriv violates the LSP, and the result is broken code.

It's a bad idea to try and erase properties from your base class. You should try to re-design your code to avoid this style of inheritance.

Community
  • 1
  • 1
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • The problem is, the common operation need to have a mass (the common is for any object rotating another body, so both planets, stars, moons and satellites). The derived class in this case is a satellite - the satellite's mass however is not a simple number I know from a database - rather it is a complex number based on all parts & sub components that are added to this object. Are you saying those two can't have the same base class? Or should I create an abstract mass-less baseclass that provides functions and expect it's derived classes to override mass? – paul23 Mar 05 '16 at 11:19
  • It seems reasonable to calculate the mass before calling the base class's constructor. `def __init__(self, component_masses): super().__init__(sum(component_masses))` – Benjamin Hodgson Mar 05 '16 at 11:22
  • Problem is, the amount of components etc is changing over time (IE: fuel is exhausted). – paul23 Mar 05 '16 at 13:04
1

In the base class initializer the derived class does not yet fully exist.

That's not correct. Both of the class definitions have been executed by the time control reaches t = deriv(1000), so both of those class types exist before you attempt to create an instance of deriv.

Inside the deriv.__init__ method super() is shorthand for super(deriv, self), so yes you are calling the base.__init__ method, but you are calling it with the self of the new deriv instance as its 1st argument, so all of its actions are being applied to that deriv instance, not to some ethereal instance of base. Thus you cannot set .mass because that refers to the .mass property of deriv not to the plain .mass attribute of base.

BTW, to conform to PEP008 style you should use CamelCase for class names.

PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
0

PM 2Ring explains why self.mass = mass raises an error (when the self in base.__init__ refers to an instance of deriv). Benjamin Hodgson explains why a class that prevents the setting of mass should not derive from one that does set the mass.

One way to avoid these problems is to create two subclasses, SettableMass (for objects with settable mass) and Deriv (for objects with calculated mass) and have them both derive from a Base class which does not assume the mass is settable:

class Base:
    def __init__(self, *args, **kwargs): pass
    def price(self):
        return self.mass * 10

class SettableMass(Base):
    def __init__(self, mass):
        super().__init__()
        self.mass = mass

class Deriv(Base):
    def __init__(self, payload, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.payload = payload
    @property
    def mass(self):
        return self.payload 

t = Deriv(1000, 'foo', bar=3.1)
print('mass {}, cost {}'.format(t.mass, t.price()))
# mass 1000, cost 10000

s = SettableMass(90)
print('mass {}, cost {}'.format(s.mass, s.price()))
# mass 90, cost 900

PS: It's not clear to me why you need Base or SettableMass classes if you plan to use them for planets or stars. Why for instance would a planet have a price? But assuming you do have a good reason for having planets and satellites inherit methods from a common base, the above would provide a suitable class hierarchy.

Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677