DEV Community

Cover image for Python OOP for Java Developers
CitronBrick
CitronBrick

Posted on

Python OOP for Java Developers

Let's consider the case of a simple Java class Square.

A Square, which is a Shape, is defined by its position & side length.

Square inherits from Shape


abstract class Shape {

    abstract int area();

    abstract int perimeter();

}

public class Square extends Shape {

    private int x;
    private int y,
    private int side;

    public Square(int x, int y, int side) {
        this.x = x;
        this.y = y;
        this.side = side;
    }

    public Square(int x1, int y1,int x2,int y2) {
        if(Math.abs(x2 - x1) != Math.abs(y2 - y1) ) {
            throw new IllegalArgumentException("Square must have equal sides");
        }
        x = x1;
        y = y1;
        side = (x2 - x1);
    }

    public int area() {
        return side * side;
    }

    public int perimeter() {
        return 4 * side;
    }

    public String toString() {
        return String.format("Square[x=%d,y=%d,side=%d", x, y, side);
    }
}
Enter fullscreen mode Exit fullscreen mode

Since this article is intended for Java developers, I am not going to explain it in detail, but feel free to ask doubts in the comments.

Now, we are going convert it step by step into Python.


class Square : Shape 
    pass #empty blocks are not allowed in Python
Enter fullscreen mode Exit fullscreen mode

Constructor (not Constructors)

Since declaration in Python, is indistinguishable from initialization, we declare the fields in the only constructor.

def __init__(self, x, y, side):
    self.x = x
    self.y = y
    self.side = side
Enter fullscreen mode Exit fullscreen mode

To create a Square instance, no new keyword is used.

The 2nd constructor having 4 parameters cannot be implemented using __init__, since Python does not support overloading neither for constructors nor methods.

You'll see how to implement it using @classmethod later.

s1 = Square(1, 3, 10)
s2 = Square(x=1, y=3, side=10)
Enter fullscreen mode Exit fullscreen mode

Encapsulation

Python does not enforce strict encapsulation as does Java via public,protected,private keywords. By convention, members beginning with _ (underscore) are private in Python.

The explicit (not implicit) argument

You can notice an extra 1st argument self in the __init__.

You have probably guessed that it's analogous to this in Java.
Except that self is not a keyword & that the 1st argument explicitly refers to the current instance.

The same holds true for the instance methods. The 1st argument refers to the current instance. You cannot refer to fields or other methods in the same class without it.

def area(self):
    return self.side * self.side

def perimeter(self):
    return 4 * self.side
Enter fullscreen mode Exit fullscreen mode

String representation

Similar to toString in Java, there are the __repr__ & __str__ methods, which get called when you use the str function. Unlike toString, the str function is not implicitly called during concatenation.

def __str__(self):
    return "Square [side=" + str(self.side) + "]"

def __repr__(self):
    return "Square(side="+str(self.side)+")"


sq = Square(5)
print(sq) # prints Square [side=5]
print(sq + ".") # error. cannot concatenate string & Square
print(str(sq) + ".") # prints Square [side=5].
Enter fullscreen mode Exit fullscreen mode
__repr__ method __str__ method
returns the constructor call expression returns string representation
has less priority during str call has more priority during str call

Static & Class methods (they're different)

In Java, class method & static method refer to the same concept, whereas in Python they are quite different.

Consider a method that for a given area, calculates the length of a side. Such a method would be static as it does not depend on the current square instance.

public static double calcSideLength(area) {
    return Math.sqrt(area);
}
Enter fullscreen mode Exit fullscreen mode
@staticmethod 
def calc_side_length(area):
    return math.sqrt(area) # math is a module not a class
Enter fullscreen mode Exit fullscreen mode

A static method does not have the 1st self argument, as it's independent of the instance. Here staticmethod is a decorator i.e. a function that returns another function for function transformation.

We can implement the 2nd constructor having 4 arguments, using another decorator @classmethod.

@classmethod 
def make_square_from_points(cls, x1, x2, y1, y2):
    return cls(x1, x2, x1 - x2) # cls is the constructor
Enter fullscreen mode Exit fullscreen mode

A class method receives the class constructor itself (cls) as it's 1st argument. Hence, it's useful for creating factory methods.

Let's check if the points passed to this method, really forms a square.

@class method
def make_square_from_points(cls, x1, y1, x2, y2):
    if x2 - x1 == y2 - y1:
        return cls(x1, x2, x1 - x2) 
    else:
        raise ValueError("The points do not form a square")
Enter fullscreen mode Exit fullscreen mode

Exceptions

No prizes for guessing that we are "throwing an Exception", i.e. "raising an Error" here. We can call make_square_from_points as:

try:
    sq = Square.make_square_from_points(4, 5, 7, 11)
except ValueError as ve:
    print(ve)
Enter fullscreen mode Exit fullscreen mode

Output

ValueError: (4, 5, 7, 11)
Enter fullscreen mode Exit fullscreen mode

Error hierarchy

We are going to see how to create child classes in Python. Let's create our own Error MalformedSquareException by inheriting from ValueError. (I haven't forgotton the Shape class, but I'm saving it for later.)

class MalformedSquareException(ValueError):

    def __init__(self, x1, y1, x2, y2):
        self.x1 = x1
        self.x2 = x2
        self.y1 = y1
        self.y2 = y2

        # Note the difference wrt __str__
    def __repr__(self):
        return "MalformedSquareException(%d, %d, %d, %d)" % (self.x1, self.y1, self.x2, self.y2)

    def __str__(self):
        return "The points (%d, %d) and (%d, %d) are not the opposite corners of a square" %(self.x1, self.y1, self.x2, self.y2)

try:
    sq = Square.make_square_from_points(4, 7, 5, 11)
except MalformedSquareException as ve:
    print(ve)
Enter fullscreen mode Exit fullscreen mode

Output

The points (4, 5) and (7, 11) are not the opposite corners of a square
Enter fullscreen mode Exit fullscreen mode

Abstract classes

Coming back to our Shape abstract, it can be written in Python as follows:

import abc

# alternative:
# class Shape(abc.ABC) 
class Shape(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def area(self):
        ... # Python abstract classes require a body

    @abc.abstractmethod
    def perimeter(self):
        ...
Enter fullscreen mode Exit fullscreen mode

Python abstract classes are created either by

  • declaring ABCMeta as a metaclass or
  • subclassing ABC (whose metaclass is ABCMeta)

As in Java, Python abstract classes can have constructors & non-abstract methods. In addition,

Python abstract classes can have static & class abstract methods.

Surprised ? Consider the method nb_sides in Shape. It's abstract as it depends on the implementation. However, for a given Shape class it's constant.

class Shape(metaclass= abc.ABCMeta):

    @staticmethod
    @abc.abstractmethod
    def nb_sides():
        ...

class Square(Shape):

    @staticmethod 
    def nb_sides():
        return 4
Enter fullscreen mode Exit fullscreen mode

Note that @staticmethod must preceed @abc.abstractmethod.

Operator Overloading

Yes, Python supports operator overloading. We can implement methods such as __add__, __mul__, __lt__, __gt__, __not__ etc and use their respective symbols to call them. In contrast, the only case of operator overloading in Java is using + for String contactenation.


class Square(metaclass=abc.ABCMeta):

    # sq2 = sq1 * 3 creates sq2 that's 5 times bigger than sq1
    def __mul__(self, n):
        return Square(self.x, self.y, self.side * math.sqrt(n)

    def __lt__(self, s):
        return self.side < s.side

    def __gt__(self, s):
        return self.side > s.side

s1 = Square(0,0, 10) 
s2 = s1 * 3 # __mul__
print(s1 < s2) # __lt__ is called
Enter fullscreen mode Exit fullscreen mode

Multiple Inheritance

Now we've come to the meat of the matter. Python supports multiple inheritance, unlike Java which supports only single inheritance.

This naturally leads to the question: When a class has multiple ancestors, which one's members have the higher priority ?

  1. Immediate ancestors' members have higher priority than those farther up. (As in Java)
  2. Within ancestors of the same hierarchy, the class declarations, parent class order determines the priority (left -> higher, right -> lower)

Eg: Consider the following scenario:

Updated Shape, Rectangle, Rhombus & updated Square classes

I've introduced 2 new classes Rectangle & Rhombus which both inherit the abstract class Shape. And the Square now inherits both Rectangle & Rhombus.

class Rectangle(Shape):

    def __init__(self, x, y ,length, breadth):
        self.x, self.y = x,y
        self.length, self.breadth = length, breadth

    def area(self):
        return self.length * self.breadth

    def perimter(self):
        return 2 * (self.length + self.breadth)

    @staticmethod
    def nb_sides():
        return 4

class Rhombus(Shape):

    def __init__(self, x, y, side, angle):
        self.x, self.y = x,y
        self.side, self.angle = side, angle

    def area(self):
        return self.side * self.side * math.sin(self.angle * math.pi/180)

    def perimeter(self):
        return 4 * self.side

    @staticmethod
    @abc.abstractmethod
    def nb_sides():
        return 4

class Square(Rhombus, Rectangle):

        def __init__(self, side):
            super().__init__(self, side, math.pi/2)

        def area():
            return super().area()


        @staticmethod
        def nb_sides():
            return 4
Enter fullscreen mode Exit fullscreen mode

There are a number of things to note here:

  • The super call does not call the parent constructor, though it still references the parent object. Hence the need to call __init__ again.
  • Since the declaration of Square contains Rhombus before Rectangle (though Rectangle is declared before Rhombus in the code), super() references Rhombus not Rectangle.
  • Though both Rhombus & Rectangle have implemented the static method nb_sides, Square still needs to implement it for itself. Failure to do so, will result in an error.
  • The explicit call of the parent constructor via __init__ needed self as an argument, while the call of the parent area() method did not need it.

To know the method resolution order (ascending) of a given object using the mro method. Eg:

sq = Square(0,0,5)
print(sq.mro())
#[<class '__main__.Shape'>, <class '__main__.Rectangle'>, <class '__main__.Rhombus'>]
# higher priority is towards the right
Enter fullscreen mode Exit fullscreen mode

Conclusion.

There are other advanced topics like :

  • how to create an interface in Python (roundabout way) &
  • how to change the default inheritance order But I've skipped them for now.

Here's a summary of the differences in Object Oriented Programming between Java & Python, as seen in this article.

Java Python
Strict encapsulation Lenient encapsulation
Overloading of constructors & methods No overloading
implicit this keyword argument method's explicit 1st argument
static members static & class methods (different)
toString __repr__ and __str__
Almost no operator overloading Operator overloading
Abstract methods cannot be static Abstract methods can be class or static
Single inheritance Multiple inheritance

The Java icon in the cover image is from Java icons created by Freepik - Flaticon.

Top comments (0)