Dynamika otwartych klas: Ruby vs. Python

Opublikowane przez Jarosław Zabiełło Sat, 04 Mar 2006 05:36:00 GMT

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 " + msg
Miło i przyjemnie. Teraz mamy duży kod i sporo modułów z których co chwilę coś importujemy i na dodatek, pracujemy w kilka osób. Dyzio w jednym ze swych modułów stworzył klase o tej samej nazwie, ale o innej treści. Np.
class Hello(object):
  def msg(self, msg):
    return "Piss off " + msg

Teraz 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.freeze
Od tego momentu nikt nie będzie w stanie nic zmienić w mojej definicji. Można jedną instrukcją zablokować wszystkie klasy wbudowane jak ktoś bardzo tego chce:
ObjectSpace.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)
Przykład:
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
end
Wynik:
Jakas klasa Druga chce przeciazyc klase Hello
Dodano metode nowy
Nie jest więc aż tak strasznie jak to co niektórzy trąbią. To prawda, że w Ruby można dodać metody do absolutnie każdej klasy (z wbudowanymi włącznie). Prawdą jest jednak także to, że można ten mechanizm w miarę kontrolować, śledzić, lub nawet zablokować. Zmiana metod klas wbudowanych pozwala na pisanie bardzo czytelnego kodu. Np. Ruby on Rails z tego sporo korzysta. Mogę samemu b. łatwo sobie zaimplementować coś podobnego.
class 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 # => 2048

Python (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.

Posted in ,  | Tagi ,  | 5 comments

Comments

  1. Avatar lopex powiedział about 3 hours later:

    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.. ;)

  2. Avatar Jarosław Zabiełło powiedział about 8 hours later:

    Na grupie pl.com.lang.python toczy się dyskusja na temat tego artykułu.

  3. Avatar Piotr powiedział 1 day later:

    A ja to dodałem ten artykuł wraz z artykułem adwersarza do polskiej edycji Reddita.

  4. Avatar cysiek10 powiedział about 1 year later:

    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.

  5. Avatar p powiedział about 1 year later:

    Mozesz stworzyc klase dziedziczaca z np String i ja rozszerzyc :)

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz