3.7. OOP Method Classmethod

  • Using class as namespace

  • Will pass class as a first argument

  • self is not required

Dynamic methods:

>>> class MyClass:
...     def mymethod(self):
...         pass

Static methods:

>>> class MyClass:
...     @staticmethod
...     def mymethod():
...         pass

Class methods:

>>> class MyClass:
...     @classmethod
...     def mymethod(cls):
...         pass

3.7.1. Manifestation

>>> 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": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')
>>> 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": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')
>>> 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": "Mark", "lastname": "Watney"}'
>>> result = User.from_json(data)
>>>
>>> print(result)
User(firstname='Mark', lastname='Watney')

3.7.2. Use Case - 0x01

  • Singleton

>>> class Singleton:
...     _instance: object
...
...     @classmethod
...     def get_instance(cls):
...         if not hasattr(cls, '_instance'):
...             cls._instance = object.__new__(cls)
...         return cls._instance
>>>
>>>
>>> class Astronaut(Singleton):
...     pass
>>>
>>> class Cosmonaut(Singleton):
...     pass
>>>
>>>
>>> a = Astronaut.get_instance()
>>> b = Cosmonaut.get_instance()
>>>
>>>
>>> print(a)  
<__main__.Astronaut object at 0x102453ee0>
>>>
>>> print(b)  
<__main__.Cosmonaut object at 0x102453ee0>

3.7.3. Use Case - 0x02

  • JSONMixin

>>> 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": "Mark", "lastname": "Watney"}'
>>>
>>> Guest.from_json(data)
Guest(firstname='Mark', lastname='Watney')
>>>
>>> Admin.from_json(data)
Admin(firstname='Mark', lastname='Watney')

3.7.4. Use Case - 0x03

File myapp/database.py:

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Database:
...     dbtype: str | None = None
...     dbname: str | None = None
...     host: str | None = None
...     port: str | None = None
...     username: str | None = None
...     password: str | None = None
...
...     @classmethod
...     def connect(cls, host, port, dbname, username, password):
...         driver = cls.driver
...         return driver.connect(host, port, dbname, username, password)
...
...     def execute(self, sql: str):
...         return ...
>>>
>>> @dataclass
... class PostgreSQL(Database):
...     driver: str = 'psycopg3'
...     port: int = 5432
>>>
>>> @dataclass
... class MySQL(Database):
...     driver: str = 'pymysql'
...     port: int = 3306
>>>
>>> @dataclass
... class Oracle(Database):
...     driver: str = 'oracledb'
...     port: int = 1521

File myapp/settings.py:

>>> 
... from myapp.database import *
...
... database = PostgreSQL

File myapp/main.py:

>>> 
... from myapp.settings import db
...
... connection = database.connect(
...     host='localhost',
...     port=5432,
...     dbname='nasa',
...     username='mwatney',
...     password='Ares3',
... )
...
... connection.execute('SELECT * FROM astronauts')

3.7.5. Use Case - 0x04

  • STAGE: prod / test

3.7.6. Use Case - 0x04

  • Interplanetary time

>>> # myapp/time.py
>>> class AbstractTime:
...     tzname: str
...     tzcode: str
...
...     def __init__(self, date, time):
...         ...
...
...     @classmethod
...     def parse(cls, text):
...         result = {'date': ..., 'time': ...}
...         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'
>>> # myapp/settings.py
>>> from myapp.time import *  
>>>
>>> time = MartianTime
>>> # myapp/usage.py
>>> from myapp.settings import time  
>>>
>>> UTC = '1969-07-21T02:53:07Z'
>>>
>>> dt = time.parse(UTC)
>>> print(dt.tzname)
Coordinated Mars Time

3.7.7. Use Case - 0x04

  • Interplanetary time

>>> # myapp/time.py
>>> class AbstractTime:
...     tzname: str
...     tzcode: str
...
...     def __init__(self, date, time):
...         ...
...
...     @classmethod
...     def parse(cls, text):
...         result = {'date': ..., 'time': ...}
...         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'
>>> # myapp/settings.py
>>> 
... import myapp.time
... from myapp.time import *
... from os import getenv
...
... time = getattr(myapp.time, getenv('MISSION_TIME'))  
>>> # myapp/usage.py
>>> 
... from myapp.settings import time
...
... UTC = '1969-07-21T02:53:07Z'
...
... dt = time.parse(UTC)
... print(dt.tzname)
Coordinated Mars Time

3.7.8. Use Case - 0x05

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str = None
...     lastname: str = None
...
...     @classmethod
...     def from_json(cls, data):
...         import json
...         data = json.loads(data)
...         return cls(**data)
>>>
>>> class Admin(User):
...     pass
>>>
>>> class Guest(User):
...     pass
>>>
>>>
>>> data = '{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> Admin.from_json(data)
Admin(firstname='Mark', lastname='Watney')
>>>
>>> Guest.from_json(data)
Guest(firstname='Mark', lastname='Watney')
>>>
>>> User.from_json(data)
User(firstname='Mark', lastname='Watney')

3.7.9. Assignments

Code 3.62. Solution
"""
* Assignment: OOP MethodClassmethod 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` object 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 obiekt typu `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



# Method `convert()` returns instance of a class, which was given
# as an argument with field set `when: datetime`
class Timezone:
    tzname: str
    when: datetime


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


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


Code 3.63. Solution
"""
* Assignment: OOP MethodClassmethod CSV
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

English:
    1. To class `CSVMixin` add methods:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> 'User' | 'Admin'`
    2. `CSVMixin.to_csv()` should return attribute values separated with coma
    3. `CSVMixin.from_csv()` should return instance of a class on which it was called
    4. Use `@classmethod` decorator in proper place
    5. Run doctests - all must succeed

Polish:
    1. Do klasy `CSVMixin` dodaj metody:
        a. `to_csv(self) -> str`
        b. `from_csv(self, text: str) -> 'User' | 'Admin'`
    2. `CSVMixin.to_csv()` powinna zwracać wartości atrybutów klasy rozdzielone po przecinku
    3. `CSVMixin.from_csv()` powinna zwracać instancje klasy na której została wywołana
    4. Użyj dekoratora `@classmethod` w odpowiednim miejscu
    5. Uruchom doctesty - wszystkie muszą się powieść

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:
    >>> import sys; sys.tracebacklimit = 0
    >>> from os import remove
    >>> from dataclasses import dataclass

    >>> @dataclass
    ... class User(CSVMixin):
    ...     firstname: str
    ...     lastname: str
    ...
    >>> @dataclass
    ... class Admin(CSVMixin):
    ...     firstname: str
    ...     lastname: str

    >>> mark = User('Mark', 'Watney')
    >>> melissa = Admin('Melissa', 'Lewis')

    >>> mark.to_csv()
    'Mark,Watney\\n'
    >>> melissa.to_csv()
    'Melissa,Lewis\\n'

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

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [User(firstname='Mark', lastname='Watney'),
     Admin(firstname='Melissa', lastname='Lewis')]

    >>> remove('_temporary.txt')

TODO: dodać test sprawdzający czy linia kończy się newline
"""

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

    def from_csv(cls, line: str):
        ...