5.13. OOP Constructor

5.13.1. Rationale

In Object Oriented Programming constructor is:

  1. Special method

  2. Called automatically on object creation

  3. Can set instance attributes with initial values

  4. Works on not fully created object

  5. Method calls are not allowed (as of object is not ready)

  6. Returns None

Python __init__() method:

  1. Yes

  2. Yes

  3. Yes

  4. No

  5. No

  6. Yes

Python __new__() method:

  1. Yes

  2. Yes

  3. Yes (could be)

  4. Yes

  5. Yes (before instantiating) / No (after instantiating)

  6. No

In Python by definition both methods __new__() and __init__() combined and called consecutively are constructors. This is something which is not existing in other programming languages, hence programmers has problem with grasping this idea.

In most cases people will take their "experience" and "habits" from other languages, mixed with vogue knowledge about __new__() and call __init__() a constructor.

5.13.2. Example

>>> class Astronaut:
...     def __new__(cls, *args, **kwargs):
...         print('Before instantiating')
...         result = super().__new__(cls, *args, **kwargs)
...         print('After instantiating')
...         return result
...
...     def __init__(self):
...         print('Initializing')
>>>
>>>
>>> astro = Astronaut()
Before instantiating
After instantiating
Initializing

5.13.3. New Method

  • object constructor

  • solely for creating the object

  • cls as it's first parameter

  • when calling __new__() you actually don't have an instance yet, therefore no self exists at that moment

class Astronaut:
    def __new__(cls):
        print('Constructing object')
        return super().__new__(cls)


Astronaut()
# Constructing object

5.13.4. Init Method

  • object initializer

  • for initializing object with initial values

  • self as it's first parameter

  • __init__() is called after __new__() and the instance is in place, so you can use self with it

  • it's purpose is just to alter the fresh state of the newly created instance

class Astronaut:
    def __init__(self):
        print('Initializing object')


Astronaut()
# Initializing object

5.13.5. Return

class Astronaut:
    def __new__(cls):
        print('Constructing object')
        return super().__new__(cls)

    def __init__(self):
        print('Initializing object')


Astronaut()
# Constructing object
# Initializing object

Missing return from constructor. The instantiation is evaluated to None since we don't return anything from the constructor:

class Astronaut:
    def __new__(cls):
        print('Constructing object')
        super().__new__(cls)

    def __init__(self):
        print('Initializing object')  # -> is actually never called


Astronaut()
# Constructing object

Return invalid from constructor:

class Astronaut:
    def __new__(cls):
        return 'Mark Watney'

Astronaut()
# 'Mark Watney'

Return invalid from initializer:

class Astronaut:
    def __init__(self):
        return 'Mark Watney'

Astronaut()
# Traceback (most recent call last):
# TypeError: __init__() should return None, not 'str'

5.13.6. Do not trigger methods for user

  • It is better when user can choose a moment when call .connect() method

Let user to call method:

class Server:
    def __init__(self, host, username, password=None):
        self.host = host
        self.username = username
        self.password = password
        self.connect()    # Better ask user to ``connect()`` explicitly

    def connect(self):
        print(f'Logging to {self.host} using: {self.username}:{self.password}')


connection = Server(
    host='example.com',
    username='myusername',
    password='mypassword')

Let user to call method:

class Server:
    def __init__(self, host, username, password=None):
        self.host = host
        self.username = username
        self.password = password

    def connect(self):
        print(f'Logging to {self.host} using: {self.username}:{self.password}')


connection = Server(
    host='example.com',
    username='myusername',
    password='mypassword')

connection.connect()

However it is better to use self.set_position(position_x, position_y) than to set those values one by one and duplicate code. Imagine if there will be a condition boundary checking (for example for negative values):

class PositionBad:
    def __init__(self, position_x=0, position_y=0):
        self.position_x = position_x
        self.position_y = position_y

    def set_position(self, x, y):
        self.position_x = x
        self.position_y = y


class PositionGood:
    def __init__(self, position_x=0, position_y=0):
        self.set_position(position_x, position_y)

    def set_position(self, x, y):
        self.position_x = x
        self.position_y = y
class PositionBad:
    def __init__(self, position_x=0, position_y=0):
        self.position_x = min(1024, max(0, position_x))
        self.position_y = min(1024, max(0, position_y))

    def set_position(self, x, y):
        self.position_x = min(1024, max(0, x))
        self.position_y = min(1024, max(0, y))


class PositionGood:
    def __init__(self, position_x=0, position_y=0):
        self.set_position(position_x, position_y)

    def set_position(self, x, y):
        self.position_x = min(1024, max(0, x))
        self.position_y = min(1024, max(0, y))

5.13.7. Use Cases - Abstract Factory

  • Factory method

  • Could be used to implement Singleton

class PDF:
    pass

class Docx:
    pass

class Document:
    def __new__(cls, *args, **kwargs):
        filename, extension = args[0].split('.')

        if extension == 'pdf':
            return PDF()
        elif extension == 'docx':
            return Docx()


file1 = Document('myfile.pdf')
file2 = Document('myfile.docx')

print(file1)
# <__main__.PDF object at 0x10f89afa0>

print(file2)
# <__main__.Docx object at 0x10f6fe9a0>

5.13.8. Use Case - Iris Factory

from dataclasses import dataclass


DATA = [(5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor'),
        (6.3, 2.9, 5.6, 1.8, 'virginica'),
        (6.4, 3.2, 4.5, 1.5, 'versicolor'),
        (4.7, 3.2, 1.3, 0.2, 'setosa')]


@dataclass(repr=False)
class Iris:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

    def __new__(cls, *args, **kwargs):
        *measurements, species = args
        clsname = species.capitalize()
        cls = globals()[clsname]
        return super().__new__(cls)

    def __repr__(self):
        cls = self.__class__.__name__
        args = tuple(vars(self).values())
        return f'\n{cls}{args}'


class Setosa(Iris):
    pass

class Virginica(Iris):
    pass

class Versicolor(Iris):
    pass


result = [Iris(*row) for row in DATA]
result
# [Virginica(5.8, 2.7, 5.1, 1.9),
#  Setosa(5.1, 3.5, 1.4, 0.2),
#  Versicolor(5.7, 2.8, 4.1, 1.3),
#  Virginica(6.3, 2.9, 5.6, 1.8),
#  Versicolor(6.4, 3.2, 4.5, 1.5),
#  Setosa(4.7, 3.2, 1.3, 0.2)]

5.13.9. Use Cases - Path

Note, that this unfortunately does not work this way. Path() always returns PosixPath:

from pathlib import Path


Path('/etc/passwd')
# PosixPath('/etc/passwd')

Path('c:\\Users\\Admin\\myfile.txt')
# WindowsPath('c:\\Users\\Admin\\myfile.txt')

Path(r'C:\Users\Admin\myfile.txt')
# WindowsPath('C:\\Users\\Admin\\myfile.txt')

Path(r'C:/Users/Admin/myfile.txt')
# WindowsPath('C:/Users/Admin/myfile.txt')

5.13.10. Assignments

Code 5.44. Solution
"""
* Assignment: OOP Constructor Syntax
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 min

English:
    1. Define class `Point` with methods:
        a. `__new__()` returning new `Point` class instances
        b. `__init__()` taking `x` and `y` and stores them as attributes
    2. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Point` z metodami:
        a. `__new__()` zwraca nową instancję klasy `Point`
        b. `__init__()` przyjmuje `x` i `y` i zapisuje je jako atrybuty
    2. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(Point)
    >>> assert hasattr(Point, '__new__')
    >>> assert hasattr(Point, '__init__')
    >>> pt = Point.__new__(Point)
    >>> assert type(pt) is Point
    >>> pt.__init__(1, 2)
    >>> assert pt.x == 1
    >>> assert pt.y == 2
"""

Code 5.45. Solution
"""
* Assignment: OOP Constructor Passwd
* Complexity: easy
* Lines of code: 21 lines
* Time: 13 min

English:
    TODO: English translation
    X. Run doctests - all must succeed

Polish:
    1. Iteruj po liniach w `DATA`
    2. Odrzuć puste linie i komentarze
    3. Podziel linię po dwukropku
    4. Stwórz klasę `Account`, która zwraca instancje klas `UserAccount` lub `SystemAccount` w zależności od wartości pola UID
    5. User ID (UID) to trzecie pole, np. `root:x:0:0:root:/root:/bin/bash` to UID jest równy `0`
    6. Konta systemowe (`SystemAccount`) to takie, które w polu UID mają wartość poniżej `1000`
    7. Konta użytkowników (`UserAccount`) to takie, które w polu UID mają wartość `1000` lub więcej
    8. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [SystemAccount(username='root'),
     SystemAccount(username='bin'),
     SystemAccount(username='daemon'),
     SystemAccount(username='adm'),
     SystemAccount(username='shutdown'),
     SystemAccount(username='halt'),
     SystemAccount(username='nobody'),
     SystemAccount(username='sshd'),
     UserAccount(username='twardowski'),
     UserAccount(username='jimenez'),
     UserAccount(username='ivanovic'),
     UserAccount(username='lewis')]
"""

DATA = """root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
nobody:x:99:99:Nobody:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
twardowski:x:1000:1000:Jan Twardowski:/home/twardowski:/bin/bash
jimenez:x:1001:1001:José Jiménez:/home/jimenez:/bin/bash
ivanovic:x:1002:1002:Иван Иванович:/home/ivanovic:/bin/bash
lewis:x:1002:1002:Melissa Lewis:/home/lewis:/bin/bash"""