Dynamika otwartych klas: Ruby vs. Python
Opublikowane przez Jarosław Zabiełło
Jedną z cech języka Ruby są tzw. otwarte klasy. Temat ten wywołuje mieszane uczucia i jest np. przedmiotem krytyki ze strony miłośników Pythona. Najczęściej jednak ta krytyka idzie w parze z nieznajomością Rubiego.
Np. w jednym z blogów spotkałem taki zarzut: zmiana działania klasy na której oparte są liczby całkowite może prowadzić do nieprzewidywalności w kodzie. Np.class Fixnum
def +(num)
5
end
end
print 2+2 # => 5 zamiast 4!No i faktycznie, zmieniliśmy metodę operowania na liczbach całkowitych i Ruby nas oszukuje. Hmm, czy podobnej sytuacji nie ma w Pythonie? Ależ jest! Tylko że nie dotyczy podstawowych typów wbudowanych. Problem jednak jest ten sam. Ba, jest gorszy, co zaraz udowodnię. Ale wpierw prześledźmy różne scenariusze.
Podmiana metody w klasie
Załóżmy, że budujemy sobie klasę:class Hello(object):
def msg(self, msg):
return "Hello " + msgclass Hello(object):
def msg(self, msg):
return "Piss off " + msgTeraz co się stanie jak moduł Dyzia zaimportuję do swojej przestrzeni nazw? Bez żadnego ostrzeżenia, klasa Dyzia wymieni moją klasę. Program może się nie posypie ale, klient uzyska zgoła odmienne przywitanie. :) Oczywiście może być gorzej, bo klasa Dyzia kompletnie usunie moją klasę co zaowocuje usunięciem wszystkich innych metod których się pracowicie natworzyłem.
Podobna sytuacja w Ruby nie owocuje kompletnym usunięciem wszystkich wcześniej stworzonych metod. Podmieniona zostanie tylko ta jedna, inna metoda. Cała reszta zostanie bez zmian.
Zabezpieczyć przed zmianą
Jak się zabezpieczyć przed modyfikacją klas? W Pythonie jest prosta odpowiedź: nie da się, bo Python nie posiada takich mechanizmów.
A co z Ruby? Okazuje się, że jest tu całkiem nieźle. Można dowolny obiekt (z klasą włącznie, bo klasa to też obiekt) zamrozić blokując możliwość jakiejkolwiek zmiany. Wystarczy więc, że wpiszęHello.freezeObjectSpace.each_object { |o| o.freeze if o.is_a? Class }Zamrożenie obiektu jest nieodwracalne aż do końca działania programu. Nie da się żadnym sposobem za pomocą odpowiedniej komendy odmrozić raz zamrożony obiekt. Można co najwyżej sprawdzić czy już nie jest zamrożony (print obiekt.frozen?)
System Hooks
No dobrze, ale co w sytuacji kiedy nie chcemy zamrażać obiektu ale też nie chcemy aby gdzieś ktoś nam w nim coś zmienił bez wcześniejszego powiadomienia nas o tym? Tutaj z pomocą przychodzi coś co nazywa się System Hooks. W Ruby można bowiem świetnie śledzić co się dzieje z naszą klasą. Ruby potrafi wyłapywać następujące zdarzenia:
- dodanie do klasy nowej metody dostępnej dla instancji klasy (Module#method_added)
- usunięcie metody instancji z klasy (Module#method_removed)
- wywołanie metody instancji, która nie istnieje (Module#method_undefined)
- dodanie metody do singletona (Kernel.singleton_method_added)
- usunięcie metody z singletona (Kernel.singleton_method_removed)
- wywołanie nie istniejącej metody dla singletona (Kernel.singleton_method_undefineded)
- stworzenie klasy potomnej (Class#inherited)
- dodanie metod do klasy na drodze mixingu z modułów (Module#extend_object)
def info(msg)
puts msg
end
class Hello
def msg(msg)
"Hello " + msg
end
def self.method_added(name)
info "Dodano metode #{name}"
end
def self.inherited(name)
info "Jakas klasa #{name} chce przeciazyc klase Hello"
end
end
class Druga < Hello
end
class Hello
def nowy
end
endJakas klasa Druga chce przeciazyc klase Hello
Dodano metode nowyclass String
def pluralize
self + "s"
end
def singularize
self[0...-1]
end
end
puts "word".pluralize # => words
puts "books".singularize # => book
class Integer
def kilobytes
self * 1024
end
end
puts 2.kilobytes # => 2048Python (i żaden inny ze znanych mi języków) takich możliwości nie ma. A odnośnie tego, że Python nie pozwala zmienić definicji stringa czy integera (ogólnie typów wbudowanych) to mała to pociecha, skoro możne zmienić dowolnie inną klasę i to bez żadnego mechanizmu kontroli.



Jedną rzecz można krócej: ObjectSpace.each_object(Class){|o|o.freeze}
ale i tak to mrozi Object, więc:
class Whatever
end
can’t modify frozen class (TypeError)
wyjściem jest: ObjectSpace.each_object(Class){|o|o.freeze unless o==Object}
tylko jaki to ma cel.. ;)
Na grupie pl.com.lang.python toczy się dyskusja na temat tego artykułu.
A ja to dodałem ten artykuł wraz z artykułem adwersarza do polskiej edycji Reddita.
A da się tak zrobić, żeby np. klasa String miała dodatkową tylko w obrębie danego pliku ?
Chodzi mi o to, że tworzę sobie jakąś bibliotekę i rozszerzam klasę String, ale nie chcę żeby u kogoś kto ją zaimportuje również pokazały się nowe metody, bo są dla niego nieprzydatne.
Mozesz stworzyc klase dziedziczaca z np String i ja rozszerzyc :)