4

I am learning about inheritance mechanisms in Python and i want to make some variables that are automatically calculated inside the child __init_() function, but the problem is that i want to make yet another class in which the variable will be overwriten. And i dont want those variables to be calculated twice, once in parent and then in child. So, is the example below correct?

class Shape(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def getcenter(self):
        return self.x, self.y


class Square(Shape):
    def __init__(self, x, y, a):
        super().__init__(x,y)
        self.a = a
        self.perimeter = 4*a
        self.field = a*a


class Rectangle(Square):
    def __init__(self, x, y, a, b):
        super().__init__(x, y, a)
        self.perimeter = (2*a) + (2*b)
        self.field = a*b
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • 4
    Why are you asking? Have you tested it? What happened? – jonrsharpe Oct 25 '16 at 15:42
  • 2
    I would suggest making the square object of type Rectangle rather than the other way round. A square is a rectangle, but a rectangle is not a square. In square, use super().__init_(x,y,a,a) then you don't need to calculate field or perimiter in square as the calculation in rectangle is still correct. – R.Sharp Oct 25 '16 at 15:45
  • Well, the code works but the problem is that i dont want to lose computing power on unnecessary calculations. I see that i made a mistake in Square class, indeed it should inherit from Rectangle not the way i did it. – Radek Lejba Oct 25 '16 at 15:50

3 Answers3

3

Well, ignoring that your above would actually be better to have Square subclass Rectangle, normally that type of logic would be moved into a new function overridden by the subclass.

class Square:
   def __init__(self, a):
      self.a = a
      self.calculate_perimeter()

   def calculate_perimeter(self):
      self.perimeter = self.a * 4


class Rectangle(Square):
   def __init__(self, a, b):
      self.b = b
      super().__init__(a)

   # automatically called by the parent class
   def calculate_perimeter(self):
      self.perimeter = self.a * 2 + self.b * 2
cwallenpoole
  • 79,954
  • 26
  • 128
  • 166
  • In the Rectangle object, should that be self.b = b ? – R.Sharp Oct 25 '16 at 15:52
  • Yes, this is exactly what i was wondering about. But the compiler that i am using (PyCharm) is pointing "instance attribute outside __init__", not an error tho. – Radek Lejba Oct 25 '16 at 15:54
2

I can think of a few different approaches here:

One would be asking "Isn't a square a particular case of a rectangle?"

Then this should work, right?

class Rectangle(Shape):
    def __init__(self, x, y, a, b):
        super(Rectangle, self).__init__(x, y)
        self.perimeter = (2 * a) + (2 * b)
        self.field = a * b


class Square(Rectangle):
    def __init__(self, x, y, a):
        super(Square, self).__init__(x, y, a, a)
        self.a = a

That way, you'll get the perimeter and field attributes calculated with a=a and b=a for the particular case of a Rectangle that a Square is.

Another great option would be using the @property decorator, and calculate the perimeter and the field on the fly (multiplying integers is a quite cheap operation)

class Shape(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def getcenter(self):
        return self.x, self.y


class Square(Shape):
    def __init__(self, x, y, a):
        super(Square, self).__init__(x, y)
        self.a = a

    @property
    def perimeter(self):
        print("Perimeter of Square")
        return 4 * self.a

    @property
    def field(self):
        return self.a * self.a


class Rectangle(Square):
    def __init__(self, x, y, a, b):
        super(Rectangle, self).__init__(x, y, a)
        self.b = b

    @property
    def perimeter(self):
        print("Perimeter of Rectangle")
        return (2 * self.a) + (2 * self.b)

    @property
    def field(self):
        return self.a * self.b


if __name__ == "__main__":
    s = Square(1, 2, 3)
    r = Rectangle(5, 6, 7, 8)
    print ("Perimeter (square) %s" % s.perimeter)
    print ("Perimeter (rectangle) %s" % r.perimeter)

More on properties on this great SO thread.

And there's more! I'd recommend you play a little with the example to see what you can do (and, overall, that you have fun doing it) :-)

Community
  • 1
  • 1
Savir
  • 17,568
  • 15
  • 82
  • 136
  • 1
    This. Properties are the appropriate answer to OP's core question, which helps avoid calculation in `__init__`. Caching can also be implemented to prevent having to do the calculation on every lookup, if the dependent values have not changed. – sytech Oct 25 '16 at 16:02
  • 1
    Certainly (and thank you for your words liking the answer)! I wanted to avoid the catching to keep the example simple, since the OP seems to be starting with OOP and this looks like an exercise, but it would definitely be worth it in "real life" **:+1:** – Savir Oct 25 '16 at 16:07
2

Rather than making perimeter and field attributes, you should implement them as properties, instead. That way, they are only calculated when called for. This will also prevent those values from having to be calculating when you instantiate the class.

As also mentioned, inheriting Square from Rectangle is useful. Square, essentially, could just be an alternate constructor for Rectangle. Though you may opt for a different design, depending on the use case.

class Rectangle(Shape):
    def __init__(self, x, y, a, b):
        self.a = a
        self.b = b
        super().__init__(x, y)
    @property
    def perimeter(self):
        return (self.a + self.b) * 2


class Square(Rectangle):
    '''Just an alternate constructor for Rectangle'''
    def __init__(self, x, y, a):
        super().__init__(x, y, a, a) #b is same as a in a square.
    #everything else already works!

Example:

>>> my_square = Square(0, 0, 10)
>>> my_square.perimeter
40
>>> my_rect = Rectangle(0,0, 10, 5)
>>> my_rect.perimeter
30

Another problem properties solve is that users cannot erroneously set the perimeter to a different value.

>>> my_square.perimeter = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Otherwise, perimeter would be allowed to be set and would therefore be inconsistent with the values of a and b

#without properties you could get inconsistent values
>>> my_square.perimeter = 10
>>> print(my_square.perimeter, my_square.a, my_square.b)
10 10 10

You could also use property setters to dictate what happens when someone tries to set a property.

class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, diam):
        self.radius = diam / 2

Ex:

>>> my_circle = Circle(radius=5)
>>> my_circle.diameter
10
>>> my_circle.diameter = 40
>>> my_circle.radius
20
sytech
  • 29,298
  • 3
  • 45
  • 86