Klasy bardziej meta

2011-11-08 21:57

W językach ze skrzywieniem obiektowym klas używa się często i gęsto. Nieco inaczej jest z innym, o wiele mniej znanym pojęciem: metaklasami. Przedrostek meta- sugeruje tu od razu nieco wyższy poziom abstrakcji, co jest generalnie słusznym podejrzeniem. Dokładne znaczenie kryjące się za tym terminem w pewnym stopniu zależy od języka programowania, lecz w każdym przypadku chodzi o narzędzie związane z manipulowaniem samymi klasami. Zupełnie intuicyjnie jest to więc “jeden poziom meta więcej” :)
Jako nieco bardziej zaawansowana funkcjonalność, metaklas nie wykorzystuje się codziennie. Tym bardziej jednak warto wiedzieć przynajmniej z grubsza, jak działają, aby poprawnie rozpoznawać sytuacje, w których są one użyteczne. I dlatego właśnie dzisiaj przyjrzymy się metaklasom w trzech wariantach, specyficznych dla popularnych języków programowania.

Pierwszy z nich dotyczy systemów refleksji, czyli przydatnego mechanizmu językowego, pozwalającego programowi na wgląd w swoją wewnętrzną strukturę. W językach obiektowych oznacza to przede wszystkim możliwość posługiwania się klasami na bardziej dynamicznym poziomie. Są one wtedy reprezentowane przez obiekty, i właśnie te reprezentacje – używane poprzez moduły refleksji – nazywa się czasem metaklasami.
W wielu językach wyglądają one podobnie. Java ma na przykład klasę java.lang.Class, zaś C# – System.Type. Zyskując dostęp do instancji tychże klas (np. poprzez Class.forName albo Type.GetType) otrzymujemy możliwość wykonywania operacji na klasach, które one reprezentują. I tak możliwe jest chociażby tworzenie ich obiektów, wywoływanie metod, dostęp do pól. Dzięki temu możemy na przykład w (miarę) łatwy sposób zaimplementować proste rozwiązania wspierające pluginy, tj. dynamicznie ładowane wtyczki do naszych aplikacji.

Drugie znaczenie pojęcia metaklasy opisuje referencje do klas. Ideą jest tu traktowanie klas jako wartości pierwszego rodzaju (first-class value), które można przypisywać do zmiennych i w uogólniony sposób przekazywać między różnymi miejscami w kodzie. Specjalne zmienne, do których możemy “przypisywać” klasy są w co najmniej jednym języku nazywane właśnie metaklasami.
Przydatność tego typu rozwiązania zależy głównie od tego, jak wielkimi jesteśmy fanami wzorców typu fabryka abstrakcyjna :) Być może podstawową funkcjonalnością takich metaklas jest bowiem abstrakcja procesu tworzenia (lub ogólniej: pozyskiwania z zewnątrz) nowych obiektów. Pozostałe przypadki użycia metaklas są pokrywane zwykle przez szablony (C++), typy generyczne (C#, Java, itp.) lub podobne mechanizmy tworzenia kodu, który w pewien określony sposób jest niezależny od dokładnego typu obiektów, którymi operuje.

Czas wreszcie na trzeci wariant metaklas. Jest on zdecydowanie najciekawszy, ale też najbardziej skomplikowany i w pewien sposób wysublimowany – mimo swojej niewątpliwej użyteczności. Mam tutaj na myśli metaklasy w Pythonie.

Metaklasy są zaawansowaną magią, którą 99% użytkowników może sobie nie zaprzątać głowy. Jeśli zastanawiasz się, czy są ci potrzebne, to na pewno nie są (bowiem ci którzy rzeczywiście ich potrzebują, po prostu o tym wiedzą i nie potrzebują tłumaczeń).

Tim Peters, guru Pythona

W rzeczywistości nie jest na szczęście aż tak źle – nie wspominając już o tym, że dosłowne branie tego cytatu skutkuje pewnego rodzaju błędnym kołem. Tak naprawdę pythonowskie metaklasy są koncepcyjnie w miarę proste, gdyż analogia kryjąca się za prefiksem meta- jest w ich przypadku całkowicie trafna. Tak jak klasy są kategoriami obiektów, tak metaklasy są kategoriami samych klas. Patrząc na to od drugiej strony: każda klasa w Pythonie jest instancją metaklasy, podobnie jak każdy “normalny” obiekt jest instancją jakiejś “zwykłej” klasy.

Podstawową, wbudowaną w język metaklasą jest type. Każda klasa jest wobec tego jej egzemplarzem, co dotyczy w równym stopniu rozbudowanych klas z rzeczywistych programów, jak i poniższego niezbyt mądrego przykładu:

  1. class Foo(object):
  2.     pass
  3.  
  4. foo = Foo()

Wiemy, że obiekt nazwany foo jest instancją klasy Foo, bo używamy jej bezpośrednio w celu stworzenia tego obiektu. Tym, czego nie da się zauważyć podobnie łatwo, jest fakt, iż samo Foo – klasa – jest instancją typu (metaklasy) type:

  1. >>> type(foo)
  2. <class '__main__.Foo'>
  3. >>> type(Foo)
  4. <type 'type'>

Nie widzimy tego od razu, gdyż relacja jest ukryta za składnią deklaracji class. Okazuje się jednak, że ten mechanizm syntaktyczny (cukierek? :]) nie jest wcale potrzebny. Możliwe jest stworzenie klasy w sposób bardziej bezpośredni, posługując się jawnie metaklasą type:

  1. >>> Foo = type('Foo', (object,), {})
  2. >>> Foo
  3. <class '__main__.Foo'>

Argumenty jej konstruktora to kolejno: nazwa klasy, klasy bazowe (tutaj tylko jedna: object) oraz słownik atrybutów, takich jak funkcje (które stają się metodami) i zmienne klasowe. Ponieważ wywołujemy konstruktor, rezultatem jest oczywiście obiekt – instancja metaklasy type – czyli zwykła klasa.

To wszystko bardzo ładne…

…ale do czego to właściwie służy? :) Sama metaklasa type rzeczywiście nie jest specjalnie interesująca – dopóki nie zdamy sobie sprawy, że można po niej dziedziczyć, tworząc własne metaklasy. Co więcej, możliwe jest następnie ich “doczepienie” do zwykłych klas. Pozwala to wówczas na przechwycenie procesu tworzenia takiej klasy i dokonanie na niej dowolnych operacji. Jest to możliwe, ponieważ – jak widać na przykładzie powyżej – metaklasy mają dostęp do wszystkich niezbędnych informacji o tworzonej klasie: jej nazwy, klas bazowych oraz słownika atrybutów.

Prawdopodobnie najbardziej ewidentnym i jednocześnie najbardziej znaczącym (bo najpopularniejszym) przykładem zastosowania metaklas są biblioteki upraszczające dostęp do zewnętrznych baz danych. Dotyczy to na przykład baz relacyjnych, których tabele mogą być w ten sposób mapowane na klasy, a wiersze na obiekty tych klas. Rozwiązanie to znane jest jako ORM (Object-Relational Mapping) i choć zdania o nim są podzielone, to trudno oprzeć się pięknu jego eleganckiej implementacji – a więc chociażby takiej, jak poniższa:

  1. class User(Base):
  2.     __tablename__ = 'users'
  3.  
  4.     id = Column("id", Integer, primary = True)
  5.     created_at = Column(DateTime, default = func.now())
  6.     first_name = Column(String(64))
  7.     last_name = Column(String(256))
  8.     email = Column(String(256), required = False)
  9.     # itd.

Niby deklarujemy tu zwykłą klasę, ale w rzeczywistości każdy obiekt User to przemyślnie skonstruowane mapowanie na jakiś (niekoniecznie istniejący) wiersz tabeli w bazie danych. Dzięki odpowiedniej metaklasie (zadeklarowanej w klasie bazowej Base) automatycznie dokonuje się interpretacja wszystkich atrybutów (id, first_name, itd.) jako pól w tejże tabeli. W rezultacie możemy operować na obiektach zamiast na wierszach, co akurat w przypadku jakiegoś rodzaju użytkowników ma całkiem głęboki sens.

Podobne rozwiązania określa się czasami jako Domain-Specific Languages (DSL), a Python jest jednym z języków (obok np. Ruby, Haskella czy C++), w którym DSL-e wychodzą całkiem zgrabnie i ekspresyjnie. Jednym z powodów jest właśnie możliwość kontrolowania procesu tworzenia klas, dzięki czemu możemy do nich zaaplikować naprawdę zaawansowaną “magię”… jeśli oczywiście wiemy, że powinniśmy to zrobić ;-)

Tags: , , , , ,
Author: Xion, posted under Programming »


One comment for post “Klasy bardziej meta”.
  1. Tomasz Dąbrowski:
    November 8th, 2011 o 23:50

    %s jest zaawansowaną magią, którą 99% użytkowników może sobie nie zaprzątać głowy. Jeśli zastanawiasz się, czy są ci potrzebne, to na pewno nie są (bowiem ci którzy rzeczywiście ich potrzebują, po prostu o tym wiedzą i nie potrzebują tłumaczeń).

    To się stosuje do bardzo dużej ilości zagadnień. :)

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.