All about classes

By on

🐍 Python: All about classes

SPbPU, Viktor Tiulpin, 2021


In the previous episodes

  • basic stuff, syntax
  • functions (wrote our cool min function) + functional stuff
  • scopes, PEP-8
  • strings, bytes
  • collections
  • classes basics
  • decorators, functools
  • exceptions, context managers
  • iterators, generators, itertools

πŸ› Classes again


πŸ‘‡ Class

>>> class Counter:
        """I count. That is all."""
        def __init__(self, initial=0): # not a constructor! initializer!
            self.value = initial

        def increment(self):
            self.value += 1

        def get(self):
            return self.value 
>>> c = Counter(42)
>>> c.increment()
>>> c.get()
43

πŸ› Classes basics wrap-up

  • all attributes are stored in dictionaries
  • properties are functions that can be called like attributes
  • do not overcomplexify the inheritance
  • https://docs.python.org/3/reference/datamodel.html#special-method-names
  • decorators can be used too, but later about it

Class decorator

from time import sleep, time

class Timer:
    def __init__(self, func):
        self.function = func
  
    def __call__(self, *args, **kwargs):
        start = time()
        result = self.function(*args, **kwargs)
        end = time()
        print(f"Execution took {end - start}")
        return result

>>> @Timer
    def some_function(delay):
        sleep(delay)
  
>>> some_function(3)
Execution took 3.003122091293335 seconds

Class and Object Attributes reminder

>>> class Fruit:
...     color = 'red'
...
>>> blueberry = Fruit()
>>> Fruit.color
'red'
>>> blueberry.color
'red'

Inheritance reminder

>>> class Car():
...     pass
...
>>> class Yugo(Car):
...     pass
...
>>> issubclass(Yugo, Car)
True
>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     pass

Method override (inheritance reminder)

>>> give_me_a_car = Car()
>>> give_me_a_yugo = Yugo()
>>> give_me_a_car.exclaim()
I'm a Car!
>>> give_me_a_yugo.exclaim()
I'm a Car!

That’s it

>>> class Car():
...     def exclaim(self):
...         print("I'm a Car!")
...
>>> class Yugo(Car):
...     def exclaim(self):
...         print("I'm a Yugo! Much like a Car.")
...
>>> give_me_a_yugo.exclaim()
I'm a Yugo! Much like a Car.

base stuff class inheritance

class LoggingDict(dict):  # ???
    ...

The right way:

class LoggingDict(UserDict):
    def get(self, key, default=None):
        if key not in self.data:
            logging.info(f"{key} not found!")

        return super().get(key, default)

property reminder

>>> class BigDataModel:
        _params = []

        @property
        def params(self):
            return self._params

        @params.setter
        def params(self, new_params):
            assert all(map(lambda p: p > 0, new_params))
            self._params = new_params

        @params.deleter
        def params(self):
            del self._params

Class methods

  • instance
  • class
  • static

Class methods

>>> class A():
...     count = 0
...     def __init__(self):
...         A.count += 1
...     def exclaim(self):
...         print("I'm an A!")
...     @classmethod
...     def kids(cls):
...         print("A has", cls.count, "little objects.")
...
>>> easy_a = A()
>>> breezy_a = A()
>>> wheezy_a = A()
>>> A.kids()
A has 3 little objects.

Static methods

>>> class CoyoteWeapon():
...     @staticmethod
...     def commercial():
...         print('This CoyoteWeapon has been brought to you by Acme')
...
>>>
>>> CoyoteWeapon.commercial()
This CoyoteWeapon has been brought to you by Acme

How? The answer is descriptors

>>> class Descr:
...     def __get__(self, instance, owner):
...         print(instance, owner)
...     def __set__(self, instance, value):
...         print(instance, value)
...     def __delete__(self, instance):
...         print(instance)
...
>>> class A:
...     attr = Descr()

>>> A.attr
None <class '__main__.A>
>>> A().attr
<__main__.A object at [...]> <class '__main__.A>
>>> instance = A()
>>> instance.attr = 42
<__main__.A object at [...]> 42

Primer

>>> class staticmethod:
...     def __init__(self, method):
...         self._method = method
...
...     def __get__(self, instance, owner):
...         return self._method
...

πŸͺ„ Magic methods


πŸ€” equals?

>>> class Word():
...    def __init__(self, text):
...        self.text = text
...
...    def equals(self, word2):
...        return self.text.lower() == word2.text.lower()
...
>>> first = Word('ha')
>>> second = Word('HA')
>>> third = Word('eh')
>>> first.equals(second)
True
>>> first.equals(third)
False

🧐 __eq__

>>> class Word():
...     def __init__(self, text):
...         self.text = text
...     def __eq__(self, word2):
...         return self.text.lower() == word2.text.lower()
...

https://docs.python.org/3/reference/datamodel.html#special-method-names


🐍 NamedTuple


namedtuple reminder

>>> from collections import namedtuple
>>> Duck = namedtuple('Duck', 'bill tail')
>>> duck = Duck('wide orange', 'long')
>>> duck
Duck(bill='wide orange', tail='long')
>>> duck.bill
'wide orange'
>>> duck.tail
'long'

and from dictionary

>>> parts = {'bill': 'wide orange', 'tail': 'long'}
>>> duck2 = Duck(**parts)
>>> duck2
Duck(bill='wide orange', tail='long')

Why?

>>> duck_dict['color'] = 'green'
>>> duck_dict
{'color': 'green', 'tail': 'long', 'bill': 'wide orange'}
>>> duck.color = 'green'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Duck' object has no attribute 'color'

namedtuple wrap-up

  • It looks and acts like an immutable object.
  • It is more space and time efficient than objects.
  • You can access attributes by using dot notation instead of dictionary-style square brackets.
  • You can use it as a dictionary key.

πŸ”₯ dataclasses


Typical boilerplate:

>> class TeenyClass():
...     def __init__(self, name):
...         self.name = name
...
>>> teeny = TeenyClass('itsy')
>>> teeny.name
'itsy'

dataclass helps:

>>> from dataclasses import dataclass
>>> @dataclass
... class TeenyDataClass:
...     name: str  # types are must-have
...
>>> teeny = TeenyDataClass('bitsy')
>>> teeny.name
'bitsy'

πŸ‘€ typing!

def concat(a, b):
    ...

def concat(a: int, b: int) -> str:
    return str(a) + str(b)


from typing import List, Tuple

def process(data: List[Tuple[int, str]]) -> None:
    do_stuff(data)

More dataclasses…

>>> from dataclasses import dataclass
>>> @dataclass
... class AnimalClass:
...     name: str
...     habitat: str
...     teeth: int = 0
...
>>> snowman = AnimalClass('yeti', 'Himalayas', 46)
>>> duck = AnimalClass(habitat='lake', name='duck')
>>> snowman
AnimalClass(name='yeti', habitat='Himalayas', teeth=46)
>>> duck
AnimalClass(name='duck', habitat='lake', teeth=0)

dataclass deeper: fields

>>> @dataclass
... class Book:
...     author: str = field()
...     title: str = field()
...     isbn: int = field(compare=False)
...     renters: List[str] = field(default_factory=list, metadata={"max": 5}, repr=False)
...     
...     def rent(self, name):
...         if len(self.renters) >= 5:
...             raise ValueError("5 People Already Rent This Book")
...         self.renters.append(name)
...     
...     def unrent(self, name):
...         self.renters.remove(name)

more: https://www.python.org/dev/peps/pep-0557/


🍰 Metaclasses


>>> class Foo:
...     pass
...
>>> x = Foo()
>>> type(x)
<class '__main__.Foo'>
>>> type(Foo)
<class 'type'>
>>> type(type)
<class 'type'>

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
>>> class Foo(metaclass=Meta):
...     pass
>>> Foo.attr
100

more: https://realpython.com/python-metaclasses/


🏁 Any questions?