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 |
|
Konwersja do liczby |
|
Emulacja kolekcji |
|
Iteracja |
|
Funkcje lub korutyny |
|
Menadżer kontekstu |
|
Tworzenie i niszczenie instancji |
|
Zarządzanie atrybutami |
|
Deskryptory |
|
Abstract base classes |
|
Metaprogramowanie |
|
Metody operatorowe#
Kategoria operatorów |
Symbole |
Nazwy metod |
---|---|---|
Jednoargumentowe |
|
|
Porównania |
|
|
Arytmetyczne |
|
|
Arytmetyczne z przypisaniem |
|
|
Bitowe |
|
|
Bitowe z przypisaniem |
|
|
Dostęp do atrybutów#
Dostęp do atrybutów kontrolują poniższe metody specjalne:
Metoda specjalna |
Opis |
---|---|
|
Wywoływana, gdy obiekt nie ma atrybutu |
|
Wywoływana podczas przypisywania atrybutów |
|
Wywoływana przy usuwaniu atrybutu ( |
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 |
---|---|
|
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 |
---|---|
|
Wywoływana do pobrania atrybutu z obiektu lub klasy “właściciela” |
|
Wywoływana do ustawienia wartości atrybutu |
|
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 polaskładowa instancji: wartość
o. __dict__[x]
non-data descriptor: wartość zwrócona z metody
__get__()
deskryptora polaskł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 klasyskładowa klasy lub klas bazowych: poszukiwanie w
C.__dict__['x']
lub klasach bazowychnon-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 |
---|---|---|
|
|
definicja metody |
|
|
definicja metody |
|
|
definicja metody |
|
|
definicja metod |
|
|
definicja metody |
|
|
próba przypisania wartości dla pola generuje wyjątek (instancje są immutable) |
|
|
tworzona jest krotka |
|
|
generowany jest atrybut |
@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 poladefault_factory
- funkcja bezargumentowa, która tworzy domyślną instancję przypisywaną do polainit
- jeśliTrue
, pole jest użyte jako parametr metody__init__()
repr
- jeśliTrue
, pole jest użyte w implementacji metody__repr__()
hash
- jeśliTrue
, pole jest użyte w implementacji metody__hash__()
compare
- jeśliTrue
, 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 podstawieobj
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