9. Klasy

Klasy umożliwiają łączenie danych i funkcjonalności. Tworzenie nowej klasy, tworzy nowy typ obiektu, umożliwiając tworzenie nowych instancji tego typu. Do każdej instancji klasy można przypisać atrybuty służące do utrzymywania jej stanu. Instancje klas mogą również posiadać metody (zdefiniowane przez klasę) umożliwiające modyfikację ich stanu.

W porównaniu do innych języków programowania, w Pythonie, mechanizm dodawania nowych klas wymaga niewielkiej ilości nowej składni i semantyki. Jest to połączenie mechanizmu klas, które można znaleźć w C++ i Modula-3. Klasy w Pythonie dostarczają wszystkie standardowe cechy programowania obiektowego: mechanizm dziedziczenia klas pozwala na wiele klas bazowych, klasy pochodne mogą nadpisać każdą metodę klasy lub klas bazowych i metoda może wywołać metody klas bazowych o tej samej nazwie. Obiekty mogą zawierać dowolną ilość i rodzaj danych. Zarówno klasy jak i moduły są częścią dynamicznej natury Pythona: są tworzone w trakcie działania programu i mogą być modyfikowane później, po stworzeniu.

Korzystając z terminologii C++, składniki klas (także pola) są publiczne (z wyjątkiem zobacz poniżej Zmienne prywatne), a wszystkie metody są wirtualne. Podobnie jak w Moduli-3, nie ma skrótów pozwalających na odnoszenie się do składników klas z ich metod: metoda jest deklarowana poprzez podanie wprost jako pierwszego argumentu obiektu, który w czasie wywołania metody zostanie jej przekazany niejawnie. Podobnie jak w Smalltalku, same klasy także są obiektami. Dostarcza nam to wyrażeń semantycznych pozwalających na importowanie i zmianę nazw klasy. Inaczej niż w C++ i Moduli-3 wbudowane typy mogą stanowić klasy, z których klasa użytkownika będzie dziedziczyć. Podobnie jak w C++, większość wbudowanych operatorów ze specjalną składnią (operatory arytmetyczne, indeksowanie) może być przedefiniowane przez instancje klasy.

(Z powodu braku ogólnie zaakceptowanej terminologii w kontekście klas, będę używał terminów ze Smalltalk i C++. Użyłbym Modula-3 ponieważ semantyka jego programowania obiektowego jest bliższa Pythonowi niż C++ ale zakładam, że mniej czytelników o nim słyszało.)

9.1. Kilka słów o nazwach i obiektach

Obiekty mają indywidualność, a wiele nazw (w wielu zakresach) może być powiązanych z tym samym obiektem. Jest to znane jako aliasing w innych językach. Zwykle nie jest to doceniane na pierwszy rzut oka w Pythonie i można je bezpiecznie zignorować, gdy mamy do czynienia z niezmiennymi typami podstawowymi (liczby, ciągi znaków, krotki). Jednak aliasing ma prawdopodobnie zaskakujący wpływ na semantykę kodu Pythona, który obejmuje zmienne obiekty, takie jak listy, słowniki i większość innych typów. Jest to zwykle wykorzystywane z korzyścią dla programu, ponieważ aliasy pod pewnymi względami zachowują się jak wskaźniki. Na przykład przekazanie obiektu jest tanie, ponieważ implementacja przekazuje tylko wskaźnik; a jeśli funkcja modyfikuje obiekt przekazany jako argument, wywołujący zobaczy zmianę — eliminuje to potrzebę stosowania dwóch różnych mechanizmów przekazywania argumentów, jak w Pascalu.

9.2. Zasięgi widoczności i przestrzenie nazw w Pythonie

Przed wprowadzeniem klas, najpierw muszę powiedzieć ci coś o zasadach zakresu Pythona. Definicje klas stosują kilka zgrabnych sztuczek z przestrzeniami nazw, a żeby w pełni zrozumieć, co się dzieje, trzeba wiedzieć, jak działają zakresy i przestrzenie nazw.

Zacznijmy od kilku definicji.

Przestrzeń nazw to odwzorowanie z nazw na obiekty. Większość przestrzeni nazw jest obecnie implementowana jako słowniki Pythona, ale to zwykle nie jest zauważalne w żaden sposób (z wyjątkiem wydajności), a to może się zmienić w przyszłości. Przykładami przestrzeni nazw są: zbiór nazw wbudowanych (zawierający funkcje np. abs() i nazwy wbudowanych wyjątków); nazwy globalne w module; oraz nazwy lokalne w wywołaniu funkcji. W pewnym sensie zbiór atrybutów obiektu również tworzy przestrzeń nazw. Ważną rzeczą, którą należy wiedzieć o przestrzeniach nazw, jest to, że nie ma absolutnie żadnych relacji między nazwami w różnych przestrzeniach nazw; na przykład, dwa różne moduły mogą zdefiniować funkcję maximize bez zamieszania — użytkownicy modułów muszą poprzedzić go nazwą modułu.

Nawiasem mówiąc, używam słowa atrybut dla każdej nazwy następującej po kropce — na przykład w wyrażeniu z.real, real jest atrybutem obiektu z. Ściśle mówiąc, odniesienia do nazw w modułach są odniesieniami atrybutowymi: w wyrażeniu modname.funcname, modname jest obiektem modułu, a funcname jest jego atrybutem. W tym przypadku istnieje proste odwzorowanie między atrybutami modułu i nazwami globalnymi zdefiniowanymi w module: mają tę samą przestrzeń nazw! [1]

Atrybuty mogą być tylko do odczytu lub zapisywalne. W tym drugim przypadku możliwe jest przypisanie do atrybutu. Atrybuty modułu są zapisywalne: można zapisać modname.the_answer = 42. Zapisywalne atrybuty można również usunąć za pomocą instrukcji del. Na przykład, del modname.the_answer usunie atrybut the_answer z obiektu o nazwie modname.

Przestrzenie nazw są tworzone w różnych momentach i mają różny czas życia. Przestrzeń nazw zawierająca nazwy wbudowane jest tworzona podczas uruchamiania interpretera Pythona i nigdy nie jest usuwana. Globalna przestrzeń nazw dla modułu jest tworzona, gdy wczytywana jest definicja modułu; zwykle przestrzenie nazw modułu również trwają do zakończenia działania interpretera. Instrukcje wykonywane przez wywołanie interpretera najwyższego poziomu, zarówno odczytane z pliku skryptu, jak i interaktywnie, są uważane za część modułu o nazwie __main__, więc mają swoją własną globalną przestrzeń nazw. (Nazwy wbudowane w rzeczywistości również znajdują się w module; nazwany jest on builtins).

Lokalna przestrzeń nazw dla funkcji jest tworzona przy wywołaniu funkcji i usuwana, gdy funkcja zwraca wynik lub rzuca wyjątek, którego nie obsługuje. (Właściwie, zapominanie byłoby lepszym słowem na opisanie tego, co faktycznie się dzieje). Oczywiście, każde wywołanie rekurencyjne ma swoją własną lokalną przestrzeń nazw.

Zakres to tekstowy obszar programu Python, w którym przestrzeń nazw jest bezpośrednio dostępna. „Bezpośrednio dostępna” oznacza tutaj, że niekwalifikowane odwołanie do nazwy próbuje znaleźć ją w przestrzeni nazw.

Chociaż zakresy są określane statycznie, są używane dynamicznie. Cały czas w trakcie wykonywania programu istnieją 3 lub 4 zagnieżdżone zakresy, których przestrzenie nazw są bezpośrednio dostępne:

  • najbardziej wewnętrzny zakres, który jest przeszukiwany jako pierwszy, zawiera nazwy lokalne

  • zakresy wszystkich otaczających funkcji, które są przeszukiwane począwszy od najbliższego otaczającego zakresu, zawierają nazwy nielokalne, ale także nieglobalne

  • przedostatni zakres zawiera globalne nazwy bieżącego modułu

  • najbardziej zewnętrznym zakresem (przeszukiwanym jako ostatni) jest przestrzeń nazw zawierająca nazwy wbudowane

Jeśli nazwa jest zadeklarowana jako globalna, wtedy wszystkie referencje i przypisania przechodzą bezpośrednio do przedostatniego zakresu zawierającego globalne nazwy modułu. Aby ponownie powiązać zmienne znajdujące się poza najbardziej wewnętrznym zakresem, można użyć instrukcji nonlocal; jeśli nie są zadeklarowane jako nielokalne, zmienne te są tylko do odczytu (próba zapisu do takiej zmiennej po prostu utworzy nową zmienną lokalną w najbardziej wewnętrznym zakresie, pozostawiając identycznie nazwaną zmienną zewnętrzną bez zmian).

Zazwyczaj zakres lokalny odwołuje się do nazw lokalnych (tekstowo) bieżącej funkcji. Poza funkcjami, zakres lokalny odwołuje się do tej samej przestrzeni nazw, co zakres globalny: przestrzeni nazw modułu. Definicje klas umieszczają jeszcze jedną przestrzeń nazw w zakresie lokalnym.

Ważne jest, aby zdać sobie sprawę, że zakresy są określane tekstowo: globalny zakres funkcji zdefiniowany w module jest przestrzenią nazw tego modułu, bez względu na to, skąd lub przez jaki alias funkcja jest wywoływana. Z drugiej strony, rzeczywiste wyszukiwanie nazw odbywa się dynamicznie, w czasie wykonywania — jednak definicja języka ewoluuje w kierunku statycznego rozpoznawania nazw, w czasie „kompilacji”, więc nie należy polegać na dynamicznym rozpoznawaniu nazw! (W rzeczywistości zmienne lokalne są już określane statycznie).

Szczególnym dziwactwem Pythona jest to, że – jeśli nie działa instrukcja global lub nonlocal – przypisanie do nazw zawsze trafia do najbardziej wewnętrznego zakresu. Przypisania nie kopiują danych – po prostu wiążą nazwy z obiektami. To samo dotyczy usuwania: instrukcja del x usuwa wiązanie x z przestrzeni nazw, do której odwołuje się zakres lokalny. W rzeczywistości wszystkie operacje, które wprowadzają nowe nazwy, używają zakresu lokalnego: w szczególności instrukcje import i definicje funkcji wiążą nazwę modułu lub funkcji w zakresie lokalnym.

Instrukcja global może być użyta do wskazania, że określone zmienne znajdują się w zakresie globalnym i powinny być tam ponownie wiązane; instrukcja nonlocal wskazuje, że określone zmienne znajdują się w zakresie otaczającym i powinny być tam ponownie wiązane.

9.2.1. Przykład zakresów i przestrzeni nazw

Oto przykład pokazujący, jak odwoływać się do różnych zakresów i przestrzeni nazw oraz jak global i nonlocal wpływają na wiązanie zmiennych:

def scope_test():
    def do_local():
        spam = "lokalna konserwa"

    def do_nonlocal():
        nonlocal spam
        spam = "nielokalna konserwa"

    def do_global():
        global spam
        spam = "globalna konserwa"

    spam = "testowa konserwa"
    do_local()
    print("Po przypisaniu lokalnym:", spam)
    do_nonlocal()
    print("Po przypisaniu nielokalnym:", spam)
    do_global()
    print("Po przypisaniu globalnym:", spam)

scope_test()
print("W globalnym zakresie:", spam)

Wyjście przykładowego kodu to:

Po przypisaniu lokalnym: testowa konserwa
Po przypisaniu nielokalnym: nielokalna konserwa
Po przypisaniu globalnym: nielokalna konserwa
W globalnym zakresie: globalna konserwa

Zauważ, że lokalne przypisanie (które jest domyślne) nie zmieniło wiązania spam w scope_test. Przypisanie nonlocal zmieniło wiązanie spam w scope_test, a przypisanie global zmieniło wiązanie na poziomie modułu.

Można również zauważyć, że nie było wcześniejszego powiązania dla spam przed przypisaniem global.

9.3. Pierwsze spojrzenie na klasy

Klasy wprowadzają trochę nowej składni, trzy nowe typy obiektów i trochę nowej semantyki.

9.3.1. Składnia definicji klasy

Najprostsza forma definicji klasy wygląda następująco:

class ClassName:
    <instrukcja-1>
    .
    .
    .
    <instrukcja-N>

Definicje klas, podobnie jak definicje funkcji (instrukcje def), muszą zostać wykonane, zanim będą miały jakikolwiek efekt. (Można sobie wyobrazić umieszczenie definicji klasy w gałęzi instrukcji if lub wewnątrz funkcji.)

W praktyce, instrukcje wewnątrz definicji klasy będą zwykle definicjami funkcji, ale inne instrukcje są dozwolone, a czasem przydatne — wrócimy do tego później. Definicje funkcji wewnątrz klasy zwykle mają specyficzną formę listy argumentów, podyktowaną konwencjami wywoływania metod — ponownie, zostanie to wyjaśnione później.

Po wejściu w definicję klasy tworzona jest nowa przestrzeń nazw i używana jako zakres lokalny — a zatem wszystkie przypisania do zmiennych lokalnych trafiają do tej nowej przestrzeni nazw. W szczególności, definicje funkcji wiążą nazwę nowej funkcji w tej przestrzeni nazw.

Kiedy definicja klasy jest opuszczana normalnie (przez koniec), tworzony jest obiekt klasy. Jest to w zasadzie opakowanie wokół zawartości przestrzeni nazw utworzonej przez definicję klasy; dowiemy się więcej o obiektach klas w następnej sekcji. Oryginalny zakres lokalny (ten, który obowiązywał tuż przed wprowadzeniem definicji klasy) zostaje przywrócony, a obiekt klasy jest powiązany z nazwą klasy podaną w nagłówku definicja klasy (ClassName w przykładzie).

9.3.2. Obiekty klas

Obiekty klas obsługują dwa rodzaje operacji: odniesienia do atrybutów i tworzenie instancji.

Odniesienia do atrybutów używają standardowej składni używanej dla wszystkich odniesień do atrybutów w Pythonie: obj.name. Prawidłowe nazwy atrybutów to wszystkie nazwy, które znajdowały się w przestrzeni nazw klasy, gdy obiekt klasy został utworzony. Tak więc, jeśli definicja klasy wyglądała tak:

class MyClass:
    """Prosta przykładowa klasa"""
    i = 12345

    def f(self):
        return 'witaj świecie'

wtedy MyClass.i i MyClass.f są poprawnymi odniesieniami do atrybutów, zwracającymi odpowiednio liczbę całkowitą i funkcję. Do atrybutów klasowych można również przypisywać wartości, więc można zmienić wartość MyClass.i przez przypisanie. __doc__ jest również poprawnym atrybutem, zwracającym docstring należący do klasy: "Prosta przykładowa klasa".

Instancjonowanie klasy używa notacji funkcji. Wystarczy udawać, że obiekt klasy jest bezparametrową funkcją, która zwraca nową instancję klasy. Na przykład (zakładając powyższą klasę):

x = MyClass()

tworzy nową instancję klasy i przypisuje ten obiekt do zmiennej lokalnej x.

Operacja instancjonowania („wywołanie” obiektu klasy) tworzy pusty obiekt. Wiele klas lubi tworzyć obiekty z instancjami dostosowanymi do określonego stanu początkowego. Dlatego klasa może zdefiniować specjalną metodę o nazwie __init__(), taką jak ta:

def __init__(self):
    self.data = []

Gdy klasa definiuje metodę __init__(), instancjonowanie klasy automatycznie wywołuje __init__() dla nowo utworzonej instancji klasy. Tak więc w tym przykładzie nową, zainicjalizowaną instancję można uzyskać przez:

x = MyClass()

Oczywiście metoda __init__() może mieć argumenty dla większej elastyczności. W takim przypadku argumenty podane operatorowi instancjonowania klasy są przekazywane do __init__(). Na przykład

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. Obiekty instancji

Co możemy zrobić z obiektami instancji? Jedynymi operacjami rozumianymi przez obiekty instancji są odniesienia do atrybutów. Istnieją dwa rodzaje poprawnych nazw atrybutów: atrybuty danych i metody.

Dane atrybut odpowiadają „zmiennym instancji” i „członkom danych” w C++. Dane atrybut nie muszą być deklarowane; podobnie jak zmienne lokalne, powstają, gdy zostaną po raz pierwszy przypisane. Na przykład, jeśli x jest instancją MyClass utworzoną powyżej, poniższy fragment kodu wydrukuje wartość 16, bez pozostawiania śladu:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

Innym rodzajem odniesienia do atrybutu instancji jest metoda. Metoda to funkcja, która „należy” do obiektu.

Prawidłowe nazwy metod obiektu instancji zależą od jego klasy. Z definicji wszystkie atrybuty klasy, które są obiektami funkcji definiują odpowiednie metody jej instancji. Tak więc w naszym przykładzie, x.f jest poprawnym odwołaniem do metody, ponieważ MyClass.f jest funkcją, ale x.i nie jest, ponieważ MyClass.i nie jest. Ale x.f nie jest tym samym co MyClass.f — jest obiektem metody, a nie obiektem funkcji.

9.3.4. Obiekty metod

Zazwyczaj metoda jest wywoływana zaraz po jej powiązaniu:

x.f()

W przykładzie MyClass będzie to zwracać napis 'hello world' . Nie jest jednak konieczne wywoływanie metody od razu: x.f jest obiektem metody i może być przechowywany i wywoływany później. Na przykład:

xf = x.f
while True:
    print(xf())

będzie drukować hello world do końca czasu.

Co dokładnie dzieje się, gdy wywoływana jest metoda? Być może zauważyłeś, że x.f() został wywołany bez argument powyżej, mimo że definicja funkcja dla f() określała argument. Co się stało z argumentem? Z pewnością Python rzucić wyjątek, gdy funkcja, który wymaga argument jest wywoływany bez żadnego — nawet jeśli argument nie jest faktycznie używany….

Właściwie, być może zgadłeś odpowiedź: szczególną rzeczą w metodach jest to, że obiekt instancji jest przekazywany jako pierwszy argument z funkcja. W naszym przykładzie wywołanie x.f() jest dokładnie równoważne wywołaniu MyClass.f(x). Ogólnie rzecz biorąc, wywołanie metody z listą n argument jest równoważne wywołaniu odpowiedniej funkcja z listą argument, która jest tworzona przez wstawienie obiektu instancji metody przed pierwszym argument.

Ogólnie rzecz biorąc, metody działają w następujący sposób. Gdy odwoływane jest nie-dane atrybut instancji, wyszukiwana jest klasa instancji. Jeśli nazwa wskazuje na prawidłową klasę atrybut, która jest obiektem funkcja, odwołania zarówno do obiektu instancji, jak i obiektu funkcja są pakowane do obiektu metody. Gdy obiekt metody jest wywoływany z listą argument, nowa lista argument jest konstruowana z instancji obiektu i listy argument, a obiekt funkcja jest wywoływany z tą nową listą argument.

9.3.5. Zmienne klas i instancji

Ogólnie rzecz biorąc, zmienne instancji są dla danych unikalnych dla każdej instancji, a zmienne klasy są dla atrybutów i metod współdzielonych przez wszystkie instancje klasy:

class Dog:

  kind = 'pies' # zmienna klasowa współdzielona przez wszystkie instancje

  def __init__(self, name):
    self.name = name # zmienna instancji unikalna dla każdej instancji

>>> d = Dog('Fido')
>>> e = Dog('Zito')
>>> d.kind # współdzielona przez wszystkie psy
'pies'
>>> e.kind # współdzielona przez wszystkie psy
'pies'
>>> d.name # unikalna dla d
'Fido'
>>> e.name # unikalna dla e
'Zito'

Jak omówiono w Kilka słów o nazwach i obiektach, współdzielone dane mogą mieć zaskakujące efekty z udziałem mutable obiektów takich jak listy i słownik. Na przykład, lista sztuczki w poniższym kodzie nie powinna być używana jako zmienna klasowa, ponieważ tylko jedna lista byłaby współdzielona przez wszystkie instancje klasy Dog:

class Dog:

    tricks = []             # błędne użycie zmiennej klasowej

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('przewróć się')
>>> e.add_trick('udawaj martwego')
>>> d.tricks                # nieoczekiwanie współdzielone przez wszystkie psy
['przewróć się', 'udawaj martwego']

Prawidłowy projekt klasy powinien zamiast tego używać zmiennej instancji:

class Dog:

def __init__(self, name):
self.name = name
self.tricks = [] # tworzy nową pustą listę dla każdego psa

def add_trick(self, trick):
self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Zito')
>>> d.add_trick('przewróć się')
>>> e.add_trick('udawaj martwego')
>>> d.tricks
['przewróć się']
>>> e.tricks
['udawaj martwego']

9.4. Uwagi losowe

Jeśli ten sam atrybut występuje zarówno w instancji, jak i w klasie, wówczas wyszukiwanie atrybut nadaje priorytet instancji:

>>> class Magazyn:
... cel = 'przechowywanie'
... region = 'zachód'
...
>>> m1 = Magazyn()
>>> print(m1.cel, m1.region)
przechowywanie zachód
>>> m2 = Magazyn()
>>> m2.region = 'wschód'
>>> print(m2.cel, m2.region)
przechowywanie wschód

Do danych atrybut mogą odwoływać się zarówno metody, jak i zwykli użytkownicy („klienci”) obiektu. Innymi słowy, klasy nie nadają się do implementacji czysto abstrakcyjnych typów danych. W rzeczywistości nic w Python nie umożliwia wymuszenia ukrywania danych — wszystko opiera się na konwencji. (Z drugiej strony, implementacja Python, napisana w C, może całkowicie ukryć szczegóły implementacji i kontrolować dostęp do obiektu, jeśli to konieczne; może to być wykorzystane przez rozszerzenia Python napisane w C).

Klienci powinni używać danych atrybut z ostrożnością — klienci mogą zepsuć niezmienniki utrzymywane przez metody poprzez stemplowanie ich danych atrybut. Należy pamiętać, że klienci mogą dodawać własne dane atrybut do obiektu instancji bez wpływu na ważność metod, o ile unika się konfliktów nazw — ponownie, konwencja nazewnictwa może tutaj zaoszczędzić wiele problemow.

Nie ma skrótu do odwoływania się do danych atrybut (lub innych metod!) z poziomu metod. Uważam, że w rzeczywistości zwiększa to czytelność metod: nie ma szans na pomylenie zmiennych lokalnych i zmiennych instancji podczas przeglądania metody.

Często pierwsza argument metody jest nazywana self. Jest to nic więcej niż konwencja: nazwa self nie ma absolutnie żadnego specjalnego znaczenia dla Python. Należy jednak pamiętać, że nieprzestrzeganie tej konwencji może sprawić, że kod będzie mniej czytelny dla innych programistów Python, a także możliwe jest napisanie programu przeglądarki klas, który opiera się na takiej konwencji.

Każdy obiekt funkcja będący klasą atrybut definiuje metodę dla instancji tej klasy. Nie jest konieczne, aby definicja funkcja była tekstowo zawarta w definicja klasy: przypisanie obiektu funkcja do zmiennej lokalnej w klasie jest również w porządku. Na przykład:

# funkcja zdefiniowane poza klasą
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

Teraz f, g i h są wszystkie atrybut klasy C, które odnoszą się do obiektów funkcja, a w konsekwencji wszystkie są metodami instancji Ch jest dokładnie równoważne g. Zauważ, że ta praktyka zwykle służy jedynie do zmylenia czytelnika programu.

Metody mogą wywoływać inne metody przy użyciu metody atrybut z self argument

class Torba:
    def __init__(self):
        self.dane = []

    def dodac(self, x):
        self.dane.append(x)

    def  dodacDwaRazy(self, x):
        self.dodac(x)
        self.dodac(x)

Metody mogą odwoływać się do nazw globalnych w taki sam sposób jak zwykłe funkcje. Zakres globalny powiązany z metodą to moduł zawierający jej definicję. (Klasa nigdy nie jest używana jako zakres globalny.) Podczas gdy rzadko można napotkać dobry powód do używania danych globalnych w metodzie, istnieje wiele uzasadnionych zastosowań zakresu globalnego: po pierwsze, funkcja i moduł importowane do zakresu globalnego mogą być używane przez metody, a także funkcja i klasy w nim zdefiniowane. Zazwyczaj klasa zawierająca metodę sama jest zdefiniowana w tym globalnym zakresie, a w następnej sekcji znajdziemy kilka dobrych powodów, dla których metoda chciałaby odwoływać się do własnej klasy.

Każda wartość jest obiektem, a zatem ma klasę (zwaną również typem). Jest ona przechowywana jako object.__class__.

9.5. Dziedziczenie

Oczywiście funkcja językowa nie byłaby godna nazwy „klasa” bez obsługi dziedziczenia. Składnia pochodnej definicja klasy wygląda tak:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

Nazwa BaseClassName musi być zdefiniowana w przestrzeni nazw dostępnej z zakresu zawierającego pochodną definicja klasy. Zamiast nazwy klasa bazowa dozwolone są również inne dowolne wyrażenia. Może to być przydatne, na przykład, gdy klasa bazowa jest zdefiniowany w innym moduł:

class DerivedClassName(modname.BaseClassName):

Wykonanie pochodnej definicja klasy przebiega tak samo, jak w przypadku klasa bazowa. Gdy konstruowany jest obiekt klasy, zapamiętywany jest klasa bazowa. Jest to wykorzystywane do rozwiązywania referencji atrybut: jeśli żądany atrybut nie zostanie znaleziony w klasie, wyszukiwanie jest kontynuowane w klasie bazowej. Ta reguła jest stosowana rekurencyjnie, jeśli sama klasa bazowa jest pochodną innej klasy.

Nie ma nic specjalnego w instancjonowaniu klas pochodnych: DerivedClassName() tworzy nową instancję klasy. Odniesienia do metod są rozwiązywane w następujący sposób: odpowiednia klasa atrybut jest przeszukiwana, w razie potrzeby schodząc w dół łańcucha klas bazowych, a odniesienie do metody jest ważne, jeśli daje to obiekt funkcja.

Klasy pochodne mogą nadpisywać metody swoich klas bazowych. Ponieważ metody nie mają specjalnych przywilejów podczas wywoływania innych metod tego samego obiektu, metoda klasa bazowa, która wywołuje inną metodę zdefiniowaną w tym samym klasa bazowa może skończyć się wywołaniem metody klasy pochodnej, która ją nadpisuje. (Dla programistów C++: wszystkie metody w Python są efektywnie virtual).

Metoda nadrzędna w klasie pochodnej może w rzeczywistości chcieć rozszerzyć, a nie tylko zastąpić metodę klasa bazowa o tej samej nazwie. Istnieje prosty sposób na bezpośrednie wywołanie metody klasa bazowa: wystarczy wywołać BaseClassName.methodname(self, arguments). Jest to czasami przydatne również dla klientów. (Należy pamiętać, że działa to tylko wtedy, gdy klasa bazowa jest dostępna jako BaseClassName w zakresie globalnym).

Python ma dwa wbudowane funkcje, które pracuja z dziedziczeniem:

  • Użyj isinstance() do sprawdzenia typu instancji: isinstance(obj, int) będzie True tylko wtedy, gdy obj.__class__ jest int lub jakąś klasą pochodną od int.

  • Użyj issubclass() do sprawdzenia dziedziczenia klas: issubclass(bool, int) jest True, ponieważ bool jest podklasa z int. Jednak issubclass(float, int) jest False, ponieważ float nie jest podklasa z int.

9.5.1. Dziedziczenie wielokrotne

Python obsługuje również dziedziczenie wielokrotne. Strona definicja klasy z wieloma klasami bazowymi wygląda tak:

class NazwaKlasyPochodnej(Baza1, Baza2, Baza3):
    <statement-1>
    .
    .
    .
    <statement-N>

Dla większości celów, w najprostszych przypadkach, można myśleć o wyszukiwaniu atrybut odziedziczonym po klasie nadrzędnej jako o wyszukiwaniu w głąb, od lewej do prawej, nie szukając dwa razy w tej samej klasie, gdy hierarchia się pokrywa. Tak więc, jeśli atrybut nie zostanie znaleziony w DerivedClassName, jest szukany w Base1, a następnie (rekurencyjnie) w klasach bazowych Base1, a jeśli nie zostanie tam znaleziony, jest szukany w Base2 i tak dalej.

W rzeczywistości jest to nieco bardziej skomplikowane; kolejność rozwiązywania metod zmienia się dynamicznie, aby wspierać kooperacyjne wywołania super(). Podejście to jest znane w niektórych innych językach wielokrotnego dziedziczenia jako call-next-method i jest bardziej wydajne niż super wywołanie występujące w językach pojedynczego dziedziczenia.

Dynamiczne porządkowanie jest konieczne, ponieważ wszystkie przypadki wielokrotnego dziedziczenia wykazują jedną lub więcej relacji diamentowych (w których co najmniej jedna z klas nadrzędnych może być dostępna poprzez wiele ścieżek z najniższej klasy). Na przykład, wszystkie klasy dziedziczą z object, więc każdy przypadek wielokrotnego dziedziczenia zapewnia więcej niż jedną ścieżkę dostępu do object. Aby zapobiec wielokrotnemu dostępowi do klas bazowych, algorytm dynamiczny linearyzuje kolejność wyszukiwania w sposób, który zachowuje kolejność od lewej do prawej określoną w każdej klasie, która wywołuje każdego rodzica tylko raz i która jest monotoniczna (co oznacza, że klasa może być podklasowana bez wpływu na kolejność pierwszeństwa jej rodziców). Łącznie właściwości te umożliwiają projektowanie niezawodnych i rozszerzalnych klas z wielokrotnym dziedziczeniem. Więcej szczegółów można znaleźć na stronie The Python 2.3 Method Resolution Order.

9.6. Zmienne prywatne

„Prywatne” zmienne instancje, do których nie można uzyskać dostępu inaczej niż z wnętrza obiektu, nie istnieją w Python. Istnieje jednak konwencja, której przestrzega większość kodu Python: nazwa poprzedzona podkreśleniem (np. _spam) powinna być traktowana jako niepubliczna część API (niezależnie od tego, czy jest to funkcja, metoda czy członek danych). Należy ją traktować jako szczegół implementacji i może ona ulec zmianie bez powiadomienia.

Ponieważ istnieje uzasadniony przypadek użycia dla członków klasy-prywatnej (mianowicie, aby uniknąć kolizji nazw z nazwami zdefiniowanymi przez podklasa), istnieje ograniczone wsparcie dla takiego mechanizmu, zwanego name mangling. Każdy identyfikator w postaci __spam (co najmniej dwa początkowe podkreślenia, co najwyżej jedno końcowe podkreślenie) jest tekstowo zastępowany przez _classname__spam, gdzie classname jest bieżącą nazwą klasy z usuniętymi początkowymi podkreśleniami. Zamiana ta jest wykonywana bez względu na pozycję składniową identyfikatora, o ile występuje on w definicji klasy.

Zobacz także

Szczegółowe informacje i przypadki specjalne znajdują się w specyfikacji private name mangling.

Manipulowanie nazwami jest pomocne w umożliwieniu podklasa nadpisywania metod bez przerywania wywołań metod wewnątrzklasowych. Na przykład:

class Mapping:
    def __init__(self, iterable):
        self.przedmiot_lista = []
        self.__update(iterable)

    def update(self, iterable):
        for przedmiot in iterable:
            self.przedmiot_lista.append(przedmiot)

    __update = update # prywatna kopia oryginalnej metody update()

class MappingSubklasa(Mapping):

    def update(self, keys, values):
        # zapewnia nową sygnaturę dla update()
        # ale nie przerywa __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Powyższy przykład zadziała nawet wtedy, gdy MappingSubclass wprowadzi identyfikator __update, ponieważ zostanie on zastąpiony odpowiednio _Mapping__update w klasie Mapping i _MappingSubclass__update w klasie MappingSubclass.

Należy pamiętać, że reguły mieszania zostały zaprojektowane głównie w celu uniknięcia wypadków; nadal możliwy jest dostęp lub modyfikacja zmiennej, która jest uważana za prywatną. Może to być nawet przydatne w szczególnych okolicznościach, takich jak debugger.

Zauważ, że kod przekazany do exec() lub eval() nie uważa nazwy klasy wywołującej za bieżącą klasę; jest to podobne do efektu global instrukcja , którego efekt jest również ograniczony do kodu, który jest kompilowany bajtowo. To samo ograniczenie dotyczy getattr(), setattr() i delattr(), a także bezpośredniego odwoływania się do __dict__.

9.7. Przypadki losowe

Czasami przydatne jest posiadanie typu danych podobnego do „rekordu” Pascala lub „struktury” C, łączącego kilka nazwanych elementów danych. Idiomatycznym podejściem jest użycie w tym celu dataclasses:

from dataclasses import dataclass

@dataclass
class Pracownik:
    imie: str
    dzial: str
    pensja: int
>>> stanislaw = Pracownik('stanislaw', 'pracownia komputerowa, 1000)
>>> stanislaw.dzial
'pracownia komputerowa'
>>> stanislaw.pensja
1000

Fragment kodu Python, który oczekuje określonego abstrakcyjnego typu danych, może często zostać przekazany klasie, która emuluje metody tego typu danych. Na przykład, jeśli masz klasę funkcja, która formatuje pewne dane z obiektu pliku, możesz zdefiniować klasę z metodami read() i readline(), które pobierają dane z bufora napis i przekazują je jako argument.

Obiekty metod instancji także mają atrybuty: m.__self__ jest obiektem instancji z metodą m(), i m.__func__ jest obiektem funkcji odpowiadającym metodzie.

9.8. Iteratory

Prawdopodobnie zauważyłeś już, że większość obiektów kontenera można iterować za pomocą for instrukcja

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("mojplik.txt"):
    print(line, end='')

Ten styl dostępu jest jasny, zwięzły i wygodny. Użycie iterators przenika i ujednolica Python. Za kulisami, for instrukcja wywołuje iter() na obiekcie kontenera. Obiekt funkcja zwracać i iterator definiują metodę __next__(), która uzyskuje dostęp do elementów w kontenerze jeden po drugim. Gdy nie ma więcej elementów, __next__() rzucić wyjątek StopIteration, który informuje pętlę for o zakończeniu. Metodę __next__() można wywołać za pomocą next() wbudowanej funkcja; ten przykład pokazuje, jak to wszystko działa:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

Po zapoznaniu się z mechaniką stojącą za protokołem iterator, łatwo jest dodać iterator zachowanie do swoich klas. Zdefiniuj metodę __iter__(), która zwracać obiekt z metodą __next__(). Jeśli klasa definiuje __next__(), to __iter__() może po prostu zwracać self

class Reverse:
    """iterator do zapętlania sekwencji wstecz. """
    def __init__(self, dane):
        self.dane = dane
        self.indeks = len(dane)

    def __iter__(self):
        return self

    def __next__(self):
        if self.indeks == 0:
            raise StopIteration
        self.indeks = self.indeks - 1
        return self.data[self.indeks]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print(char)
...
m
a
p
s

9.9. Generatory

Generatory są prostym i potężnym narzędziem do tworzenia iteratorów. Są one pisane jak zwykłe funkcje, ale używają instrukcji yield za każdym razem, gdy chcą zwracać dane. Za każdym razem, gdy next() jest na nim wywoływany, generator wznawia działanie od miejsca, w którym zostało przerwane (pamięta wszystkie wartości danych i które instrukcje było ostatnio wykonywane). Przykład pokazuje, że tworzenie generatorów może być banalnie proste:

def reverse(dane):
    for indeks in range(len(dane)-1, -1, -1):
        yield dane[indeks]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

Wszystko, co można zrobić za pomocą generatorów, można również zrobić za pomocą iteratorów opartych na klasach, jak opisano w poprzedniej sekcji. Kompaktowość generatorów polega na tym, że metody __iter__() i __next__() są tworzone automatycznie.

Inną kluczową cechą jest to, że zmienne lokalne i stan wykonania są automatycznie zapisywane między wywołaniami. Sprawia to, że funkcja jest łatwiejszy do napisania i znacznie bardziej przejrzysty niż podejście wykorzystujące zmienne instancji, takie jak self.index i self.data.

Oprócz automatycznego tworzenia metod i zapisywania stanu programu, po zakończeniu generatory automatycznie rzucić StopIteration . W połączeniu, funkcje te ułatwiają tworzenie iteratorów bez większego wysiłku niż napisanie zwykłego funkcja.

9.10. generator wyrażenia

Niektóre proste generatory mogą być kodowane w zwięzły sposób jako wyrażenia przy użyciu składni podobnej do list comprehensions, ale z nawiasami zamiast nawiasów kwadratowych. Wyrażenia te są przeznaczone do sytuacji, w których generator jest używane od razu przez otaczającą je funkcja. Wyrażenia generator są bardziej zwięzłe, ale mniej wszechstronne niż pełne definicje generator i zwykle są bardziej przyjazne dla pamięci niż równoważne wyrażenia listowe.

Przykłady:

>>> sum(i*i for i in range(10))                 # suma kwadratów
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # iloczyn skalarny
260

>>> unique_words = set(word for line in page for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

Przypisy