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 prymitywne 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)
140268810295360
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¶
Metoda specjalna |
Opis |
---|---|
|
Stworzenie nowej instancji klasy. Dokładne omówienie tej metody znajduje się w rozdziale o metaklasach. |
|
Konstruktor. Metoda wywoływana przy tworzeniu nowej instancji danej klasy. |
|
Destruktor. Ta metoda powinna być przeciążana tylko w szczególnych przypadkach. W większości przypadków lepiej jest utworzyć osobną metodę (np. |
|
Wywoływana przez funkcję |
|
Wywoływana przez funkcję |
|
Operatory porównania wywoływane przez wyrażenia, takie jak |
|
Funkcja porównania. Wywoływana, gdy nie zdefiniowano powyższych operatorów. |
|
Wywoływana przez funkcję |
|
Wywoływana w trakcie sprawdzania wartości |
Dostęp do atrybutów¶
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
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)
<ipython-input-29-e6f05cd5e055> in <module>
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 dowolny obiekt, który zawiera przynajmniej jedną z trzech poniższych metod specjalnych. 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:
Metody specjalne deskryptora |
Opis |
---|---|
|
Wywoływana do pobrania atrybutu z klasy - “właściciela” |
|
Wywoływana do ustawienia atrybutu z klasy - “właściciela” |
|
Wywoływana w czasie usuwania atrybutu w klasie - “właścicielu” |
Przykład deskryptora¶
import random
class Die:
def __init__(self, sides=6):
self.sides = sides
def __get__(self, instance, owner):
print('self =', self)
print('instance =', instance)
print('owner =', owner)
return int(random.random() * self.sides) + 1
def __set__(self, instance, value):
print('self =', self)
print('instance =', instance)
print('value =', value)
def __delete__(self, instance):
print('self =', self)
print('instance =', instance)
class Game:
d6 = Die()
d10 = Die(sides=10)
d20 = Die(sides=20)
Game.d6
self = <__main__.Die object at 0x7fc85b329460>
instance = None
owner = <class '__main__.Game'>
4
game = Game()
game.d20
self = <__main__.Die object at 0x7fc85b33b070>
instance = <__main__.Game object at 0x7fc85b3815b0>
owner = <class '__main__.Game'>
15
game.d20 = 42
self = <__main__.Die object at 0x7fc85b33b070>
instance = <__main__.Game object at 0x7fc85b3815b0>
value = 42
del game.d20
self = <__main__.Die object at 0x7fc85b33b070>
instance = <__main__.Game object at 0x7fc85b3815b0>
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.
Powoduje to spory narzut na pamięć.
Jeśli nasza klasa nie będzie korzystała z dynamicznej natury takiego słownika, to w klasie można zdefiniować __slots__
i podać listę wszystkich składowych.
class Point:
__slots__ = ['x', 'y']
p = Point()
p.x = 10
p.y = 20
p.z = 30
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-75-0ad9e402b8fa> in <module>
2 p.x = 10
3 p.y = 20
----> 4 p.z = 30
AttributeError: 'Point' 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 __lt__(self, other):
return self <= other and (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 __le__(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.