7.4. Classmethod

7.4.1. Rationale

  • Using class as namespace

  • Will pass class as a first argument

  • self is not required

class MyClass:
    def mymethod(self):
        pass
class MyClass:
    @staticmethod
    def mymethod():
        pass
class MyClass:
    @classmethod
    def mymethod(cls):
        pass

7.4.2. Example

import json
from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str

    def from_json(self, data):
        data = json.loads(data)
        return User(**data)


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# Traceback (most recent call last):
# TypeError: __init__() missing 2 required positional arguments: 'firstname' and 'lastname'

User(None, None).from_json(DATA)
# User(firstname='Jan', lastname='Twardowski'
import json
from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str

    @staticmethod
    def from_json(data):
        data = json.loads(data)
        return User(**data)


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# User(firstname='Jan', lastname='Twardowski'
import json
from dataclasses import dataclass


class JSONMixin:
    @staticmethod
    def from_json(data):
        data = json.loads(data)
        return User(**data)


@dataclass
class User(JSONMixin):
    firstname: str
    lastname: str


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

print(User.from_json(DATA))
# User(firstname='Jan', lastname='Twardowski')
import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        return User(**data)


@dataclass
class User(JSONMixin):
    firstname: str = None
    lastname: str = None


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# User(firstname='Jan', lastname='Twardowski')

Trying to use method with self:

import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        return self(**data)


@dataclass
class User(JSONMixin):
    firstname: str = None
    lastname: str = None


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# Traceback (most recent call last):
# TypeError: 'User' object is not callable

Trying to use method with self.__init__():

import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        self.__init__(**data)
        return self


@dataclass
class User(JSONMixin):
    firstname: str = None
    lastname: str = None


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# User(firstname='Jan', lastname='Twardowski')

Trying to use methods self.__new__() and self.__init__():

import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        instance = object.__new__(type(self))
        instance.__init__(**data)
        return instance


@dataclass
class User(JSONMixin):
    firstname: str = None
    lastname: str = None


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# User(firstname='Jan', lastname='Twardowski')
import json
from dataclasses import dataclass


class JSONMixin:
    def from_json(self, data):
        data = json.loads(data)
        instance = object.__new__(type(self))
        instance.__init__(**data)
        return instance


@dataclass
class User(JSONMixin):
    firstname: str = None
    lastname: str = None


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# Traceback (most recent call last):
# TypeError: from_json() missing 1 required positional argument: 'data'

User().from_json(DATA)
# User(firstname='Jan', lastname='Twardowski')
import json
from dataclasses import dataclass


class JSONMixin:
    @classmethod
    def from_json(cls, data):
        data = json.loads(data)
        return cls(**data)

@dataclass
class User(JSONMixin):
    firstname: str
    lastname: str


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

User.from_json(DATA)
# User(firstname='Jan', lastname='Twardowski')

7.4.3. Use Cases

import json
from dataclasses import dataclass


class JSONMixin:
    @classmethod
    def from_json(cls, data):
        data = json.loads(data)
        return cls(**data)

@dataclass
class Guest(JSONMixin):
    firstname: str
    lastname: str

@dataclass
class Admin(JSONMixin):
    firstname: str
    lastname: str


DATA = '{"firstname": "Jan", "lastname": "Twardowski"}'

Guest.from_json(DATA)
# Guest(firstname='Jan', lastname='Twardowski')

Admin.from_json(DATA)
# Admin(firstname='Jan', lastname='Twardowski')
class AbstractTime:
    tzname: str
    tzcode: str

    @classmethod
    def parse(cls, text):
        result = ...
        return cls(**result)

class MartianTime(AbstractTime):
    tzname = 'Coordinated Mars Time'
    tzcode = 'MTC'

class LunarTime(AbstractTime):
    tzname = 'Lunar Standard Time'
    tzcode = 'LST'

class EarthTime(AbstractTime):
    tzname = 'Universal Time Coordinated'
    tzcode = 'UTC'


# Settings
time = MartianTime

# Usage
from settings import time

UTC = '1969-07-21T02:53:07Z'

dt = time.parse(UTC)
print(dt.tzname)
# Coordinated Mars Time

7.4.4. Assignments

Code 7.24. Solution
"""
* Assignment: Protocol Classmethod CSV
* Complexity: easy
* Lines of code: 5 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. To class `CSVMixin` add methods:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    3. `CSVMixin.to_csv()` should return attribute values separated with coma
    4. `CSVMixin.from_csv()` should return instance of a class on which it was called
    5. Use `@classmethod` decorator in proper place
    6. All tests must pass
    7. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Do klasy `CSVMixin` dodaj metody:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> Union['Astronaut', 'Cosmonaut']`
    3. `CSVMixin.to_csv()` powinna zwracać wartości atrybutów klasy rozdzielone po przecinku
    4. `CSVMixin.from_csv()` powinna zwracać instancje klasy na której została wywołana
    5. Użyj dekoratora `@classmethod` w odpowiednim miejscu
    6. Wszystkie testy muszą przejść
    7. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Hints:
    * `CSVMixin.to_csv()` should add newline `\n` at the end of line
    * `CSVMixin.from_csv()` should remove newline `\n` at the end of line

Tests:
    >>> from dataclasses import dataclass

    >>> @dataclass
    ... class Astronaut(CSVMixin):
    ...     firstname: str
    ...     lastname: str
    ...
    >>> @dataclass
    ... class Cosmonaut(CSVMixin):
    ...     firstname: str
    ...     lastname: str

    >>> mark = Astronaut('Mark', 'Watney')
    >>> jan = Cosmonaut('Jan', 'Twardowski')
    >>> mark.to_csv()
    'Mark,Watney\\n'
    >>> jan.to_csv()
    'Jan,Twardowski\\n'

    >>> with open('_temporary.txt', mode='wt') as file:
    ...     data = mark.to_csv() + jan.to_csv()
    ...     file.writelines(data)

    >>> result = []
    >>> with open('_temporary.txt', mode='rt') as file:
    ...     lines = file.readlines()
    ...     result += [Astronaut.from_csv(lines[0])]
    ...     result += [Cosmonaut.from_csv(lines[1])]

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Astronaut(firstname='Mark', lastname='Watney'),
     Cosmonaut(firstname='Jan', lastname='Twardowski')]
    >>> from os import remove
    >>> remove('_temporary.txt')
"""


# Given
from typing import Union


class CSVMixin:
    def to_csv(self) -> str:
        ...

    @classmethod
    def from_csv(cls, line: str) -> Union['Astronaut', 'Cosmonaut']:
        ...