Good Python Interface

by Szymon LipiƄski

In the previous blog post I wrote about changing interfaces using a Java class as an example.

The public interface of the final implementation was:

constructor:

    (int, int)

methods:
 
    getWidth() -> int
    getHeight() -> int
    getArea() -> int
    setWidth(int)
    setHeight(int) 

There were no public fields, all were accessible with the setters and getters.

There was also a C# example with the properties. This way I had the public interface like:

constructor:

    (int, int)

fields:

    int width
    int height

Because of the properties those fields were also writable.

A Bad Python Example

In Python I see lots of libraries which follow the Java getters and setters pattern:

constructor:

    (int, int)

methods:

    getWidth() -> int
    getHeight() -> int
    getArea() -> int
    setWidth(int)
    setHeight(int) 

It’s not that bad, it is just not too pythonic. It doesn’t feel like the Python’s good practices (I’m not talking about PEP 8).

The Main Java To Python Translation

Just to remind you the final Java implementation from the previous post:

public class Rectangle {
    private int width;
    private int height;
    private Integer area;

    private void validateWidth(int width) {
        if (width < 0) {
            throw new IllegalArgumentException(
                    "Width cannot be smaller than 0.");
        }
    }

    private void validateHeight(int height) {
        if (height < 0) {
            throw new IllegalArgumentException(
                    "Height cannot be smaller than 0.");
        }
    }

    public Rectangle(int width, int height){
        this.setWidth(width);
        this.setHeight(height);
    }

    public int getWidth() {
        return this.width;
    }

    public int getHeight() {
        return this.height;
    }

    public int getArea() {
        if (this.area == null) {
            this.area = this.width * this.height;
        }
        return this.area;
    }

    public void setWidth(int width) {
        validateWidth(width);
        if (this.width != width) {
            this.width = width;
            this.area = null;
        }
    }

    public void setHeight(int height) {
        validateHeight(height);
        if (this.height != height) {
            this.height = height;
            this.area = null;
        }    }

}

We can easily rewrite it into Python:

class Rectangle(object):

    def __init__(self, width, height):
        self._width = width
        self._height = height
        self._area = None

    def _validateWidth(self, width):
        if width < 0:
            raise ValueError("Width cannot be smaller than 0.")
    
    def _validateHeight(self, height):
        if height < 0:
            raise ValueError("Height cannot be smaller than 0.")

    def getWidth(self):
        return self._width

    def getHeight(self):
        return self._height

    def getArea(self):
        if self._area is None:
            self._area = self._width * self._height
        return self._area

    def setWidth(self, width):
        self._validateWidth(width)
        if self._width != width:
            self._width = width
            self._area = None
    
    def setHeight(self, height):
        self._validateHeight(height)
        if self._height != height:
            self._height = height
            self._area = None

Making The Code More Pythonic

The above example is not too pythonic. The community uses rather different naming scheme, more like this_name than thisName.

Let’s change the names:

class Rectangle(object):

    def __init__(self, width, height):
        self._width = width
        self._height = height
        self._area = None

    def _validate_width(self, width):
        if width < 0:
            raise ValueError("Width cannot be smaller than 0.")
    
    def _validate_height(self, height):
        if height < 0:
            raise ValueError("Height cannot be smaller than 0.")

    def get_width(self):
        return self._width

    def get_height(self):
        return self._height

    def get_area(self):
        if self._area is None:
            self._area = self._width * self._height
        return self._area

    def set_width(self, width):
        self._validate_width(width)
        if self._width != width:
            self._width = width
            self._area = None
    
    def set_height(self, height):
        self._validate_height(height)
        if self._height != height:
            self._height = height
            self._area = None

Making it Even More Pythonic

Do you remember what was the logic behind this interface in Java? It was something like this:

In Python this way of thinking has one huge flaw: in Python we have properties. Because we have properties, we can have the interface like this:

constructor:

    (int, int)

fields:

    width
    height
    area

So let’s implement this:


class Rectangle(object):

    def __init__(self, width, height):
        self._width = width
        self._height = height
        self._area = None

    def _validate_width(self, width):
        if width < 0:
            raise ValueError("Width cannot be smaller than 0.")
    
    def _validate_height(self, height):
        if height < 0:
            raise ValueError("Height cannot be smaller than 0.")

    @property
    def width(self):
        return self._width

    @property
    def height(self):
        return self._height

    @property
    def area(self):
        if self._area is None:
            self._area = self._width * self._height
        return self._area

    @width.setter
    def width(self, width):
        self._validate_width(width)
        if self._width != width:
            self._width = width
            self._area = None
    
    @height.setter
    def height(self, height):
        self._validate_height(height)
        if self._height != height:
            self._height = height
            self._area = None

One More Change

Let’s add one more change to have a function in the interface. This will be a function named enlarge(int) which will increase the width, and the height.

So the final implementation will be:


class Rectangle(object):

    def __init__(self, width, height):
        self._width = width
        self._height = height
        self._area = None

    def _validate_width(self, width):
        if width < 0:
            raise ValueError("Width cannot be smaller than 0.")
    
    def _validate_height(self, height):
        if height < 0:
            raise ValueError("Height cannot be smaller than 0.")

    @property
    def width(self):
        return self._width

    @property
    def height(self):
        return self._height

    @property
    def area(self):
        if self._area is None:
            self._area = self._width * self._height
        return self._area

    @width.setter
    def width(self, width):
        self._validate_width(width)
        if self._width != width:
            self._width = width
            self._area = None
    
    @height.setter
    def height(self, height):
        self._validate_height(height)
        if self._height != height:
            self._height = height
            self._area = None

    def enlarge(self, value):
        self.width += value
        self.height += value

As you can see there are some differences:

This way we have properties named with Nouns as using proper names is important

The Final Remarks

The final interface is simple:

constructor:

    (int, int)

fields:

    width  (read/write)
    height (read/write)
    area   (read only)

functions:
   
    enlarge(int)

So I can use it like this:

r = Rectangle(10, 20)
print("width:{} height:{} area:{}".format(r.width, r.height, r.area))

r.height = 15
print("width:{} height:{} area:{}".format(r.width, r.height, r.area))

r.enlarge(7)
print("width:{} height:{} area:{}".format(r.width, r.height, r.area))

r.area = 3

The output is:

width:10 height:20 area:200
width:10 height:15 area:150
width:17 height:22 area:374
Traceback (most recent call last):
  File "/tmp/a.py", line 49, in <module>
    r.area = 3
AttributeError: can't set attribute

As you can see: