-
Notifications
You must be signed in to change notification settings - Fork 0
Love Your Representation
Embrace the power of __repr__
There are two ways out that Python provides us with, out of the box, to convert an object into a string: __str__
and __repr__
.
__str__
gets all the love, __repr__
is a bit more obscure.
What is the difference anyway?
-
__str__
is meant to define the "informal" or user-friendly string representation of an object. It's used when you want to provide a human-readable description of the object. -
__repr__
is meant for the "formal" or unambiguous string representation of an object. It's used for debugging, development, and to represent the object in a way that could be used to recreate the same object.
Think of a Python object as a toy. When we want to describe this toy to someone, we can use two ways:
__str__
is like giving a friendly and simple description of the toy. It's what we tell our friends when we want them to know what the toy looks like or what it does. It's like saying, "This toy is colourful and fun to play with!"
__repr__
is like giving a very detailed and technical description of the toy. It's what we tell someone who needs to know all the nitty-gritty details about the toy. A look under the hood.
Out of the box, python provides an unambiguous representation.
>>> class Simple:
... pass
...
>>> simple = Simple()
>>> simple
<__main__.Simple object at 0x7f3d3b5fa230>
>>>
That is not much help.
A better example is the datetime
module.
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> now
datetime.datetime(2023, 11, 3, 18, 55, 45, 862092)
>>>
Data classes provide a nice implementation.
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class InventoryItem:
... """Class for keeping track of an item in inventory."""
... name: str
... unit_price: float
... quantity_on_hand: int = 0
...
>>> ii = InventoryItem('python', 897)
>>> ii
InventoryItem(name='python', unit_price=897, quantity_on_hand=0)
In the Python Standard Library, we also find some weird examples
>>> from enum import Enum
>>>
>>> class DataType(Enum):
... string = "string"
... int_ = "int"
... uint = "uint"
... short = "short"
... ushort = "ushort"
... float_ = "float"
... double = "double"
... bool_ = "bool"
...
>>> ui = DataType('uint')
>>> ui
<DataType.uint: 'uint'>
But that can be overridden (this is copied from: "3.12.0 Documentation » The Python Standard Library » Data Types » enum — Support for enumerations")
>>> from enum import auto
>>> class OtherStyle(Enum):
... ALTERNATE = auto()
... OTHER = auto()
... SOMETHING_ELSE = auto()
... def __repr__(self):
... cls_name = self.__class__.__name__
... return f'{cls_name}.{self.name}'
...
>>> OtherStyle.ALTERNATE
OtherStyle.ALTERNATE
Another example is fastkml
>>> from fastkml.gx import Track
>>> doc = """
... <gx:Track xmlns:gx="http://www.google.com/kml/ext/2.2"
... xmlns:kml="http://www.opengis.net/kml/2.2">
... <kml:when>2010-05-28T02:02:09Z</kml:when>
... <kml:when>2010-05-28T02:02:56Z</kml:when>
... <kml:when />
... <gx:angles>45.54 66.23 77.0</gx:angles>
... <gx:angles />
... <gx:angles>75.54 86.23 17.0</gx:angles>
... <gx:coord>-122.20 37.37 156.00</gx:coord>
... <gx:coord>-122.20 37.37 152.00</gx:coord>
... <gx:coord>-122.20 37.37 147.00</gx:coord>
... </gx:Track>
... """
>>> track = Track.from_string(doc, ns="")
>>> track
Track(
ns="",
id="",
target_id="",
extrude=None,
tessellate=None,
altitude_mode=None,
track_items=[
TrackItem(
when=datetime.datetime(2010, 5, 28, 2, 2, 9, tzinfo=tzutc()),
coord=Point(-122.2, 37.37, 156.0),
angle=Angle(heading=45.54, tilt=66.23, roll=77.0),
),
TrackItem(
when=datetime.datetime(2010, 5, 28, 2, 2, 56, tzinfo=tzutc()),
coord=Point(-122.2, 37.37, 152.0),
angle=None,
),
TrackItem(
when=None,
coord=Point(-122.2, 37.37, 147.0),
angle=Angle(heading=75.54, tilt=86.23, roll=17.0),
),
],
)
This is implemented as
class Track(_Geometry):
"""A track describes how an object moves through the world over a given time period."""
def __init__(
self,
*,
ns: Optional[str] = None,
id: Optional[str] = None,
target_id: Optional[str] = None,
extrude: Optional[bool] = False,
tessellate: Optional[bool] = False,
altitude_mode: Optional[AltitudeMode] = None,
track_items: Optional[Sequence[TrackItem]] = None,
) -> None:
...
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"ns={self.ns!r}, "
f"id={self.id!r}, "
f"target_id={self.target_id!r}, "
f"extrude={self.extrude!r}, "
f"tessellate={self.tessellate!r}, "
f"altitude_mode={self.altitude_mode}, "
f"track_items={self.track_items!r}"
")"
)
The pattern is simple, construct a string out of the objects attributes for each parameter in the __init__
method.
This could be the end. Just add a __repr__
that mirrors the __init_
signature of the class. There is nothing much to it, easily done and it will save you time and effort in the future when you have to debug your code or figure out what caused a bug from the log files.
That looks like a lot of ungrateful, repetitive manual labour.
Being a pythonista - I’m too lazy to spend 8 hours mindlessly performing a function, but not too lazy to spend 16 hours automating it.
It is pronounced like crêpe, the French pancake.
A Python script that takes a module name as a command-line argument, imports the specified module, and then prints a __repr__
method for each class defined in the module. It uses the definition found in the __init__
method of the class.
❯ crepr --help
Usage: crepr [OPTIONS] FILE_NAME
Create a __repr__ method for each class in the specified file.
Arguments:
FILE_NAME [required]
Options:
--help Show this message and exit.
For a class definition like
class KwOnly:
def __init__(self, name: str, *, age: int) -> None:
"""Initialize the class."""
self.name = name
self.age = age
The output is
class KwOnly:
"""The happy path class."""
def __init__(self, name: str, *, age: int) -> None:
"""Initialize the class."""
self.name = name
self.age = age
def __repr__(self) -> str:
"""Return the string (c)representation of KwOnly"""
return (f'{self.__class__.__name__}('
f'name={self.name!r}, '
f'age={self.age!r}, '
')')
It is Unix-ish, does just one thing: it prints the modified file to StdOut
so you can redirect or pipe the output.
The code is tidy and clean, follows best practises, is fully tested and type checked as a hypermodern package should be.
You don't have to work in an unsafe environment when you contribute to this project.
It is still in its infancy, but you can install it from PyPI.
That is it, Give your representations some love.
❤️.__repr__(self) -> str: