13.3. OOP Init Method

13.3.1. Rationale

  • It's a first method run after object is initiated

  • All classes has default __init__()

constructor

Method called at object instantiation used to create object. Constructor is called on not fully initialized object and hence do not have access to object methods. Constructor should return None.

initializer

Method called at object instantiation used to fill empty object with values. Initializer is called upon object initialization and hence can modify object and use its methods. Initializer should return None.

13.3.2. Syntax

>>> class MyClass:
...     myattribute: str
...
...     def __init__(self, myvar):
...         self.myattribute = myvar
>>>
>>>
>>> myobj = MyClass('myvalue')
>>>
>>> print(myobj.myattribute)
myvalue

13.3.3. Initializer Method Without Arguments

Initializer method without arguments:

>>> class Astronaut:
...     def __init__(self):
...         print('Hello')
>>>
>>>
>>> astro = Astronaut()
Hello

13.3.4. Initializer Method With Arguments

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         print(f'Hello {firstname} {lastname}')
>>>
>>>
>>> astro = Astronaut()
Traceback (most recent call last):
TypeError: __init__() missing 2 required positional arguments: 'firstname' and 'lastname'
>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         print(f'Hello {firstname} {lastname}')
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
Hello Mark Watney
>>>
>>> astro = Astronaut(firstname='Mark', lastname='Watney')
Hello Mark Watney
>>> class Astronaut:
...     def __init__(self, firstname, lastname='Unknown'):
...         print(f'Hello {firstname} {lastname}')
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
Hello Mark Watney
>>>
>>> astro = Astronaut('Mark')
Hello Mark Unknown

13.3.5. Constant Attributes

>>> class Astronaut:
...     def __init__(self):
...         self.firstname = 'Mark'
...         self.lastname = 'Watney'
>>>
>>>
>>> mark = Astronaut()
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> melissa = Astronaut()
>>> vars(melissa)
{'firstname': 'Mark', 'lastname': 'Watney'}

13.3.6. Variable Attributes

>>> class Astronaut:
...     def __init__(self, a, b):
...         self.firstname = a
...         self.lastname = b
>>>
>>>
>>> mark = Astronaut('Mark', 'Watney')
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> ivan = Astronaut(a='Ivan', b='Ivanovich')
>>> vars(ivan)
{'firstname': 'Ivan', 'lastname': 'Ivanovich'}
>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> mark = Astronaut('Mark', 'Watney')
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}
>>>
>>> ivan = Astronaut(firstname='Ivan', lastname='Ivanovich')
>>> vars(ivan)
{'firstname': 'Ivan', 'lastname': 'Ivanovich'}

13.3.7. Combine Attributes

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self.name = f'{firstname} {lastname}'
>>>
>>>
>>> mark = Astronaut('Mark', 'Watney')
>>>
>>> print(mark.name)
Mark Watney
>>>
>>> print(mark.firstname)
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'firstname'
>>>
>>> print(mark.lastname)
Traceback (most recent call last):
AttributeError: 'Astronaut' object has no attribute 'lastname'

13.3.8. Example

>>> class Point:
...     def __init__(self, x, y, z=0):
...         self.x = x
...         self.y = y
...         self.z = z
>>>
>>>
>>> p1 = Point(10, 20)
>>> p2 = Point(x=10, y=20)
>>> p3 = Point(10, 20, 30)
>>> p4 = Point(10, 20, z=30)
>>> p5 = Point(x=10, y=20, z=30)

13.3.9. Checking Values

>>> class Point:
...     x: int
...     y: int
...
...     def __init__(self, x, y):
...         if x < 0 or y < 0:
...             raise ValueError('Coordinate cannot be negative')
...         self.x = x
...         self.y = y
>>>
>>>
>>> point1 = Point(x=1, y=2)
>>> vars(point1)
{'x': 1, 'y': 2}
>>>
>>> point2 = Point(x=-1, y=-2)
Traceback (most recent call last):
ValueError: Coordinate cannot be negative

13.3.10. Use Case - Iris

>>> class Iris:
...     def __init__(self, sepal_length, sepal_width,
...                  petal_length, petal_width, species):
...         self.sepal_length = sepal_length
...         self.sepal_width = sepal_width
...         self.petal_length = petal_length
...         self.petal_width = petal_width
...         self.species = species
>>>
>>>
>>> setosa = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
>>>
>>> vars(setosa)  
{'sepal_length': 5.1,
 'sepal_width': 3.5,
 'petal_length': 1.4,
 'petal_width': 0.2,
 'species': 'setosa'}
>>> class Iris:
...     def __init__(self, sepal_length, sepal_width,
...                  petal_length, petal_width, species):
...         self.sepal_length = sepal_length
...         self.sepal_width = sepal_width
...         self.petal_length = petal_length
...         self.petal_width = petal_width
...         self.species = species
>>>
>>>
>>> virginica = Iris(
...     sepal_length=5.8,
...     sepal_width=2.7,
...     petal_length=5.1,
...     petal_width=1.9,
...     species='virginica')
>>>
>>> vars(virginica)  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': 5.1,
 'petal_width': 1.9,
 'species': 'virginica'}

13.3.11. Use Case - Dataclasses

Since Python 3.7: there is a @dataclass decorator, which automatically generates __init__() arguments and fields. More information in OOP Dataclass.

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
...     species: str = 'Iris'
>>>
>>>
>>> virginica = Iris(
...     sepal_length=5.8,
...     sepal_width=2.7,
...     petal_length=5.1,
...     petal_width=1.9,
...     species='virginica')
>>>
>>> vars(virginica)  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': 5.1,
 'petal_width': 1.9,
 'species': 'virginica'}

13.3.12. Use Case - Kelvin

>>> class Kelvin:
...     value: float
...     MINIMAL_VALUE = 0.0
...
...     def __init__(self, value):
...         if value < self.MINIMAL_VALUE:
...             raise ValueError('Temperature must be greater than 0')
...         self.value = value
>>>
>>>
>>> t1 = Kelvin(273.15)
>>> print(t1.value)
273.15
>>>
>>> t2 = Kelvin(-300)
Traceback (most recent call last):
ValueError: Temperature must be greater than 0

13.3.13. Use Case - Boundaries

>>> class Point:
...     x: int
...     y: int
...     z: int
...
...     def __init__(self, x, y, z):
...         if not 0 <= x < 1024:
...             raise ValueError(f'{x} is out of boundary')
...         elif not 0 <= y < 1024:
...             raise ValueError(f'{y} is out of boundary')
...         elif not 0 <= z < 1024:
...             raise ValueError(f'{z} is out of boundary')
...         else:
...             self.x = x
...             self.y = y
...             self.z = z
>>>
>>>
>>> point1 = Point(x=-10, y=1, z=0)
Traceback (most recent call last):
ValueError: x=-10 is out of boundary 0, 1024

13.3.14. Use Case - Parametrized Boundaries

>>> class Point:
...     x: int
...     y: int
...     z: int
...
...     X_MIN: int = 0
...     X_MAX: int = 1024
...     Y_MIN: int = 0
...     Y_MAX: int = 1024
...     Z_MIN: int = 20
...     Z_MAX: int = 500
...
...     def __init__(self, x: int, y: int, z: int):
...         if not self.X_MIN <= x < self.X_MAX:
...             raise ValueError(f'{x=} is out of boundary {self.X_MIN}, {self.X_MAX}')
...         elif not self.Y_MIN <= y < self.Y_MAX:
...             raise ValueError(f'{y=} is out of boundary {self.Y_MIN}, {self.Y_MAX}')
...         elif not self.Z_MIN <= z < self.Z_MAX:
...             raise ValueError(f'{z=} is out of boundary {self.Z_MIN}, {self.Z_MAX}')
...         else:
...             self.x = x
...             self.y = y
...             self.z = z
>>>
>>>
>>> point1 = Point(x=-10, y=1, z=0)
Traceback (most recent call last):
ValueError: x=-10 is out of boundary 0, 1024

13.3.15. Assignments

Code 13.4. Solution
"""
* Assignment: OOP Init Print
* Required: yes
* Complexity: easy
* Lines of code: 3 lines
* Time: 3 min

English:
    1. Create one class `Echo`
    2. Value `text` must be passed at the initialization
    3. At initialization instance print `text`
    4. Do not store values in the instances (only print on instance creation)
    5. Do not use `@dataclass`
    6. Run doctests - all must succeed

Polish:
    1. Stwórz jedną klasę `Echo`
    2. Wartość `text` maja być podawana przy inicjalizacji
    3. Przy inicjalizacji instancja wypisuje `text`
    4. Nie przechowuj informacji w instancjach
       (tylko wypisz przy inicjalizacji)
    5. Nie używaj `@dataclass`
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> _ = Echo('hello')
    hello
    >>> _ = Echo('world')
    world
    >>> result = Echo('Test')
    Test
    >>> vars(result)
    {}
"""


Code 13.5. Solution
"""
* Assignment: OOP Init Model
* Required: yes
* Complexity: easy
* Lines of code: 12 lines
* Time: 8 min

English:
    1. Model the data using classes Astronaut and SpaceAgency
       a. Watney, USA, 1969-07-21
       b. NASA, USA, 1958-07-29
    2. Create instances (watney, nasa) filling it with data
    3. Values must be passed at the initialization
    4. Create instances of a first class using positional arguments
    5. Create instances of a second class using keyword arguments
    6. Do not use `@dataclass`
    7. Run doctests - all must succeed

Polish:
    1. Zamodeluj dane za pomocą klas Astronaut i SpaceAgency
       a. Watney, USA, 1969-07-21
       b. NASA, USA, 1958-07-29
    2. Stwórz instancje (watney, nasa) wypełniając je danymi
    3. Wartości mają być podawane przy inicjalizacji
    4. Twórz instancje pierwszej klasy używając argumentów pozycyjnych
    5. Twórz instancje drugiej klasy używając argumentów nazwanych
    6. Nie używaj `@dataclass`
    7. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isinstance(watney, Astronaut)
    >>> assert isinstance(nasa, SpaceAgency)
    >>> assert 'Watney' in vars(watney).values()
    >>> assert 'USA' in vars(watney).values()
    >>> assert '1969-07-21' in vars(watney).values()
    >>> assert 'NASA' in vars(nasa).values()
    >>> assert 'USA' in vars(nasa).values()
    >>> assert '1958-07-29' in vars(nasa).values()
"""


# Watney, USA, 1969-07-21
# NASA, USA, 1958-07-29