After writing object-oriented Python for a while, there comes a point you probably need to write some getters and setters. Unlike C++, the convention in Python is to get and set the class instance property directly if there's no need for writing a setter.
For example, if we have a Rectangle
class that has height
and width
properties and instance methods area
and perimeter
:
class Rectangle:
def __init__(self, height, width):
self.height = height
self.width = width
def area(self):
return self.height * self.width
def.perimeter(self):
return (self.height + self.width) * 2
If we never needed to check the values of height
and width
, this would be fine. For anything like writing experiments in a Jupyter Notebook, this maybe fine, but if we wanted this code to become a part of something with a longer lifespan like a library, we probably want to have something a bit more robust.
There are some things we know about a rectangle that we should probably check for. Rectangles probably should only have positive heights and widths so we should probably check for that. They probably shouldn't be zero either, since that would make it either a point (both height and width is zero) or a line (either height or width is a zero).
For example, we could directly access the height
and width
but we can also directly change those properties to some value we know is invalid:
rect = Rectangle(3, 4) # Rectangle(height=3, width=4)
rect.height # 3
rect.width # 4
rect.height = 0 # Should be invalid
rect.width = -5 # Should also be invalid
Let's change up the code and use Python getters and setters without changing our API interface by updating the __init__
method and adding getters and setters with the Python decorators @property
for the getters and @height.setter
and @width.setter
for setting the height and width respectively:
class Rectangle:
def __init__(self, height, width):
self._height = height
self._width = width
@property
def height(self):
return self._height
@property
def width(self):
return self._width
@height.setter
def height(self, new_height):
self._height = new_height
@width.setter
def width(self, new_width):
self.width = new_width
...
So far there's no new functionality. We can still set the properties to some invalid value, but we can already see where we can check for valid values--in the method marked with @___.setter
.
@height.setter
def height(self, new_height):
if new_height <= 0:
return
self._height = new_height
@width.setter
def width(self, new_width):
if new_width <= 0:
return
self.width = new_width
So after updating the setters, we won't be able to set the _height
and _width
properties to something invalid. But there's still an issue. We can still initialize our rectangle's _height
and _width
to invalid values. What we want to do is to use the setters in our __init__
method also. Let's also raise and exception rather than returning silently:
def __init__(self, height, width):
self.height = height
self.width = width
...
@height.setter
def height(self, new_height):
if new_height <= 0:
raise Exception
self._height = new_height
@width.setter
def width(self, new_width):
if new_width <= 0:
raise Exception
self.width = new_width
Lets' also clean up some duplication by creating a static method to check the validity of height and width values. The final code should look like this with a new static method check_value
using the @staticmethod
decorator:
class Rectangle:
def __init__(self, height, width):
self.height = height
self.width = width
@property
def height(self):
return self._height
@property
def width(self):
return self._width
@height.setter
def height(self, new_height):
Rectangle.check_value(new_height)
self._height = new_height
@width.setter
def width(self, new_width):
Rectangle.check_value(new_width)
self._width = new_width
def area(self):
return self.height * self.width
def perimeter(self):
return (self.height + self.width) * 2
@staticmethod
def check_value(value):
if value <= 0:
raise Exception
Python's approach with getters and setters is to allow you to keep writing simple programs and only having to use getters and setters when appropriate without changing the interface. This is a pretty elegant approach especially for prototyping without fear of changing too much code later when experiments prove themselves out later when you need to productionize the code.
Originally published at warrenwong.org on April 12, 2019.
Top comments (1)
let's say you have 6 attributes, the fun begins ...