Python is one of the most popular languages being by developers and it's being used in developing websites and software, task automation, data analysis, and data visualization. As the project progresses the complexity and size of the codebase increase. This also means the complexity of user-defined classes begins to increase as time progresses.
As the complexity of the custom classes increases more difficult and time-consuming to debug the code as the default string representation of a class object may not be useful. Even if we simply use the code, we may need to inspect such class objects from time to time to implement our business logic. Default string representation of objects does not give much helpful information.
Let's take the below simple example of a class point and try to inspect the object then it does not give any useful information that aids in our debugging or understanding the Point object for the further use case.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
>>> print(p)
>>> <__main__.Point object at 0x000001A9B5B46F08>
Inbuilt string representation function
Python has three inbuild functions to inspect the object. Based on the use case one can use them.
repr(obj)
str(obj)
format(obj)
Customizing these inbuilt functions in our class improves maintainability, debuggability, and usability. As we know in python every class inherits from the Object class which contains the implementation of various methods.
>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
If we try to inspect the Point object using default string representation methods then we get below output, which would be not useful.
>>> str(p)
'<__main__.Point object at 0x000001A9B5B46F08>'
>>> repr(p)
'<__main__.Point object at 0x000001A9B5B46F08>'
>>> format(p)
'<__main__.Point object at 0x000001A9B5B46F08>'
Customizing String representation of Object
If you are a developer of a custom class and expect other developers will use your class or other customers or system will use your module then you need to override the default behavior of repr, str, and format. Let's try to use a simple example of class Position which contains longitude and latitude information.
Customizing repr()
If our class is being used by other developers or software engineers then it's a good practice to overload the default repr(). Below is a simple example of overriding default __repr__() for Class Position which contains latitude and longitude data.
# repr_overloading.py
class Position:
def __init__(self, latitude, longitude):
if not (-90 <= latitude <= +90):
raise ValueError(f"Latitude {latitude} out of range")
if not (-180 <= longitude <= +180):
raise ValueError(f"Longitude {longitude} out of range")
self._latitude = latitude
self._longitude = longitude
def __repr__(self):
return f"{typename(self)}(latitude={self._latitude}, longitude={self._longitude})"
def typename(obj):
return type(obj).__name__
Now if we try to print the Position object using print or repr() then it prints gives much useful information.
>>> mount_everest= Position(27.9881,86.9250)
>>> repr(mount_everest)
'Position(latitude=27.9881, longitude=86.925)'
As the default __repr__ inherited from the object is not much of use, always override __repr__ to return a more useful string, which is ideally formatted as source code for a constructor call.
This will ease the job of other developers while using our class and help in debugging the issue (if any).
Customizing str()
If your custom class is being used by system consumer-like users, people in the user interface, or other systems then override __str__(). Since it's meant for consumers we need to present data in easy to understand format. In our earlier example, we've overloaded __repr__() which returns information similar to constructor invocation, which will be confusing for other users who are not part of our own development team. In these cases, it would be much more useful to have data in 80.5° S, 150.2° E format rather than Position(27.9881,86.9250).
# str_overloading.py
class Position:
def __init__(self, latitude, longitude):
if not (-90 <= latitude <= +90):
raise ValueError(f"Latitude {latitude} out of range")
if not (-180 <= longitude <= +180):
raise ValueError(f"Longitude {longitude} out of range")
self._latitude = latitude
self._longitude = longitude
def __repr__(self):
return f"{typename(self)}(latitude={self._latitude}, longitude={self._longitude})"
@property
def latitude_hemisphere(self):
return "N" if self._latitude >= 0 else "S"
@property
def longitude_hemisphere(self):
return "E" if self._longitude >= 0 else "W"
def __str__(self):
return (
f"{abs(self._latitude)}° {self.latitude_hemisphere}, "
f"{abs(self._longitude)}° {self.longitude_hemisphere}"
)
def typename(obj):
return type(obj).__name__
If we try to print mount_everest objects information using __str__() it gives much more useful information for the consumer of our class.
>>> mount_everest= Position(27.9881,86.9250)
>>> str(mount_everest)
'27.9881° N, 86.925° E'
If we've customized repr() in our class and decide to skip implementing overload str() then call to str(object_name) will result in invocation of repr(). Image 4 shows how constructor delegation works with string function.
Customizing format()
Overloading __format__() can give us more precision and control. It's not necessary to overload format() in each class but the developer can overload it to return more precious information.
# format_overloading.py
class Position:
def __init__(self, latitude, longitude):
if not (-90 <= latitude <= +90):
raise ValueError(f"Latitude {latitude} out of range")
if not (-180 <= longitude <= +180):
raise ValueError(f"Longitude {longitude} out of range")
self._latitude = latitude
self._longitude = longitude
def __repr__(self):
return f"{typename(self)}(latitude={self._latitude}, longitude={self._longitude})"
@property
def latitude_hemisphere(self):
return "N" if self._latitude >= 0 else "S"
@property
def longitude_hemisphere(self):
return "E" if self._longitude >= 0 else "W"
def __str__(self):
return (
f"{abs(self._latitude)}° {self.latitude_hemisphere}, "
f"{abs(self._longitude)}° {self.longitude_hemisphere}"
)
def __format__(self, format_spec):
return "FORMATTED POSITION"
def typename(obj):
return type(obj).__name__
Below is the output of the calling overloaded format on the mount_everest object.
>>> format(mount_everest)
'FORMATTED POSITION'
>>> f"Highest mountain in the world is {mount_everest}"
'Highest mountain in the world is FORMATTED POSITION'
Image 5 shows delegation of format function in case class have implemented either str() or repr().
Conclusion
As time progresses, the size of the codebase increases and we need to implement custom classes. All classes inherits default __repr__(), __str__() and __format() and the default versions are not useful for our custom classes. As a developer, we should override __repr__() to return useful information about the class object.
Refernce:
https://docs.python.org/3/reference/datamodel.html#customization
Core python classes by Robert Smallshire and Austin Bingham
Comments