5.4. OOP Classmethod

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

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

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

5.4.4. Assignments

Code 5.1. Solution
"""
* Assignment: OOP Classmethod Time
* Complexity: easy
* Lines of code: 5 lines
* Time: 8 min

English:
    1. Define class `Timezone` with:
       a. Field `when: datetime`
       b. Field `tzname: str`
       c. Method `convert()` taking class and `datetime` as arguments
    2. Method `convert()` returns instance of a class, which was given
       as an argument with field set `when: datetime`
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Timezone` z:
       a. polem `when: datetime`
       b. polem `tzname: str`
       c. Metodą `convert()` przyjmującą klasę oraz `datetime` jako argumenty
    2. Metoda `convert()` zwraca instancję klasy, którą dostała jako argument
       z ustawionym polem `when: datetime`
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:

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

    >>> assert isclass(Timezone)
    >>> assert isclass(CET)
    >>> assert isclass(CEST)

    >>> dt = datetime(1969, 7, 21, 2, 56, 15)

    >>> cet = CET.convert(dt)
    >>> assert cet.tzname == 'Central European Time'
    >>> assert cet.when == datetime(1969, 7, 21, 2, 56, 15)

    >>> cest = CEST.convert(dt)
    >>> assert cest.tzname == 'Central European Summer Time'
    >>> assert cest.when == datetime(1969, 7, 21, 2, 56, 15)
"""
from datetime import datetime


class Timezone:
    tzname: str


class CET(Timezone):
    tzname = 'Central European Time'


class CEST(Timezone):
    tzname = 'Central European Summer Time'