Klasy i obiekty - elementy zaawansowane#

Python jako język obiektowy - podstawowe informacje#

Wszystko jest obiektem#

Python jest językiem w pełni obiektowym - wszystko jest obiektem, także wartości typów prymitywnych i funkcje.

(5).__add__(3)
8

Dynamiczne typowanie#

Python jest językiem z dynamicznym typowaniem. Zmienne przechowują referencję do obiektu, a dopiero obiekty posiadają typ. Dlatego raz zdefiniowana zmienna może “zmienić typ”.

var = 'text'
var
'text'
var = 1
var
1

Dynamiczne typowanie znacznie ułatwia programowanie, ponieważ nie narzuca ograniczeń na typ zmiennych.

Dzięki temu programista może skupić się na bardziej istotnych aspektach, takich jak poprawność kodu, albo po prostu napisać kod szybciej.

Dzięki dynamicznemu typowaniu dostępna jest funkcja eval, która pozwala na wykonanie dowolnego, dynamicznie utworzonego wyrażenia:

x = 1
eval('x+1')
2

Dynamiczne typowanie umożliwia także łatwe użycie technik metaprogramowania, takich jak metaklasy, które zostaną omówione później. Pozwala to np. na dynamiczne tworzenie nowych typów. Dzięki temu implementacje ORM (Object-Relational Mapping, mapowanie obiektowo-relacyjne) są bardziej naturalne.

Tożsamość, typ a wartość#

Każdy obiekt posiada:

  • tożsamość (identity) - wskazuje na lokalizację obiektu w pamięci i można ją sprawdzić wywołując wbudowaną funkcję id;

  • typ (type) opisuje reprezentację obiektu dla Pythona;

  • wartość (value) to dane przechowywane w obiekcie.

numbers = [1, 2, 3]
id(numbers)
139999096994176
type(numbers)
list
numbers
[1, 2, 3]

Definiowanie klasy#

Klasę definiujemy za pomocą słowa kluczowego class:

class MyClass:
    def method(self):
        pass

W Pythonie wszystko jest obiektem, także klasa, dlatego możemy mówić o obiekcie klasy. Taki obiekt również ma swój typ:

MyClass
__main__.MyClass
type(MyClass)
type

Metody specjalne#

Metodami specjalnymi nazywane są metody zaczynające i kończące się dwoma podkreślnikami. Implementują one operacje, które wywołujemy przy użyciu specjalnej składni (np. porównanie dwóch obiektów a < b, dostęp do atrybutów obj.attribute lub składnia obj[key]).

Najważniejsze metody specjalne#

Kategoria

Nazwy metod

String/bytes representation

__repr__ __str__ __format__ __bytes__ __fspath__

Konwersja do liczby

__bool__ __complex__ __int__ __float__ __hash__ __index__

Emulacja kolekcji

__len__ __getitem__ __setitem__ __delitem__ __contains__

Iteracja

__iter__ __aiter__ __next__ __anext__ __reversed__

Funkcje lub korutyny

__call__ __await__

Menadżer kontekstu

__enter__ __exit__ __aexit__ __aenter__

Tworzenie i niszczenie instancji

__new__ __init__ __del__

Zarządzanie atrybutami

__getattr__ __getattribute__ __setattr__ __delattr__ __dir__

Deskryptory

__get__ __set__ __delete__ __set_name__

Abstract base classes

__instancecheck__ __subclasscheck__

Metaprogramowanie

__prepare__ __init_subclass__ __class_getitem__ __mro_entries__

Metody operatorowe#

Kategoria operatorów

Symbole

Nazwy metod

Jednoargumentowe

- + abs()

__neg__ __pos__ __abs__

Porównania

< <= == != > >=

__lt__ __le__ __eq__ __ne__ __gt__ __ge__

Arytmetyczne

+ - * / // % @ divmod() round() ** pow()

__add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __matmul__ __divmod__ __round__ __pow__

Arytmetyczne z przypisaniem

+= -= *= /= //= %= @= **=

__iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __imatmul__ __ipow__

Bitowe

& | ^ << >> ~

__and__ __or__ __xor__ __lshift__ __rshift__ __invert__

Bitowe z przypisaniem

&= |= ^= <<= >>=

__iand__ __ior__ __ixor__ __ilshift__ __irshift__

Dostęp do atrybutów#

Dostęp do atrybutów kontrolują poniższe metody specjalne:

Metoda specjalna

Opis

__getattr__(self, name)

Wywoływana, gdy obiekt nie ma atrybutu name

__setattr__(self, name, value)

Wywoływana podczas przypisywania atrybutów

__delattr__(self, name)

Wywoływana przy usuwaniu atrybutu (del obj.attr)

Przykład słownika wykorzystującego składnię dict.key zamiast dict[key]:

class Record:
    def __init__(self):
        # Nie możemy użyć poniższego kodu:
        #     self._d = {}
        # ponieważ zakończyłby się on rekurencyjnym wywoływaniem metody __setattr__
        super().__setattr__('_dict', {})

    def __getattr__(self, name):
        print('getting', name)
        return self._dict[name]
    
    def __setattr__(self, name, value):
        print('setting', name, 'to', value)
        self._dict[name] = value
        
    def __delattr__(self, name):
        print('deleting', name)
        del self._dict[name]
person = Record()
person.first_name = "John"
person.first_name
setting first_name to John
getting first_name
'John'
del person.first_name
deleting first_name

Oprócz wspomnianych metod

Metoda specjalna

Opis

__getattribute__(self, name)

Wywoływana bezwarunkowo przy dostępie do atrybutów klasy, nawet jeśli dany atrybut istnieje.

class Person:
    def __init__(self, first_name):
        self.first_name = first_name
    
    def __getattribute__(self, name):
        print('getattribute', name)
        return object.__getattribute__(self, name)
p = Person('John')
p.first_name
getattribute first_name
'John'

Przykład ilustrujący różnicę między __getattr__ a __getattribute__:

class Foo:
    def __init__(self):
        self.a = "a"

    def __getattr__(self,attribute):
        return f"You asked for {attribute}, but I'm giving you default"


class Bar:
    def __init__(self):
        self.a = "a"

    def __getattribute__(self,attribute):
        return f"You asked for {attribute}, but I'm giving you default"
foo = Foo()
foo.a
'a'
foo.b
"You asked for b, but I'm giving you default"
getattr(foo, "a")
'a'
getattr(foo, "b")
"You asked for b, but I'm giving you default"
bar = Bar()
bar.a
"You asked for a, but I'm giving you default"
bar.b
"You asked for b, but I'm giving you default"
getattr(bar, "a")
"You asked for a, but I'm giving you default"
getattr(bar, "b")
"You asked for b, but I'm giving you default"

Składowe chronione i prywatne#

Składowe chronione#

Składowe, które powinny być modyfikowane tylko przez klasę, powinny zaczynać się od podkreślnika. Jest to powszechnie przyjęta konwencja oznaczająca, że dana składowa nie powinna być modyfikowana z zewnątrz. Jest to jednak tylko konwencja - Python nie posiada mechanizmu ukrywającego takie składowe. Wciąż można je modyfikować spoza klasy.

class BankAccount:
    def __init__(self, initial_balance):
        self._balance = initial_balance
    
    @property
    def balance(self):
        return self._balance
account = BankAccount(100.0)
print(account.balance)
print(account._balance) # # składowe chronione są wciąż dostępne z zewnątrz klas
100.0
100.0

Składowe prywatne#

Aby ukryć atrybut lub metodę przed dostępem spoza klasy (składowa private), należy jej nazwę poprzedzić dwoma podkreślnikami (np. __atrybut). Taka składowa jest dostępna tylko wewnątrz tej klasy. Składowe zaczynające się od dwóch podkreślników (nie będące metodami specjalnymi) są traktowane w szczególny sposób - ich nazwa zostaje zmieniona na _NazwaKlasy__atrybut. Do tej składowej można się wciąż odwołać z zewnątrz klasy, ale tylko używając zmienionej nazwy.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance
    
    def withdraw(self, amount):
        self.__balance -= amount
    
    def deposit(self, amount):
        self.__balance += amount
    
    def info(self):
        print("owner:", self.owner, "; balance:", self.__balance)
jk = BankAccount("Jan Kowalski", 1000)
print(jk.__balance) # błąd!
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[29], line 2
      1 jk = BankAccount("Jan Kowalski", 1000)
----> 2 print(jk.__balance) # błąd!

AttributeError: 'BankAccount' object has no attribute '__balance'
print(jk._BankAccount__balance) # ok
1000

Atrybuty klasy i metody statyczne#

Składowe statyczne są wspólne dla wszystkich instancji klasy.

Z kolei metoda statyczna to po prostu funkcja w przestrzeni nazw klasy. Taką funkcję należy udekorować @staticmethod. Taka funkcja nie przyjmuje instancji klasy self.

class CountedObject(object):
    count = 0   # statyczna składowa
    
    def __init__(self):
        CountedObject.count += 1
    
    @staticmethod  # statyczna metoda
    def get_count():
        return CountedObject.count
CountedObject.get_count()
0
c1 = CountedObject()
c2 = CountedObject()
cs = [CountedObject(), CountedObject()]

CountedObject.get_count()
4

Czasami atrybutów klasy używa się, aby zainicjalizować domyślną wartość dla pewnych atrybutów instancji. Należy jednak być ostrożnym:

class PersonWithDefaultAttributes:
    first_name = 'John'
    last_name = 'Smith'
    phones = []

p1 = PersonWithDefaultAttributes()
p2 = PersonWithDefaultAttributes()

print(p1.first_name)
print(p2.first_name)
John
John
p1.first_name = 'Bob'
print(p1.first_name)
print(p2.first_name)
Bob
John
PersonWithDefaultAttributes.last_name = 'Williams'
print(p1.last_name)
print(p2.last_name)
Williams
Williams
p1.phones.append('+48123456789')
print(p1.phones)
print(p2.phones)
['+48123456789']
['+48123456789']

Metody klasy#

Zwykła metoda ma dostęp do instancji klasy (poprzez parametr self). Z kolei metoda klasy ma dostęp do klasy, z której została wywołana, lub do klasy instacji, z której została wywołana.

Metody klasy są przydatne, jeżeli chcemy pozwolić na więcej niż jeden sposób tworzenia instancji.

class Date:
    def __init__(self, day, month, year):
        self.day = day
        self.month = month
        self.year = year
    
    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = date_as_string.split('-')
        return cls(int(day), int(month), int(year)) # utworzenie instancji klasy cls
d1 = Date(20, 1, 2016)
d2 = Date.from_string('20-01-2016')

Warto zwrócić uwagę na to, że wewnątrz metody from_string tworzona jest nowa instancja klasy cls. Nie musi być to klasa Date. Tak będzie w przypadku klas dziedziczących po Date.

Deskryptory#

Deskryptorem jest atrybut (obiekt), który zawiera przynajmniej jedną z trzech metod specjalnych tzw. “protokołu deskryptora”.

Metody specjalne deskryptora

Opis

__get__(self, instance, owner)

Wywoływana do pobrania atrybutu z obiektu lub klasy “właściciela”

__set__(self, instance, value)

Wywoływana do ustawienia wartości atrybutu

__delete__(self, instance)

Wywoływana w czasie usuwania atrybutu

Taki obiekt musi pojawić się jako atrybut w obiekcie - “właścicielu”. W momencie dostępu do takiego atrybutu wywoływane są odpowiednie metody deskryptora.

Rodzaje deskryptorów#

  • non-data descriptor - posiada tylko metodę __get__(). Przy dostępie do atrybutu podjęta może zostać akcja zaimplementowana w metodzie __get__()

import os

class DirectorySize:
    def __get__(self, instance, owner_class):
        return len(os.listdir(instance.directory_name))
    
class Directory:
    size = DirectorySize() # descriptor instance

    def __init__(self, directory_name):
        self.directory_name = directory_name # regular instance attribute
local_dir = Directory('.')
local_dir.size
28
  • data descriptor - posiada zarówno metodę __get__() jak i __set__(). Może też zawierać definicję metody __delete__(). Umożliwia tworzenie obiektu mutable, do którego delegowane są próby dostępu do atrybutu i ustawiania jego wartości.

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAgeAccess:

    def __get__(self, instance, owner_class=None):
        value = instance._age
        logging.info('Accessing %r.%r giving %r', instance, 'age', value)
        return value

    def __set__(self, instance, value):
        logging.info('Updating %r.%r to %r', instance, 'age', value)
        instance._age = value


class Person:
    age = LoggedAgeAccess()             # Descriptor instance

    def __init__(self, name, age):
        self.name = name                # Regular instance attribute
        self.age = age                  # Calls __set__()

    def birthday(self):
        self.age += 1                   # Calls both __get__() and __set__()
john = Person("John", 31)
mary = Person("Mary", 27)
john.birthday()
mary.age = 30
INFO:root:Updating <__main__.Person object at 0x000001BE9E42C490>.'age' to 31
INFO:root:Updating <__main__.Person object at 0x000001BE9E9E2450>.'age' to 27
INFO:root:Accessing <__main__.Person object at 0x000001BE9E42C490>.'age' giving 31
INFO:root:Updating <__main__.Person object at 0x000001BE9E42C490>.'age' to 32
INFO:root:Updating <__main__.Person object at 0x000001BE9E9E2450>.'age' to 30

Nazwy atrybutów i deskryptory#

Jeśli klasa używa wielu deskryptorów, to często powstaje potrzeba związania obiektu deskryptora z nazwą atrybutu, nad którym deskryptor przejmuje kontrolę. Wykorzystuje się do tego metodę __set_name__() w klasie deskryptora:

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAccess:

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name
        logging.info('Setting names: %r and %r', self.public_name, self.private_name)

    def __get__(self, instance, owner_class=None):
        value = getattr(instance, self.private_name)
        logging.info('Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, instance, value):
        logging.info('Updating %r to %r', self.public_name, value)
        setattr(instance, self.private_name, value)

class Person:
    name = LoggedAccess()                # First descriptor instance
    age = LoggedAccess()                 # Second descriptor instance

    def __init__(self, name, age):
        self.name = name                 # Calls the first descriptor
        self.age = age                   # Calls the second descriptor

    def birthday(self):
        self.age += 1
INFO:root:Setting names: 'name' and '_name'
INFO:root:Setting names: 'age' and '_age'
vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
james = Person("James", 43)
INFO:root:Updating 'name' to 'James'
INFO:root:Updating 'age' to 43
james.birthday()
INFO:root:Accessing 'age' giving 43
INFO:root:Updating 'age' to 44

Kolejność wyszukiwania nazw przy dostępie do atrybutów#

Wywołanie z instancji#

Przy próbie dostępu do atrybutu x dla obiektu o (o.x):

  • data descriptor: wartość zwrócona z metody __get__() deskryptora pola

  • składowa instancji: wartość o. __dict__[x]

  • non-data descriptor: wartość zwrócona z metody __get__() deskryptora pola

  • składowa klasy: type(o).__dict__['x']

  • składowe klas bazowych: wyszukiwanie w kolejności MRO,

  • __getattr__(), jeśli ta metoda została zdefiniowana w klasie

Wywołanie z klasy#

Przy próbie dostępu do atrybutu x dla klasy C (C.x):

  • data descriptor: wartość zwrócona z metody __get__() deskryptora pola klasy

  • składowa klasy lub klas bazowych: poszukiwanie w C.__dict__['x'] lub klasach bazowych

  • non-data descriptor: wartość zwrócona z metody __get__() deskryptora pola

Właściwości (properties)#

Przykładem deskryptora jest wbudowany dekorator @property pozwalający na enkapsulację obiektu, to znaczy kontrolę dostępu do atrybutów przy użyciu metod dostępowych.

class BankAccount:
    def __init__(self, daily_limit):
        self.__daily_limit = daily_limit
    
    @property
    def daily_limit(self):
        print('getting daily_limit')
        return self.__daily_limit
    
    @daily_limit.setter
    def daily_limit(self, value):
        if value < 0:
            raise ValueError('Value must be >= 0')
        self.__daily_limit = value
account = BankAccount(100.0)
account.daily_limit
getting daily_limit
100.0
account.daily_limit = 200.0
account.daily_limit
getting daily_limit
200.0
account.daily_limit = -100.0
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-73-764421fb5f17> in <module>
----> 1 account.daily_limit = -100.0

<ipython-input-70-a7a363ba8f92> in daily_limit(self, value)
     11     def daily_limit(self, value):
     12         if value < 0:
---> 13             raise ValueError('Value must be >= 0')
     14         self.__daily_limit = value

ValueError: Value must be >= 0

Gdyby nie został zdefiniowany setter, to znaczy w kodzie nie pojawiłby się @daily_limit.setter, wówczas daily_limit byłaby właściwością tylko do odczytu. Próby zmiany jej wartości kończyłyby się błędem.

Sloty#

Każdy obiekt Pythona posiada __dict__ - słownik atrybutów typu dict. W rezultacie dla każdego obiektu mamy dość spory narzut związany ze zużyciem pamięci i czasem dostępu do składowych słownika.

Jeśli nasza klasa nie zamierza korzystać z dynamicznej natury takiego słownika, to można w klasie zdefiniować atrybut __slots__ i podać listę wszystkich składowych. Atrybuty wymienione na liście są przechowane w tablicy referencji, która zużywa znacząco mniej pamięci.

class Pixel:
    __slots__ = ('x', 'y')
p = Pixel()

Utworzona instancja typu Pixel nie posiada słownika __dict__:

p.__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-66-33b2432b7e42> in <module>
----> 1 p.__dict__

AttributeError: 'Pixel' object has no attribute '__dict__'

Możemy normalnie korzystać z atrybutów x i y:

p.x = 10
p.y = 20

Próba ustawienia nowego atrybutu (nie wymienionego na liście slotów) skończy się wyjątkiem:

p.z = 30
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-68-cadbcb940683> in <module>
----> 1 p.z = 30

AttributeError: 'Pixel' object has no attribute 'z'

Dziedziczenie#

Podstawy#

Dziedziczenie definiowane jest za pomocą składni:

class Base:
    def base_method(self):
        pass

class Derived(Base):
    def derived_method(self):
        pass

Method Resolution Order (MRO)#

Wszystkie metody zdefiniowane bezpośrednio w klasie C są przechowywane w słowniku C.__dict__:

Base.__dict__
mappingproxy({'__module__': '__main__',
              'base_method': <function __main__.Base.base_method(self)>,
              '__dict__': <attribute '__dict__' of 'Base' objects>,
              '__weakref__': <attribute '__weakref__' of 'Base' objects>,
              '__doc__': None})
Derived.__dict__
mappingproxy({'__module__': '__main__',
              'derived_method': <function __main__.Derived.derived_method(self)>,
              '__doc__': None})
d = Derived()
d.base_method() # it works

W klasie potomnej Derived dostępne są metody zdefiniowane w klasie bazowej Base (takie jak base_method), mimo że nie występują one bezpośrednio w słowniku potomka. W momencie wywoływania metody d.base_method(), metoda base_method jest poszukiwana w słowniku klasy Derived. Ponieważ ten słownik nie ma tej metody, przeszukiwany jest słownik klasy Base.

W ogólnym przypadku, przeszukiwane są słowniki wszystkich klas określonych w C.__mro__. Ta krotka zawiera klasę C, jej klasy nadrzędne, itd.

Derived.__mro__
(__main__.Derived, __main__.Base, object)

Dziedziczenie wielobazowe#

W Pythonie możliwe jest dziedziczenie po więcej niż jednej klasie.

Przeciążając metody (w szczególności konstruktor __init__) należy pamiętać, aby wywołać także wersję rodzica przy użyciu funkcji super, która zwraca obiekt proxy służący do wywoływania metod rodzica. Obiekt jest wybierany zgodnie z MRO.

class A:
    def __init__(self):
        print("A")

class B(A):
    def __init__(self):
        super().__init__()
        print("B")

class C(A):
    def __init__(self):
        super().__init__()
        print("C")

class D(B, C):
    def __init__(self):
        super().__init__()
        print("D")
D.__mro__
(__main__.D, __main__.B, __main__.C, __main__.A, object)
D()
A
C
B
D
<__main__.D at 0x7fc85b329ee0>

Czasami nie jest możliwe utworzenie sensownego MRO:

class A: pass
class B: pass
class C(A, B): pass
class D(B, A): pass
class E(C, D): pass
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-86-b8bb9ff5be5c> in <module>
      3 class C(A, B): pass
      4 class D(B, A): pass
----> 5 class E(C, D): pass

TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

Mixins (klasy domieszkowe)#

Klasy domieszkowe (mixins) to klasy, które dostarczają określoną funkcjonalność innym klasom (poprzez mechanizm wielokrotnego dziedziczenia). Nie są samodzielnymi klasami i, w związku z tym, nie tworzy się instancji klas domieszkowych. Zazwyczaj nazwa takiej klasy kończy się sufixem Mixin (np. ComparableMixin), ale nie jest to bezwględnie obowiązująca konwencja.

Definiując klasę i wymieniając jej rodziców należy pamiętać, aby najpierw wymienić wszystkie klasy domieszkowe, a dopiero na końcu podać klasę bazową (chyba że jest nią object).

W przypadku porównywania obiektów wywoływana jest jedna z sześciu specjalnych metod (__lt__, __le__, __eq__, __ne__, __gt__ lub __ge__). Wystarczy jednak zdefiniować dwie z nich (np. __le__ i __eq__), a pozostałe porównania to odpowiednia kombinacja tych dwóch metod.

Poniżej przedstawiono klasę domieszkową ComparableMixin. Jej użycie powoduje, że wystarczy zdefiniować metody __le__ i __eq__, aby obiekty klasy dziedziczącej po ComparableMixin mogły być porównywane.

class ComparableMixin:
    def __ne__(self, other):
        return not (self == other)
    def __le__(self, other):
        return self < other or (self == other)
    def __gt__(self, other):
        return not self <= other
    def __ge__(self, other):
        return self > other or self == other

class MyInteger(ComparableMixin):  # klasą bazową jest "object"
    def __init__(self, i):
        self.i = i
    def __lt__(self, other):
        return self.i < other.i
    def __eq__(self, other):
        return self.i == other.i
MyInteger(1) > MyInteger(0)
True

Dziedziczenie po typach wbudowanych#

Od Pythona 2.2 można dziedziczyć po wszystkich typach wbudowanych.

class CountDict(dict):
    def __getitem__(self, key):
        if key in self:
            return super(CountDict, self).__getitem__(key)
        else:
            return 0
cd = CountDict()
cd['unknown-key']
0

Dziedziczenie po typach niezmiennych#

Dla typów niezmiennych (immutable) nie działa przeciążanie konstruktora __init__. Po utworzeniu obiektu jest już za późno na jakąkolwiek modyfikację.

class PositiveInt(int):
    def __new__(cls, value):
        print('__new__')
        obj = int.__new__(cls, value)
        return obj if obj > 0 else -obj
PositiveInt(-7)
__new__
7

Przykład ilustrujący gdzie przekazywane są argumenty:

class Test:
    def __new__(cls, *args):
        print('__new__', args)
        obj = object.__new__(cls)
        obj.new_attr = "test"
        return obj
    
    def __init__(self, *args):
        print('__init__', args)
        self.args = args
t = Test("gadget", 42)
__new__ ('gadget', 42)
__init__ ('gadget', 42)
t.args
('gadget', 42)
t.new_attr
'test'

Abstract Base Classes#

W Pythonie stosowany jest duck-typing, w związku z tym nie ma potrzeby definiowania abstrakcyjnych klas lub interfejsów określających kontrakt. Warto jednak czasami jawnie określić kontrakt, to znaczy stworzyć abstrakcyjną klasę bazową i określić, jakie metody powinny zostać zdefiniowane. Nie tworzy się instancji takiej klasy - służy ona jedynie jako dokumentacja.

import abc

class BaseCalculator(abc.ABC):
    @abc.abstractmethod
    def process(self, expr):
        pass

class Calculator(BaseCalculator):
    def process(self, expr):
        return eval(expr)
c = Calculator()
c.process('2 + 2')
4

Jeżeli w klasie potomnej nie zostanie zdefiniowana wymagana metoda, zostanie rzucony wyjątek.

class InvalidCalculator(BaseCalculator):
    pass
ic = InvalidCalculator()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-101-4973d17f9fb6> in <module>
----> 1 ic = InvalidCalculator()

TypeError: Can't instantiate abstract class InvalidCalculator with abstract methods process

Klasa jako obiekt#

W Pythonie wszystko jest obiektem, także klasa. Dlatego możemy mówić o obiekcie klasy. obiekt klasy, tak samo jak każdy obiekt, może być modyfikowany po jego utworzeniu.

class Record:
    pass

Record.name = "John"
person = Record()
person.name
'John'
Record.age = 42
person.age
42

Ma to wpływ na wszystkie instancje.

Class Builders (@dataclass)#

Moduł dataclasses dostarcza dekorator, który umożliwia automatyczne generowanie wybranych metod specjalnych, takich jak __init__(), __repr__() lub __eq__().

Składowe obiektu, które są wykorzystywane w implementacjach metod, są definiowane na podstawie pól klasy wraz adnotacjami typu.

Na przykład, dla poniższej klasy:

from dataclasses import dataclass

@dataclass
class LineItem:
    name: str
    unit_price: float
    quantity: int = 0

    def total(self) -> float:
        return self.unit_price * self.quantity

wygenerowana zostanie funkcja __init__(), która wygląda następująco:

def __init__(self, name: str, unit_price: float, quantity: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity = quantity

Stosując dekorator @dataclass możemy dużo szybciej tworzyć proste klasy i rozbudowywać je dodając do nich potrzebne metody.

line_1 = LineItem("ipad", 7665.0, 1)
repr(line_1)
"LineItem(name='ipad', unit_price=7665.0, quantity=1)"
line_1.total()
7665.0

Parametry dekoratora @dataclass#

Do dekoratora @dataclass możemy przekazać następujące paramtetry:

Parametr

Wartość domyślna

Efekt (jeśli True)

init

True

definicja metody __init__

repr

True

definicja metody __repr__ (jeśli klasa nie definiuje własnej implementacji)

eq

True

definicja metody __eq__ (instancje są porównywane jak krotki)

order

False

definicja metod __lt__(), __le__(), __gt__(), and __ge__()

unsafe_hash

False

definicja metody __hash__() (ta opcja implikuje, że instancje powinny być immutable)

frozen

False

próba przypisania wartości dla pola generuje wyjątek (instancje są immutable)

match_args

True

tworzona jest krotka __match_args__ z listy parametrów metody __init__()

slots

True

generowany jest atrybut __slots__ i nowa klasa jest zwrócona w miejsce klasy ogryginalnej

@dataclass(order=True, unsafe_hash=True, frozen=True)
class Person:
    name: str
    age: int
people = [Person("John", 33), Person("Eve", 44), Person("Adam", 33)]
sorted(people)
[Person(name='Adam', age=33),
 Person(name='Eve', age=44),
 Person(name='John', age=33)]
hash(people[0])
-5191814165429172402
people[0].name = "Unknown"
---------------------------------------------------------------------------
FrozenInstanceError                       Traceback (most recent call last)
<ipython-input-21-2c1af0d06026> in <module>
----> 1 people[0].name = "Unknown"

<string> in __setattr__(self, name, value)

FrozenInstanceError: cannot assign to field 'name'

Implementacja składowych#

Funkcja field() precyzuje sposób, w jaki dane pole jest implementowane.

from typing import List
from dataclasses import field


@dataclass(order=True)
class Person:
    name: str
    age: int = field(compare=False)
    friends: List['Person'] = field(default_factory=list)
p1 = Person("John", 44)
p2 = Person("John", 23)
p1 == p2
True
p1.friends.append(p2)
p1
Person(name='John', age=44, friends=[Person(name='John', age=23, friends=[])])

Opcje funkcji field():

  • default - domyślna wartość dla pola

  • default_factory - funkcja bezargumentowa, która tworzy domyślną instancję przypisywaną do pola

  • init - jeśli True, pole jest użyte jako parametr metody __init__()

  • repr - jeśli True, pole jest użyte w implementacji metody __repr__()

  • hash - jeśli True, pole jest użyte w implementacji metody __hash__()

  • compare - jeśli True, pole jest użyte w implementacji metod __eq__() i innych

Przydatne metody dataclass’y#

  • fields() - zwraca krotkę pól klasy

import dataclasses

dataclasses.fields(Person)
(Field(name='name',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x7f8fbd270a60>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f8fbd270a60>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD),
 Field(name='age',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x7f8fbd270a60>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f8fbd270a60>,init=True,repr=True,hash=None,compare=False,metadata=mappingproxy({}),_field_type=_FIELD),
 Field(name='friends',type=typing.List[ForwardRef('Person')],default=<dataclasses._MISSING_TYPE object at 0x7f8fbd270a60>,default_factory=<class 'list'>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD))
  • asdict() - konweruje instancję dataclass’y do słownika

dataclasses.asdict(p1)
{'name': 'John',
 'age': 44,
 'friends': [{'name': 'John', 'age': 23, 'friends': []}]}
  • astuple() - konwertuje instancję do krotki

dataclasses.astuple(p2)
('John', 23, [])
  • replace(obj, **changes) - tworzy nową instancję na podstawie obj ze zmienionymi wartościami pól z **changes

dataclasses.replace(p1, age = 88)
Person(name='John', age=88, friends=[Person(name='John', age=23, friends=[])])

Post-init#

Metoda __post_init__() umożliwia inicjalizację tzw. pól wyliczanych:

@dataclass
class LineItem:
    name: str
    unit_price: float
    quantity: int = 0
    total: float = field(init=False)

    def __post_init__(self):
        self.total = self.unit_price * self.quantity
line_1 = LineItem("ipad", 7999.99, 2)
line_1.total
15999.98

Pola klasy#

from typing import ClassVar


@dataclass
class Entity:
    id: int
    count: ClassVar[int] = 0

    def __new__(cls, *args, **kwargs):
        cls.count += 1
        return super().__new__(cls)
        
entities = [Entity(id) for id in range(1, 11)]
Entity.count
10