6.12. Constructor

6.12.1. Rationale

my = MyClass()
instance = MyClass.__new__()
instance.__init__()

6.12.2. Example

class Astronaut:
    def __new__(cls):
        return super().__new__(cls)

    def __init__(self):
        pass


Astronaut()
# Astronaut.__new__() called
# Astronaut.__init__() called

6.12.3. New Method

  • the 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(f'Astronaut.__new__() called')
        return super().__new__(cls)

Astronaut()
# Astronaut.__new__() called

6.12.4. Init Method

  • the initializer

  • for initializing object with data

  • 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('Astronaut.__init__() called')

Astronaut()
# Astronaut.__init__() called

6.12.5. Return

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

    def __init__(self):
        print('Astronaut.__init__() called')


Astronaut()
# Astronaut.__new__() called
# Astronaut.__init__() called

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('Astronaut.__new__() called')

    def __init__(self):
        print('Astronaut.__init__() called')  # -> is actually never called


Astronaut()
# Astronaut.__new__() called

Return invalid from constructor:

class Astronaut:
    def __new__(cls):
        print('Astronaut.__new__() called')
        return 1337

Astronaut()
# Astronaut.__new__() called
# 1337

Return invalid from initializer:

class Astronaut:
    def __init__(self):
        print('Astronaut.__new__() called')
        return 1337

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

6.12.6. Use Cases

  • 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>
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')]


class Iris:
    def __new__(cls, *args, **kwargs):
        *measurements, species = args

        if species == 'setosa':
            cls = Setosa
        elif species == 'versicolor':
            cls = Versicolor
        elif species == 'virginica':
            cls = Virginica
        else:
            raise TypeError

        return super().__new__(cls)

    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

    def __repr__(self):
        cls = self.__class__.__name__
        args = tuple(self.__dict__.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)]

6.12.7. 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 Bad:
    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 Good:
    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 Bad:
    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 Good:
    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))

6.12.8. Use Cases

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

6.12.9. Assignments

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

English:
    1. Use data from "Given" section (see below)
    2. Define class `Point` with methods:
        a. `__new__()` returning new `Point` class instances
        b. `__init__()` taking `x` and `y` and stores them as attributes
    4. Compare result with "Tests" section (see below)

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Zdefiniuj klasę `Point` z metodami:
        a. `__new__()` zwraca nową instancję klasy `Point`
        b. `__init__()` przyjmuje `x` i `y` i zapisuje je jako atrybuty
    3. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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 6.41. Solution
"""
* Assignment: OOP Constructor Passwd
* Complexity: easy
* Lines of code: 21 lines
* Time: 13 min

English:
    TODO: English translation

Polish:
    1. Użyj kodu z sekcji "Given" (patrz poniżej)
    2. Iteruj po liniach w `DATA`
    3. Odrzuć puste linie i komentarze
    4. Podziel linię po dwukropku
    5. Stwórz klasę `Account`, która zwraca instancje klas `UserAccount` lub `SystemAccount` w zależności od wartości pola UID
    6. User ID (UID) to trzecie pole, np. `root:x:0:0:root:/root:/bin/bash` to UID jest równy `0`
    7. Konta systemowe (`SystemAccount`) to takie, które w polu UID mają wartość poniżej `1000`
    8. Konta użytkowników (`UserAccount`) to takie, które w polu UID mają wartość `1000` lub więcej
    9. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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')]
"""


# Given
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"""