12.3. Operators Stringify

12.3.1. Rationale

12.3.2. Rationale

Table 12.2. String Operator Overload

Operator

Method

str(obj)

obj.__str__()

repr(obj)

obj.__repr__()

format(obj, name)

obj.__format__(name)

print(obj)

str(obj) -> obj.__str__()

obj + other

obj.__add__(other)

obj - other

obj.__sub__(other)

obj * other

obj.__mul__(other)

obj % other

obj.__mod__(other)

obj += other

obj.__iadd__(other)

obj -= other

obj.__isub__(other)

obj *= other

obj.__imul__(other)

obj %= other

obj.__imod__(other)

12.3.3. Example

>>> import datetime
>>> date = datetime.date(1961, 4, 12)
>>>
>>>
>>> str(date)
'1961-04-12'
>>>
>>> repr(date)
'datetime.date(1961, 4, 12)'
>>>
>>> format(date, '%Y-%m-%d')
'1961-04-12'
>>>
>>> print(date)
1961-04-12

12.3.4. 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'

12.3.5. 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]

12.3.6. 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

12.3.7. Use Case - Mod

  • % (__mod__) operator behavior for int and str:

>>> 13 % 4
1
>>>
>>> '13' % '4'
Traceback (most recent call last):
TypeError: not all arguments converted during string formatting
>>> pi = 3.1514
>>>
>>>
>>> 'String: %s' % pi
'String: 3.1514'
>>>
>>> 'Double: %d' % pi
'Double: 3'
>>>
>>> 'Float: %f' % pi
'Float: 3.151400'
>>> firstname = 'Mark'
>>> lastname = 'Watney'
>>>
>>>
>>> 'Hello %s' % firstname
'Hello Mark'
>>>
>>> 'Hello %s %s' % (firstname, lastname)
'Hello Mark Watney'
>>>
>>> 'Hello %(fname)s %(lname)s' % {'fname': firstname, 'lname': lastname}
'Hello Mark Watney'
>>> text = 'Hello %s'
>>> text %= 'Mark Watney'
>>>
>>> print(text)
Hello Mark Watney
>>> class Str:
...     def __mod__(self, other):
...         """str substitute"""
...
...         if type(other) is str:
...             ...
...         if type(other) is tuple:
...             ...
...         if type(other) is dict:
...             ...

Note, that using %s, %d, %f is currently deprecated in favor of f'...' string formatting. More information in Builtin Printing

12.3.8. Use Case - Duration

>>> 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

12.3.9. Use Case - Duration Many Units

>>> 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

12.3.10. Use Case - Temperature

>>> 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

12.3.11. Use Case - Point

>>> 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}

12.3.12. Assignments

Code 12.23. Solution
"""
* Assignment: Operators String Str
* Required: yes
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

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

Polish:
    1. Przeciąż `str()`
    2. Przy wypisywaniu obiektu pokaż: nazwę gatunku i sumę `self.features`
    3. Wynik sumowania zaokrąglij do jednego miejsca po przecinku
    4. 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:
    features: list
    label: str

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


Code 12.24. Solution
"""
* Assignment: Operators String Repr
* Required: yes
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. Overload `repr()`
    2. Run doctests - all must succeed

Polish:
    1. Przeciąż `repr()`
    2. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass, ismethod

    >>> assert isclass(Iris)
        >>> iris = Iris(DATA)

    >>> assert hasattr(Iris, '__repr__')
    >>> assert ismethod(iris.__repr__)
    >>> repr(iris)
    "Iris(features=[4.7, 3.2, 1.3, 0.2], label='setosa')"
"""
from typing import Any, List


DATA = (4.7, 3.2, 1.3, 0.2, 'setosa')

# repr() -> Iris(features=[4.7, 3.2, 1.3, 0.2], label='setosa')
class Iris:
    features: list
    label: str

    def __init__(self, data):
        self.features = list(data[:-1])
        self.label = str(data[-1])


Code 12.25. Solution
"""
* Assignment: Operators String Format
* Required: yes
* Complexity: easy
* Lines of code: 8 lines
* Time: 8 min

English:
    1. Overload `format()`
    2. Has to convert length units: km, cm, m
    3. Run doctests - all must succeed

Polish:
    1. Przeciąż `format()`
    2. Ma konwertować jednostki długości: km, cm, m
    3. 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:
    meters: int

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


Code 12.26. Solution
"""
* Assignment: Operators String Nested
* Required: yes
* Complexity: medium
* Lines of code: 9 lines
* Time: 13 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