Wrócimy na chwilę do przeszłości. Za czasów, kiedy nie można było dziedziczyć wbudowanych typów danych np. słownika, powstawały tzw. klasy opakowujące, które pełniły te same funkcję, co typy wbudowane, ale można je było dziedziczyć. Klasą opakowującą dla słownika była klasa UserDict, która nadal jest dostępna wraz z nowymi wersjami Pythona. Przyjrzenie się implementacji tej klasy może być dla nas cenną lekcją. Zatem zajrzyjmy do kodu źródłowego klasy UserDict, który znajdują się w module UserDict. Moduł ten z kolei jest przechowywany w katalogu lib instalacji Pythona, a pełna nazwa pliku to UserDict.py (nazwa modułu z rozszerzeniem .py).
W IDE ActivePython na Windowsie możemy szybko otworzyć dowolny moduł, który znajduje się w ścieżce do bibliotek, gdy wybierzemy File->Locate... (Ctrl-L).
Klasa UserDict nie dziedziczy nic z innych klas. Jednak nie patrzmy się na to, pamiętajmy, żeby zawsze dziedziczyć z object (lub innego wbudowanego typu), bo wtedy mamy dostęp do dodatkowych możliwości, które dają nam klasy w nowym stylu.
Jak pamiętamy, metoda __init__ jest wywoływana bezpośrednio po utworzeniu instancji klasy. Przy tworzeniu instancji klasy UserDict możemy zdefiniować początkowe wartości, poprzez przekazanie słownika w argumencie dict.
W Pythonie możemy tworzyć atrybuty danych (zwane polami w Javie i PowerBuilderze). Atrybuty to kawałki danych przechowywane w konkretnej instancji klasy (moglibyśmy je nazwać atrybutami instancji). W tym przypadku każda instancja klasy UserDict będzie posiadać atrybut data. Aby odwołać się do tego pola z kodu spoza klasy, dodajemy z przodu nazwę instancji np. instancja.data; robimy to w identyczny sposób, jak odwołujemy się do funkcji poprzez nazwę modułu, w którym ta funkcja się znajduje. Aby odwołać się do atrybutu danych z wnętrza klasy, używamy self. Zazwyczaj wszystkie atrybuty są inicjalizowane sensownymi wartościami już w metodzie __init__. Jednak nie jest to wymagane, gdyż atrybuty, podobnie jak zmienne lokalne, są tworzone, gdy po raz pierwszy przypisze się do nich jakąś wartość.
Metoda update powiela zachowanie metody słownika: kopiuje wszystkie klucze i wartości z jednego słownika do drugiego. Metoda ta nie czyści słownika docelowego (tego, z którego wywołaliśmy metodę), ale jeśli były tam już jakieś klucze, to zostaną one nadpisane tymi, które są w słowniku źródłowym; pozostałe klucze nie zmienią się. Myślmy o update jak o funkcji łączenia, nie kopiowania.
Z tej składni nie korzystaliśmy jeszcze w tej książce. Jest to instrukcja if, ale zamiast wciętego bloku, który rozpoczynałby się w następnej linii, korzystamy tu z instrukcji, która znajduje się w jednej linii, zaraz za dwukropkiem. Jest to całkowicie poprawna, skrótowa składnia, której możemy używać, jeśli mamy tylko jedną instrukcję w bloku (tak jak pojedyncza instrukcja bez klamer w C++). Możemy albo skorzystać z tej skrótowej składni, albo tworzyć wcięte bloki, jednak nie możemy ich ze sobą łączyć w odniesieniu do tego samego bloku kodu.
Java i Powerbuilder mogą przeciążać funkcje mające różne listy argumentów, na przykład klasa może mieć kilka metod z taką samą nazwą, ale z różną liczbą argumentów lub z argumentami różnych typów. Inne języki (na przykład PL/SQL) obsługują nawet przeciążanie funkcji, które różnią się jedynie nazwą argumentu np. jedna klasa może mieć kilka metod z tą samą nazwą, tą samą liczbą argumentów o tych samych typów, ale inaczej nazwanych. Python nie ma żadnej z tych możliwości, nie ma tu w ogóle przeciążania funkcji. Metody są jednoznacznie definiowane przez ich nazwy i w danej klasie może być tylko jedna metoda o danej nazwie. Jeśli więc mamy w jakiejś klasie potomnej metodę __init__, to zawsze zasłoni ona metodę __init__ klasy rodzicielskiej, nawet jeśli klasa pochodna definiuje ją z innymi argumentami. Ta uwaga stosuje się do wszystkich metod.
Guido, pierwszy twórca Pythona, tak wyjaśnia zasłanianie funkcji: "Klasy pochodne mogą zasłonić metody klas bazowych. Ponieważ metody nie mają żadnych specjalnych przywilejów, kiedy wywołujemy inne metody tego samego obiektu, może okazać się, że metoda klasy bazowej wywołująca inną metodę zdefiniowaną w tej samej klasie bazowej wywołuje właściwie metodę klasy pochodnej, która ją zasłania. (Dla programistów C++: wszystkie metody w Pythonie zachowują się tak, jakby były wirtualne.)" Jeśli dla Ciebie nie ma to sensu, możesz to zignorować. Po prostu warto było o tym wspomnieć.
Uwaga!
Zawsze przypisujmy wartości początkowe wszystkim zmiennym obiektu w jego metodzie __init__. Oszczędzi to godzin debugowania w poszukiwaniu wyjątków AtributeError, które są spowodowane odwołaniami do niezainicjalizowanych (czyli nieistniejących) atrybutów.
clear jest normalną metodą klasy; jest dostępna publicznie i może być wołana przez kogokolwiek w dowolnej chwili. Zauważmy, że w clear, jak we wszystkich metodach klas, pierwszym argumentem jest self. (Pamiętajmy, że nie dodajemy self, gdy wywołujemy metodę; Python robi to za nas.) Zwróćmy uwagę na podstawową cechę tej klasy opakowującej: przechowuje ona prawdziwy słownik w atrybucie data i definiuje wszystkie metody wbudowanego słownika, a w każdej z tych metod zwraca wynik identyczny do odpowiedniej metody słownika. (Gdybyśmy zapomnieli, metoda słownika clear czyści cały słownik kasując jego wszystkie klucze i wartości.)
Metoda słownika o nazwie copy zwraca nowy słownik, który jest dokładną kopią oryginału (mający takie same pary klucz-wartość). Natomiast klasa UserDict nie może po prostu wywołać self.data.copy, ponieważ ta metoda zwraca wbudowany słownik, a my chcemy zwrócić nową instancję klasy tej samej klasy, jaką ma self.
Używamy atrybutu __class__, żeby sprawdzić, czy self jest obiektem klasy UserDict; jeśli tak, to jesteśmy w domu, bo wiemy, jak zrobić kopię UserDict: tworzymy nowy obiekt UserDict i przekazujemy mu słownik wyciągnięty z self.data, a wtedy możemy od razu zwrócić nowy obiekt UserDict nie wykonując nawet instrukcji import copy z następnej linii.
Jeśli self.__class__ nie jest UserDict-em, to self musi być jakąś podklasą UserDict-a, a w takim przypadku życie wymaga użycia pewnych trików. UserDict nie wie, jak utworzyć dokładną kopię jednego ze swoich potomków. W tym celu możemy np. znając atrybuty zdefiniowane w podklasie, wykonać na nich pętlę, podczas której kopiujemy każdy z tych atrybutów. Na szczęście istnieje moduł, który wykonuje dokładnie to samo, nazywa się on copy. Nie będziemy się tutaj wdawać w szczegóły (choć jest to wypaśny moduł, jeśli się w niego trochę wgłębimy). Wystarczy wiedzieć, że copy potrafi kopiować dowolne obiekty, a tu widzimy, jak możemy z niego skorzystać.
Pozostałe metody są bezpośrednimi przekierowaniami, które wywołują wbudowane metody z self.data.
Uwaga!
Od Pythona 2.2 nie korzystamy z klasy UserDict, ponieważ od tej wersji możemy już dziedziczyć z wbudowanych typów danych.