13.8. OOP Stringify Objects

13.8.1. Rationale

  • str(obj) -> obj.__str__()

  • repr(obj) -> obj.__repr__()

  • format(obj, name) -> obj.__format__(name)

  • print(obj) -> str(obj) -> obj.__str__()

>>> import datetime
>>> date = datetime.datetime(1961, 4, 12, 6, 7)
>>>
>>> str(date)
'1961-04-12 06:07:00'
>>> repr(date)
'datetime.datetime(1961, 4, 12, 6, 7)'
>>> format(date, '%Y-%m-%d')
'1961-04-12'
>>> print(date)
1961-04-12 06:07:00

13.8.2. String

  • Calling function str(obj) calls obj.__str__()

  • Calling function print(obj) calls str(obj), which calls obj.__str__()

  • Method obj.__str__() must return str

  • for end-user

>>> class Astronaut:
...     pass
>>>
>>> astro = Astronaut()
>>> str(astro)  
'<Astronaut object at 0x...>'

Object without __str__() method overloaded prints their memory address:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
>>>
>>>
>>> astro = Astronaut('José Jiménez')
>>>
>>> print(astro)  
<Astronaut object at 0x...>
>>> str(astro)  
'<Astronaut object at 0x...>'
>>> astro.__str__()  
'<Astronaut object at 0x...>'

Objects can verbose print if __str__() method is present:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def __str__(self):
...         return f'My name... {self.name}'
>>>
>>>
>>> astro = Astronaut('José Jiménez')
>>>
>>> print(astro)
My name... José Jiménez
>>> str(astro)
'My name... José Jiménez'
>>> astro.__str__()
'My name... José Jiménez'

13.8.3. Representation

  • Calling function repr(obj) calls obj.__repr__()

  • Method obj.__repr__() must return str

  • for developers

  • object representation

  • copy-paste for creating object with the same values

  • useful for debugging

  • printing list will call __repr__() method on each element

>>> class Astronaut:
...     pass
>>>
>>> astro = Astronaut()
>>> repr(astro)  
'<Astronaut object at 0x...>'

Using __repr__() on a class:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def __repr__(self):
...         return f'Astronaut(name="{self.name}")'
>>>
>>>
>>> astro = Astronaut('José Jiménez')
>>>
>>> repr(astro)
'Astronaut(name="José Jiménez")'
>>> astro
Astronaut(name="José Jiménez")

Printing list will call __repr__() method on each element:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
>>>
>>> crew = [Astronaut('Jan Twardowski'),
...         Astronaut('Mark Watney'),
...         Astronaut('Melissa Lewis')]
>>>
>>> print(crew)  
[<Astronaut object at 0x...>, <Astronaut object at 0x...>, <Astronaut object at 0x...>]

Printing list will call __repr__() method on each element:

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def __repr__(self):
...         return f'{self.name}'
>>>
>>> crew = [Astronaut('Jan Twardowski'),
...         Astronaut('Mark Watney'),
...         Astronaut('Melissa Lewis')]
>>>
>>> print(crew)
[Jan Twardowski, Mark Watney, Melissa Lewis]

13.8.4. Format

  • Calling function format(obj, fmt) calls obj.__format__(fmt)

  • Method obj.__format__() must return str

  • Used for advanced formatting

>>> class Astronaut:
...     def __init__(self, name):
...         self.name = name
...
...     def __format__(self, mood):
...         if mood == 'happy':
...             return f"Yuppi, we're going to space!"
...         elif mood == 'scared':
...             return f"I hope we don't crash"
>>>
>>>
>>> jose = Astronaut('José Jiménez')
>>>
>>> print(f'{jose:happy}')
Yuppi, we're going to space!
>>> print(f'{jose:scared}')
I hope we don't crash
>>> SECOND = 1
>>> MINUTE = 60 * SECOND
>>> HOUR = 60 * MINUTE
>>> DAY = 24 * HOUR
>>>
>>>
>>> class Duration:
...     def __init__(self, seconds):
...         self.seconds = seconds
...
...     def __format__(self, unit):
...         if unit == 'minutes':
...             result = self.seconds / MINUTE
...         elif unit == 'hours':
...             result = self.seconds / HOUR
...         elif unit == 'days':
...             result = self.seconds / DAY
...         return str(round(result, 2))
>>>
>>> duration = Duration(seconds=3600)
>>>
>>> print(f'Duration was {duration:minutes} min')
Duration was 60.0 min
>>> print(f'Duration was {duration:hours} hour')
Duration was 1.0 hour
>>> print(f'Duration was {duration:days} day')
Duration was 0.04 day
>>> SECOND = 1
>>> MINUTE = 60 * SECOND
>>> HOUR = 60 * MINUTE
>>> DAY = 24 * HOUR
>>>
>>>
>>> class Duration:
...     seconds: int
...
...     def __init__(self, seconds):
...         self.seconds = seconds
...
...     def __format__(self, unit):
...         duration = self.seconds
...         unit = 'seconds' if unit == '' else unit
...
...         if unit in ('s', 'sec', 'second', 'seconds'):
...              duration /= SECOND
...         elif unit in ('m', 'min', 'minute', 'minutes'):
...             duration /= MINUTE
...         elif unit in ('h', 'hour', 'hours'):
...             duration /= HOUR
...         elif unit in ('d', 'day', 'days'):
...             duration /= DAY
...         return f'{duration:.2f} {unit}'
...
>>> duration = Duration(seconds=3600)
>>>
>>> print(f'Duration: {duration:s}')
Duration: 3600.00 s
>>> print(f'Duration: {duration:min}')
Duration: 60.00 min
>>> print(f'Duration: {duration:h}')
Duration: 1.00 h
>>> print(f'Duration: {duration:days}')
Duration: 0.04 days
>>> class Temperature:
...     def __init__(self, kelvin):
...         self.kelvin = kelvin
...
...     def to_fahrenheit(self):
...         return (self.kelvin-273.15) * 1.8 + 32
...
...     def to_celsius(self):
...         return self.kelvin - 273.15
...
...     def __format__(self, unit):
...         if unit == 'kelvin':
...             value = self.kelvin
...         elif unit == 'celsius':
...             value = self.to_celsius()
...         elif unit == 'fahrenheit':
...             value = self.to_fahrenheit()
...         unit = unit[0].upper()
...         return f'{value:.2f} {unit}'
>>>
>>>
>>> temp = Temperature(309.75)
>>>
>>> print(f'Temperature is {temp:kelvin}')
Temperature is 309.75 K
>>> print(f'Temperature is {temp:celsius}')
Temperature is 36.60 C
>>> print(f'Temperature is {temp:fahrenheit}')
Temperature is 97.88 F
>>> class Point:
...     def __init__(self, x, y, z=0):
...         self.x = x
...         self.y = y
...         self.z = z
...
...     def __format__(self, name):
...
...         if name == 'in_2D':
...             result = f"Point(x={self.x}, y={self.y})"
...         elif name == 'in_3D':
...             result = f"Point(x={self.x}, y={self.y}, z={self.z})"
...         elif name == 'as_dict':
...             result = vars(self)
...         elif name == 'as_tuple':
...             result = tuple(vars(self).values())
...         elif name == 'as_json':
...             import json
...             result = json.dumps(vars(self))
...         return str(result)
>>>
>>>
>>> point = Point(x=1, y=2)
>>>
>>> print(f'{point:in_2D}')
Point(x=1, y=2)
>>> print(f'{point:in_3D}')
Point(x=1, y=2, z=0)
>>> print(f'{point:as_tuple}')
(1, 2, 0)
>>> print(f'{point:as_dict}')
{'x': 1, 'y': 2, 'z': 0}
>>> print(f'{point:as_json}')
{"x": 1, "y": 2, "z": 0}

13.8.5. Assignments

Code 13.14. Solution
"""
* Assignment: OOP Stringify Str
* Required: yes
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. While printing object show: species name and a sum of `self.features`
    2. Result of sum round to one decimal place
    3. Run doctests - all must succeed

Polish:
    1. Przy wypisywaniu obiektu pokaż: nazwę gatunku i sumę `self.features`
    2. Wynik sumowania zaokrąglij do jednego miejsca po przecinku
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> for *features, label in DATA:
    ...     iris = Iris(features, label)
    ...     print(iris)
    setosa 9.4
    versicolor 16.3
    virginica 19.3
"""

DATA = [
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
    (7.0, 3.2, 4.7, 1.4, 'versicolor'),
    (7.6, 3.0, 6.6, 2.1, 'virginica'),
]


class Iris:
    def __init__(self, features, label):
        self.features = features
        self.label = label


Code 13.15. Solution
"""
* Assignment: OOP Stringify Repr
* Required: yes
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. Print representation of each instance with values (use `repr()`)
    2. Result of sum round to two decimal places
    3. Run doctests - all must succeed

Polish:
    1. Wypisz reprezentację każdej z instancji z wartościami (użyj `repr()`)
    2. Wynik sumowania zaokrąglij do dwóch miejsc po przecinku
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> result = [Iris(X,y) for *X,y in DATA]
    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Iris(features=[4.7, 3.2, 1.3, 0.2], label='setosa'),
     Iris(features=[7.0, 3.2, 4.7, 1.4], label='versicolor'),
     Iris(features=[7.6, 3.0, 6.6, 2.1], label='virginica')]
"""

DATA = [
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
    (7.0, 3.2, 4.7, 1.4, 'versicolor'),
    (7.6, 3.0, 6.6, 2.1, 'virginica'),
]


class Iris:
    def __init__(self, features, label):
        self.features = features
        self.label = label


Code 13.16. Solution
"""
* Assignment: OOP Stringify Format
* Required: yes
* Complexity: easy
* Lines of code: 8 lines
* Time: 5 min

English:
    1. Overload `__format__()` to convert length units
    2. Run doctests - all must succeed

Polish:
    1. Przeciąż `__format__()` aby konwertował jednostki długości
    2. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * 1 km = 1000 m
    * 1 m = 100 cm

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> result = Distance(meters=1337)
    >>> format(result, 'km')
    '1.337'
    >>> format(result, 'cm')
    '133700'
    >>> format(result, 'm')
    '1337'
"""


class Distance:
    def __init__(self, meters):
        self.meters = meters


Code 13.17. Solution
"""
* Assignment: OOP Stringify Nested
* Required: yes
* Complexity: medium
* Lines of code: 9 lines
* Time: 21 min

English:
    1. Overload `str` and `repr` to achieve desired printing output
    2. Run doctests - all must succeed

Polish:
    1. Przeciąż `str` i `repr` aby osiągnąć oczekiwany rezultat wypisywania
    2. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * Define `Crew.__str__()`
    * Define `Astronaut.__str__()` and `Astronaut.__repr__()`
    * Define `Mission.__repr__()`

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> melissa = Astronaut('Melissa Lewis')
    >>> print(f'Commander: \\n{melissa}\\n')  # doctest: +NORMALIZE_WHITESPACE
    Commander:
    Melissa Lewis

    >>> mark = Astronaut('Mark Watney', experience=[
    ...     Mission(2035, 'Ares 3')])
    >>> print(f'Space Pirate: \\n{mark}\\n')  # doctest: +NORMALIZE_WHITESPACE
    Space Pirate:
    Mark Watney veteran of [
          2035: Ares 3]

    >>> crew = Crew([
    ...     Astronaut('Jan Twardowski', experience=[
    ...         Mission(1969, 'Apollo 11'),
    ...         Mission(2024, 'Artemis 3'),
    ...     ]),
    ...     Astronaut('José Jiménez'),
    ...     Astronaut('Mark Watney', experience=[
    ...         Mission(2035, 'Ares 3'),
    ...     ]),
    ... ])

    >>> print(f'Crew: \\n{crew}')  # doctest: +NORMALIZE_WHITESPACE
    Crew:
    Jan Twardowski veteran of [
          1969: Apollo 11,
          2024: Artemis 3]
    José Jiménez
    Mark Watney veteran of [
          2035: Ares 3]
"""


class Crew:
    def __init__(self, members=()):
        self.members = list(members)


class Astronaut:
    def __init__(self, name, experience=()):
        self.name = name
        self.experience = list(experience)


class Mission:
    def __init__(self, year, name):
        self.year = year
        self.name = name