Good Python Interface
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:
- create a class with public fields
- add the
area
function - because Java doesn’t have properties, implement all those getters and setters
- the getters and setters are functions, so name them using verbs
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:
- there are two functions named
width
and two namedheight
but with different decorators - one of these functions is used as the getter, one as the setter
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:
- all the field names are Nouns
- all the function names are Verbs
- I can set the
height
, and thewidth
argument - the
area
is automatically updated (and as a user I don’t care how it is done) - the
area
field is read only - there is a function, which performs some operation on the object internals