Ruby, rozszerzanie klas i docstringi

Posted by Jarosław Zabiełło Thu, 13 Apr 2006 19:22:00 GMT

W jednym z wcześniejszych tekstów opisywałem zalety docstringów – wbudowanego mechanizmu dokumentacji w Pythonie.

W języku Ruby nie ma takiego mechanizmu. Jednak Ruby posiada unikalną cechę która pozwala go rozszerzać bez konieczności sięgania do języka C. Tą cechą są otwarte klasy.

class X
  def hello
    "Witaj"
  end
end

x = X.new
puts x.hello # => Witaj

class X
  def bye
    "Żegnaj"
  end
end

x = X.new
puts x.hello # => Witaj
puts x.bye # => Żegnaj

Coś takiego jest w ogóle nie do pomyślenia w językach tak mało dynamicznych jak Java czy C++ gdzie definicje klas są ustalane na poziomie kompilacji. W Ruby klasy są obiektami i są tworzone w momencie uruchomienia programu co daje nam w efekcie znacznie większy dynamizm i siłę ekspresji kodu.

Z kolei próba napisania czegoś podobnego w innych językach dynamicznych prowadzi także do zgoła kompletnie odmiennych zachowań. W przypadku PHP 5.1.2 poniższy kod wygeneruje błąd składni:

<?php
class X {
    function hello() {
        return "Witaj";
    }
}

$x = new X();
print $x->hello() # => Witaj

class X { # => Parse error: syntax error, unexpected T_CLASS
    function bye() {
        return "Żegnaj";
    }
}
?>

Zaś w Pythonie poznikają nam definicje poprzednich metod (ale wcześniej zdefiniowane instancje klasy nie ulegną zmianie).

class X(object):
    def hello(self):
        return "Witaj"

x = X()
print x.hello() # => Witaj

class X(object):
    def bye(self):
        return "Żegnaj"

print x.hello() # => Witaj

x = X()
print x.bye() # => Żegnaj
print x.hello() # => AttributeError: X instance has no attribute 'hello'

No dobrze, wróćmy do próby zaimplementowanie w Rubym czegoś podobnego do pythonowych docstringów. Załóżmy, że chcielibyśmy korzystać z następującej składni (poniższy kod jest oparty na PixAxe2, s.389):

class Przyklad
  doc "To jest przykład doctringu"
  # reszta kodu 
end

W związku z tym, że chcielibyśmy aby taka składnia działała dla każdego modułu i klasy, kod implementujący taki mechanizm powinniśmy zdefiniować w klasie Module.

class Module
  @@docs = {}
  def doc(s)
    @@docs[self.name] = s.gsub(/^\s+/, '')
  end
  def Module::doc(aClass)
    aClass = aClass.name if aClass.class <= Module
    @@docs[aClass] || 'Nie ma dokumentacji'
  end
end

Oraz przykład użycia:

class A
end

class B
  doc "Przykład dokumentacji"
end

module C
  doc <<-edoc
    Dokumentacja
    wielowierszowa
  edoc
end

puts Module::doc(A) # => Nie ma dokumentacji
puts Module::doc(B) # => Przykład dokumentacji
puts Module::doc(C) #=> Dokumentacja\nwielowierszowa

Posted in  | Tags  | 4 comments

Ruby vs. Python - operacje na plikach

Posted by Jarosław Zabiełło Tue, 11 Apr 2006 19:47:00 GMT

Ostatnio miałem potrzebę napisania skryptu kopiującego całe pliki z całego drzewa folderów. Problem w tym, że chciałem kopiować tylko pliki, które nie miały ustawionych atrybutów tylko do odczytu. I to musiało być zrobione pod Windowsami. Zwykle jestem przyzwyczajony że tego typu zadania to piszę w Pythonie w kilka minut. Nie spodziewałem się że tu będzie inaczej….

Okazało się że Python (2.4.3) nie ma porządnie zaimplementowanych metod sprawdzania atrybutów pliku. Na próżno szukałem w manualu, Python Cookbook , internecie.

Zgodnie z prawami Murphego rozwiązanie znalazłem jak już było niepotrzebne (w międzyczasie zdążyłem napisać to w Ruby).

>>> os.stat(r'c:\tmp\test1')[stat.ST_MODE] & stat.S_IWRITE
128
>>> os.stat(r'c:\tmp\test2')[stat.ST_MODE] & stat.S_IWRITE
0

Wygląda to raczej paskudnie na tle analogicznego kodu Rubiego:

>>> File.writable?('c:\tmp\test1')
true
>>> File.writable?('c:\tmp\test2')
false

Może to szczególny przypadek, ale jakoś nie sprawdziła się w tym wypadku prostota Pythona. Metody Rubiego w tym temacie są znacznie bardziej intuicyjne. Zresztą zobaczmy poniższy przykład z manuala Pythona:

import os, sys
from stat import *
def walktree(top, callback):
    for f in os.listdir(top):
        pathname = os.path.join(top, f)
        mode = os.stat(pathname)[ST_MODE]
        if S_ISDIR(mode):
            walktree(pathname, callback)
        elif S_ISREG(mode):
            callback(pathname)
        else:
            print 'Skipping %s' % pathname
def visitfile(file):
    print 'visiting', file
if __name__ == '__main__':
    walktree(sys.argv[1], visitfile)

Ten sam kod w języku Ruby wygląda tak:

require 'find'
if $0 == __FILE__
  Find.find(ARGV[0]) do |f|
    if File.file?(f)
      puts 'visiting ' + f 
    elsif not File.directory?(f)
      puts 'skipping ' + f
    end
  end
end

I kto tu powie, że Ruby jest mniej czytelny od Pythona?

Posted in ,  | Tags ,  | 8 comments

RadRails najlepsze narzędzie do RoR?

Posted by Jarosław Zabiełło Thu, 23 Mar 2006 05:53:00 GMT

RadRails to wysokiej klasy edytor klasy IDE dedykowany do pracy z Ruby on Rails. Jest on przeróbką kodu słynnego Eclipse. RadRails otrzymał nagrodę najlepszego narzędzia developerskiego pod Eclipse: “Eclipse Best Developer Tool”.

Najnowsza wersja RadRails (0.6.1) wprowadza kolejne udogodnienia w pracy, więcej generatorów, dodano obsługę unit testów, lepsze uzupełnianie kodu (hmm, przypomina coraz bardziej TextMate).

Dzięki RadRails, nie trzeba w ogóle wchodzić do konsoli aby uruchamiać skrypty Railsów. RadRails generuje (lub usuwa kontrolery), modele, restartuje webrick’a (wbudowany serwer www dedykowany do pracy developerskiej), ma wbudowany podgląd przeglądarki, . W sumie wszystko w jednym.

Posted in ,  | Tags , ,  | 9 comments

Mechanizmy bezpieczeństwa - Ruby vs Python

Posted by Jarosław Zabiełło Fri, 10 Mar 2006 21:37:00 GMT

W języku Ruby (mimo jego wielkiego dynamizmu) kwestia bezpieczeństwa została potraktowana poważnie. Ruby posiada różne mechanizmy umożliwiające kontrolę nad bezpieczeństwem kodu. Generalnie są to 3 różne mechanizmy.

  • Zakresy metod: private, protected i public.
  • Możliwość nieodwracalnego zamrożenia dowolnego obiektu.
  • Możliwość nieodwracalnego ustawiania poziomów bezpieczeństwa.

Zakresy private, protected, public

  • Metody publiczne moga być wywoływane przez kogokolwiek. Domyślnie wszystkie metody są publiczne (poza konstruktorem initialize który zawsze jest prywatny)
  • Metody chronione (protected) mogą być wywoływane tylko przez obiekty w tej samej klasie i klasach potomnych.
  • Metody prywatne mogą być wywoływane tylko w kontekście bieżącego obiektu. Nie można wywoływać metod prywatnych innego obiektu.

Np.:

class C
  public
  def publiczna
    p "Metoda publiczna"
  end  
  def get_chroniona
    chroniona
  end
  def get_prywatna
    prywatna
  end
  protected
  def chroniona
   p "Metoda chroniona"
  end 
  private
  def prywatna
    p "Metoda prywatna"
  end 
end

c = C.new
c.publiczna # => Metoda publiczna
c.get_chroniona  # => Metoda chroniona
c.get_prywatna  # => Metoda prywatna
c.chroniona # => protected method `chroniona' called for #<C:0x28e6300> (NoMethodError)
c.prywatna # => private method `prywatna' called for #<C:0x28e62e8> (NoMethodError)

Dla porównania, w Pythonie wszystkie metody są publiczne. Efekt metod prywatnych i chronionych uzyskuje się za pomocą konwencji. Metody prywatne mają prefiks dwóch underscorów, a chronionie – jeden underscore. Oczywiście nadal są one formalnie publicznie ale dostęp do nich (szczególnie do metod “prywatnych” jest utrudniony)

class C(object):
  def publiczna(self):
    print "Metoda publiczna"
  def get_chroniona(self):
    self._chroniona()
  def get_prywatna(self):
    self.__prywatna()
  def _chroniona(self):
   print "Metoda chroniona"
  def __prywatna(self):
    print "Metoda prywatna"

c = C()
c.publiczna()
c.get_chroniona() # => Metoda chroniona
c.get_prywatna() # => Metoda prywatna
c._chroniona() # => Metoda chroniona
c._C__prywatna() # => Metoda prywatna
c.__prywatna() # => AttributeError: 'C' object has no attribute '__prywatna'

Python wychodzi z innych przesłanek niż Ruby. Zakłada, że “jesteśmy dorośli i wiemy co chcemy robić”. Tzn. utrudnia popełnienie pomyłki przypadkowego dostępu do metody “prywatnej” za pomocą zmodyfikowania nazwy która da dostęp do takiej metody. Jednak jak ktoś, chce to Python nie będzie utrudniał. Można dostać się do każdej metody. Jak zobaczymy dalej (przy omawianiu poziomów bezpieczeństwa Rubiego), ta zasada spowodowała daleko idące konsekwencje dla kwestii bezpieczeństwa kodu Pythona.

Zamrażanie obiektów

Jeśli chodzi o kwestie zamrażania obiektów, to opisuję to w artykule o dynamice klas Python nie posiada jednak aż tak eleganckiego mechanizmu co Ruby. Da się co prawda przyblokować zmiany w obiekcie, ale trzeba sięgnąć po bardziej zaawansowane mechanizmy:

class ImmutableClass(type):
  def __setattr__(cls, name, value):
    raise AttributeError, "'%s' is immutable" % cls.__name__

class C(object):
  def hello(self):
    return 'Hello World!'
  __metaclass__ = ImmutableClass # blokada zmiany

def hello2(self): return 'Blah!'

c = C()
print c.hello() # => Hello World!
C.hello = hello2 #=> AttributeError: 'C' is immutable
print c.hello() # => Hello World!

Poziomy bezpieczeństwa

Jedną z ciekawych cech języka Ruby jest możliwość uruchamiania kodu na różnym poziomie bezpieczeństwa. Załóżmy np, że chcemy stworzyć (w najprostszy możliwy sposób) kalkulator który następnie udostępnimy na stronie internetowej. Przykładowy kod (na podst. PixAxe2) mógłby wyglądać np. tak:
require 'cgi'
cgi = CGI.new('html4')
# pobierz warto z pola formularza 'wyrazenie'
expr = cgi['wyrazenie'].to_s
begin
  result = eval(expr)
rescue Exception => detail
  # obsłuż złe wyrażenie
end
# wyświetl wynik dla użytkownika
Wszystko pięknie, ale co się stanie jak jakiś złośliwy użytkownik wpisze:
system("rm *")

Cały nasz kod zostanie skasowany! Na szczęście Ruby udostępnia mechanizm uruchamiania kodu w specjalnym trybie tak, aby kod nie był w stanie zbytnio rozrabiać. Poziom Rubinowej paranoi bezpieczeństwa określa zmienna globalna $SAFE. Może ona przyjmować następujące wartości:

  • 0 Brak mechanizmów zabezpieczeń. Wartość domyślna.
  • >= 1 Ruby zabrania uzywanie danych w potencjalnie niebezpiecznych operacjach
  • >= 2 Ruby zabrania ładowanie innych skryptów z dostępnych globalnie miejsc
  • >= 3 Wszystkie nowo stworzone obiekty są oznakowane jako niebezpieczne
  • >= 4 Ruby dzieli uruchamiany program tak, że obiekty nie oznaczone jako niebezpieczne również nie mogą być modyfikowane.

Domyślna, zerowa wartość $SAFE jest wystarczająca w większości przypadków. Jednak, gdy skrypt Rubiego jest uruchamiany z ustawionym atrybutem suid lub setgid (co pod uniksem umożliwia uruchomienie skryptu w kontekście innego użytkownika) lub, gdy ruby pracuje w trybie modułu apache’a (mod_ruby), to poziom bezpieczeństwa jest automatycznie podnoszony do 1. Poziom bezpieczeństwa można także ustawić za pomocą parametru -T z wiersza poleceń lub poprzez przypisanie zmiennej $SAFE innej wartości. Przy czym, nie jest możliwe zmniejszenie raz ustawionego poziomu bezpieczeństwa aż do końca działania skryptu.

Bieżąca wartość $SAFE jest dziedziczona przez wszystkie nowo tworzone wątki. Jednakże wewnątrz każdego wątka, wartość zmiennej globalnej $SAFE może być zmieniana bez wpływu na tą wartość w innym wątku. Ta cecha pozwala na budowanie bezpiecznych piaskownic (sandbox), obszarów w których można odpalać potencjalnie niebezpieczny kod bez ryzyka, że wpłynie on na resztę aplikacji czy systemu. Np.
File.new(filename, 'w').write('niebezpieczny kod')
Thread.start do
  $SAFE = 4
  load(filename, true)
end

Dla porównania, Python w ogóle nie posiada takich mechanizmów. Na domiar złego, okazało się, że (obecny we wcześniejszych wersjach Pythona) moduł Bastion okazał się dziurawy Został usunięty w biblioteki podstawowej i jak na razie Python nie posiada już nic w tym temacie do zaoferowania w zamian.

Posted in ,  | Tags ,  | 3 comments

Dynamika otwartych klas: Ruby vs. Python

Posted by 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 ,  | Tags ,  | 5 comments

Edytory dla Pythona, Ruby, PHP

Posted by Jarosław Zabiełło Sat, 11 Feb 2006 23:17:00 GMT

Osoby zaczynające pracę z językiem Python, Ruby czy PHP często pytają się na grupach dyskusyjnych na temat edytorów zalecanych do pracy. Istnieją dwie szkoły, ci którym zależy na minimalnych wymaganiach pamięci i procesora oraz ci, którzy chcieliby aby edytor nie tylko kolorował składnię, ale także podpowiadał kod, uzupełniał składnię, miał wbudowany debugger itp.

Edytory tekstowe (nie IDE):

  • SciTE – łatwy w obsłudze, kolorowanie b. wielu języków
  • Vim – powszechny na każdym linuksie, jest wersja dla win32. B. szybki, doskonały do obsługi wielkich plików, niezbyt intuicyjna obsługa, np. kto by wpadł na pomysł aby wyjść z edytora za pomocą kombinacji: Esc + :q! (klawisz Escape, potem znak cudzysłowa, małe q i znak wykrzyknika)
  • Notepad++ – prosta, intuicyjna obsługa, po instalacji podmienia dla Internet Explorera idiotycznego Notatnika do podglądu źródeł stron html, warto instalować choćby dla tej jednej cechy i wymiany starego Notatnika.
  • JEdit – napisany w Javie, mnóstwo pluginów
  • XEmacs – kompletnie nieintuicyjna obsługa, ten edytor zupełnie w niczym nie przypomina skrótów klawiszy z innych programów, ale jest b. potężny. Jak ktoś już się przekatuje i go nauczy, to podobno jest to doskonałe narzędzie. Mnie nigdy nie starczyło cierpliwości aby go opanować. Wolę już edytor Vi.

Edytory IDE – Python

Użytkownikom win32 polecam instalację ActivePython zamiast instalacji na stronie główej Pythona. Do Pythona istnieje mnóstwo niezłych edytorów tekstowych jak i edytorów IDE. Moimi ulubionymi są:

  • Eclipse + pyDEV – chyba najlepsza opcja. Edytor Eclipse jest napisany w Javie i wymaga trochę pamięci, ale jest b. dobry. Posiada intuicyjny interfejs, mnóstwo pluginów dających spójne środowisko do pracy z javą, pythonem, ruby i php. pyDEV z zintegrowanym pakietem pyLint nie tylko podpowiada kod, ale także ma możliwość wyłapania nieużytej zmiennej, czy brak zgodności stylu programowania z zaleceniem PEP8 itp. Oczywiście jest też zintegrowany debugger.
  • PythonWin – standardowo instalowany w ActivePython. Jest znacznie szybszy i lżejszy od Eclipse, bo jest napisany w języku C. Ma świetny debugger, wbudowaną konsolę, podgląd obiektów COM. Jedyną jego wadą jest brak obsługi utf-8. Zaletą jest też to, że nadaje się do szybkiej edycji pojedyńczych plików (Eclipse wymaga tworzenia projektu, co często jest b. niewygodne). PythonWin ma także ulepszoną konsolę, bo pełnoekranową. Łatwiej w nim szybko zaznaczyć fragment poprzedniej komendy i uzupełnić.
  • SPE – edytor napisany w Pythonie z wykorzystaniem biblioteki wxPython. Ma unikalne, b. ładne kolorowe zakładki (umożliwia lepsze wyświetlanie podglądu metod i atrybutów klas). Wbudowany debugger, listę Todo, wyszukiwanie po wielu plikach i co ważniejsze: lepiej podpowiada kod niż np. PythonWin.
  • Eric – napisany w Pythonie z użyciem biblioteki PyQT. Ma dużo możliwości, wspomaganie tworzenia unittestów, refactoring, debugger itp. Obsługuje nie tylko Pythona ale i Rubiego.
  • Boa Constructor – ten edytor może zainteresować osoby piszące aplikacje oparte na wxPython bo posiada wbudowane wizualne wspomaganie tego procesu. Wzorowany jest na Delphi.
  • Komodo: Kombajn podobnie jak Eclipse. Występuje w wersji IDE (płatnej, ale licencja od razu obejmuje Windows, Linux i Mac OS X) i Edit (bezpłatnej). Ma świetny debuger do Pythona. Jest szybszy od Eclipse i obsługuje dobrze Pythona, Rubiego, PHP, C++ i inne języki (nawet szablony Django). Bardzo dobrze podpowiada kod HTML, CSS, zamyka tagi HTML podobnie jak Dreamweaver. Niestety, podpowiadanie kodu pozbawione jest na razie wyświetlania zintegrowanej dokumentacji (docstringów dla Pythona czy rdoc dla Rubiego, wyświetlana jest tylko lista metod).
  • WingIDE – wymieniam go na końcu bo jest płatny. Doskonale (zdecydownie najlepiej z wszystkich) podpowiada kod, posiada możliwość edycji w Zope, doskonały debugger.

Edytory IDE – Ruby

Do Rubiego istnieje też sporo niezłych edytorów, choć nie aż tyle co do Pythona.

  • Netbeans IDE 6.x ma zdecydowanie najlepsze podpowiadanie składni do Rubiego. Najlepszy IDE do Rubiego i Rails obok Aptana IDE.
  • Aptana IDE+Rails (RadRails został przejęty) to nic innego jak Eclipse dostosowany do pracy z Ruby on Rails. Posiada wszystkie zalety Eclipse, np. po dodaniu pluginów ma dobre wsparcie dla innych języków, np. Pythona. Od chwili wchłonięcia RadRails przez Aptanę, posiada świetne dodatkowe mocne wsparcie dla kodu HTML, CSS, JavaScript.
  • Arachno IDE – bardzo ładny i szybki. Może kolorować kod (nie tylko samego Rubiego ale także szablonów ERb, czyli nadaje się dobrze do Railsów) podobnie jak TextMate, ma wbudowany debugger, generowanie dokumentacji RDoc dla projektu, przeglądarka klas Rubiego i zainstalowanych gemsów. Niestety jest płatny i bieżąca wersja nie obsługuje jeszcze utf-8.
  • TextMate – działa tylko z systemem OS-X, czyli wymaga Macintosha. Ale jest to ulubiony edytor developerów Railsów. Sam edytor jest niestety płatny. Ma bardzo rozbudowane makra (snipety) przyśpieszające wprowadzanie kodu, ale ma beznadziejnie słabe podpowiadanie kodu, nie rozwija metod do bibliotek wbudowanych Rubiego, dokumentacji RDOC w ogóle nie ma scalonej z podpowiedziami do kodu. Do Pythona jest jeszcze gorzej. Ale edytor jest szybki, lekki i ma ładne kolorowanie wielu języków.
  • RDE czyli Ruby Development Environmnent. Edytor jest b. szybki i pomocny jak ktoś chce pisać kod w czystym Ruby.

Edytory IDEPHP

Do PHP istnieje sporo edytorów, ale moim zdaniem większość to badziewie. Sam PHP jest językiem który bardzo utrudnia napisanie na niego dobrego debuggera. Właściwie w nic dobrego tu nie ma poza komercyjnym Zendem. Ale gdybym miał wybierać, to wybrałbym:

  • Eclipse + plugin phpeclipse – po pierwsze jest darmowy, po drugie doskonale zarządza kodem PHP i koloruje szablony Smarty. Po trzecie, doskonale współpracuje z SVN. Miałem okazję sprawdzić nowy Zend Studio 5.1. Mimo, że lepiej podpowiada kod, to nie koloruje Smartów i kiepsko współpracuje z SVN, beznadziejnie wręcz.
  • Zend Studio – komercyjny IDE stworzony przez twórców PHP. Najlepiej ze wszystkich podpowiada kod (lepiej od Eclipse) Niestety nie potrafi kolorować kodu popularnych szablonów Smarty (Eclipse to potrafi!) Szybkość ta sama co Eclipse, wymagania co do pamięci – też.
  • Macromedia Dreamwever 8 – komercyjny, ale nieźle koloruje i podpowiada kod. Najnowsza wersja Dreamweawera ma sporo usprawnień, np. w końcu bada końce bloków (klamr). Dreamweawer jest też oczywście szybszy od Zend Studio i Eclipse bo jest napisany w języku C.

Niestety edytory PHP mają fatalne możliwości debugowania kodu w stosunku do tego, co można spotkać dla Pythona i Ruby. Do szybkiego testowania kodu PHP, najlepiej użyć SciTE. Można mu ustawić opcję aby po wciśnięciu klawisza F5 natychmiast generował wynik bez żadnego pośrednictwa serwera www.

Posted in , , ,  | Tags , , ,  | 14 comments

Akcesory w Javie, Pythonie, Ruby i PHP5

Posted by Jarosław Zabiełło Tue, 07 Feb 2006 22:25:00 GMT

Java, język który dobył sobie silną pozycję na rynku korporacyjnym, jest od jakiegoś czasu pod obstrzałem krytyki z różnych stron. Wyszła cała seria książek krytykujących model obiektowy oraz metodologie promowane przez Javę (od najsłynniejszej Beyond Java po Bitter Java, Bitter EJB, czy inne)

Nie umniejszając zalet Javy, jej krytycy wytykają jej niepotrzebną nadmiarowość kodu, ociężałośći i małą zwrotność, która powoduje że język ten słabo nadaje się do modnej ostatnio metodologii _agile programming. _ Aby lepiej ten problem zobaczyć, przyjrzyjmy się typowej praktyce programistów Javy. Tekst ten zainspirowany jest niedawną dyskusją jaka miała miejsce w jednym z blogów mojego kolegi.

Czym są akcesory?

Akcesory (lub inaczej: gettery i settery) to slangowe okreslenie metod jakie używa obiekt do odczytu i modyfikacji swoich atrybutów. Są one tak nagminnie używane przez programistów Javy, że niektóre edytory (np. Eclipse) zostały nawet wyposażone w makra do automatycznego ich generowania.

Dorzucanie nieużywanego kodu “na wszelki wypadek”...

Nagminna (wśród programistów Javy) praktyka dodawania akcesorów do atrybutów klasy pachnie jakimś antipatternem i są głosy, które używanie akcesorów nazwyają wprost złą praktyką . Jest w tym trochę racji. Ale, jak to poniżej wykażę, jest to pewnego rodzaju kompromis w związku ze słabym modelem obiektowy Javy.

Dlaczego programiści Javy generują akcesory dla każdego atrybutu nawet, jak nie przewidują potrzeby ich używania? Otóż czynią to “na wszelki wypadek” bo nie wiedzą, czy w przyszłości nie będzie im to potrzebne…

Weźmy np. taki kod (z powodu kolejnego ograniczenia Javy nie można w jednym pliku trzymać więcej, niż jednej klasy; tu dla krótkości umieszczam kod z obu plików razem)

public class First {
  public String msg = "hello";
}

public class Second extends First {
  public static void main(String[] args) {
    First obj = new First();        
    System.out.println(obj.msg);
  }
}

Jak widać, w Javie można sięgać do atrybutu w sposób bezpośredni.

Wyobraźmy sobie jednak, że projekt nam się rozrósł i mamy już całkiem sporo kodu oraz sporo plików (pomnażanych wydatnie przez javowe ograniczenia co do ilości klas mogących wystąpić w pliku). Teraz przychodzi polecenie aby w momencie odczytu i zapisu atrybutu coś dodatkowego wykonać. (Np. niech to będzie logowanie informacji o takim zdarzeniu, albo zablokowanie możliwości modyfikacji treści atrybutu. Wszystko jedno co)

Mamy zatem pierwszy problem. Trzeba w tych wszystkich milionach miejsc, gdzie odwoływaliśmy się bezpośrednio do atrybutów, wymienić kod…

Właśnie dlatego, aby takich niespodzianek uniknąć w przyszłości, programiści Javy dorzucają dodatkowe metody opakowujące odczyt i zapis atrybutów. Eclipse upraszcza ten proces zwalniając programistę od ręcznego wpisywania tego kodu.

Klasa First po zmianie:

public class First {
  public String msg = "hello";
  public String getMsg() {
    return msg;
  }
  public void setMsg(String msg) {
    this.msg = msg;
  }
}

Wydaje się, że problem jest rozwiązany. Ale to dopiero początek innych problemów.

Jeśli bowiem, automatycznie dorzucamy te metody do wszystkich tysięcy atrybutów nowo tworzonych klas, to pierwszą rzeczą którą zauważamy, jest nagłe “spuchnięcie kodu”. Mamy kupę nieużywanych linii kodu, z których prawdopodobnie większość nie będzie nigdy używana!

Ale to nie wszystko. Załóżmy, że stajemy przed potrzebą zmiany nazwy atrybutu. Oczywiście, możemy to zmienić w jednym miejscu (w ciele akcesora), ale wtedy wprowadzamy chaos w pozostałej części odnośnie… nazw (atrybut “msg” zmieniony na “title” trochę głupio wygląda z akcesorami o nazwie “getMsg” i “setMsg”) Musimy przekopać się przez miliony miejsc w kodzie i pozmieniać nazwy starym wywołaniom.

Zobaczmy jak ta sytuacja wygląda w językach dynamicznych.

Python

Python (podobnie jak Java) pozwala na bezpośredni dostęp do atrybutów klasy. A co w sytuacji kiedy chcemy opakować atrybut akcesorami? Nic prostszego. Python pozwala na dodanie akcesorów wtedy, i tylko wtedy, kiedy są potrzebne. Na dodatek czyni to w sposób całkowicie przezroczysty dla pozostałej części kodu. Programista Javy może sobie o tym tylko pomarzyć.

class X(object):
  def get_msg(self):
    return self.__msg
  def set_msg(self, val):
    self.__msg = val
  msg = property(get_msg, set_msg)

obj = X()
obj.msg = "hello"
print obj.msg

Python pozwala także związać z dowolną metodą, atrybutem, klasą czy modułem docstring, czyli tekst z dokumentacją, objaśnieniem itp. To jedna z genialnych cech Pythona specjalnie pomyślana dla leniwych programistów, którym nie chce się pisać dokumentacji. ;)

Ruby

W języku Ruby z definicji nie ma żadnej możliwości dostępu do atrubutów klasy inaczej jak przez akcesory. Odpada więc problem zapominania aby je dodać. Ruby jednak narzuca nazwy dla akcesorów (mają nazwę taką jak atrybut!) i całość wygląda tak, jakby operowano bezpośrednio na atrybucie.

class X
  def msg
    @msg
  end
  def msg=(val)
    @msg = val
  end
end

obj = X.new
obj.msg = "hello"
puts obj.msg
Dla osób, które nie lubią za dużo pisać, Ruby ma wygodne skróty. Powyższą definicję klasy można zapisać także w ten sposób:
class X
  attr_accessor :msg
end

Istnieją także oddzielne skróty dla getterów i setterów. No i można po przecinku dodać akcesory dla całej grupy atrybutów.

PHP5

PHP w wersji 5 ma przebudowany model obiektowy od podstaw. Twórcy języka PHP nie starali się poprawiać modelu obiektowego PHP4 (był on tak zły, że prościej było im napisać go od nowa). W nowym PHP5 mamy już możliwość przezroczystego dodania akcesora.

<?php
class X {
  private $attributes = array('msg'=>null);
  private function __get($attrname) {
    return $this->attributes[$attrname];
  }
  private function __set($attrname, $val) {
    $this->attributes[$attrname] = $val;
  }
}

$obj = new X();
$obj->msg = "hello";
print $obj->msg;
?>

Składnia może nie jest tak prosta jak w Ruby, ale (przynajmniej w tym miejscu), PHP5 zachowuje się tu sensownie i unika dylematów Javy.

Posted in , ,  | Tags , , ,  | 11 comments

pl.comp.lang.ruby

Posted by Jarosław Zabiełło Thu, 05 Jan 2006 10:36:00 GMT

Pojawiła się inicjatywa założenia grupy dyskusyjnej pl.comp.lang.ruby. Aby grupa powstała potrzebne są głosy poparcia sympatyków tego języka na pl.news.nowe-grupy.

Posted in  | Tags  | no comments

Ruby, Python vs Java,C++

Posted by Jarosław Zabiełło Mon, 14 Nov 2005 20:41:00 GMT

Od czasu do czasu na grupach dyskusyjnych mają miejsce przepychanki na temat wyższości jednych języków nad drugimi. Czasami takie dyskusje wywołują sporo emocji. Zmęczony ciągłymi ogólnikowymi sloganami o tym jak to w C++ lub Javie pisze się lepiej i szybciej (dobry dowcip) programy niż w językach dynamicznych takich jak Python czy Ruby, postanowiłem rzucić małe wyzwanie miłośnikom tych języków.

Zadanie polegało na napisaniu wyszukiwarki do polskiego tekstu Koranu (w sumie nie widziałem nic takiego, stąd pomysł). Aby nie było za banalnie, trzeba było napisać taki kod który by automatycznie wyssał przekład Koranu ze strony internetowej http://www.planetaislam.com/koran.html następnie przetworzył go i wstawił do bazy mysql oraz przełączył się w tryb wyszukiwania fraz.

Wyzwanie zostało przyjęte i… mijają tygodnie za tygodniami a moi dyskutanci nie są w stanie przedstawić żadnego działającego kodu w Javie ani w C++. Tak to teoria zderza się z praktyką. Oto naprędce stworzona wersja w Ruby:

require 'mysql'
require 'open-uri'

@przeklad = 'bielawski'
@conn = Mysql.new('localhost', 'login', 'hasło', 'koran', 3308)

def download(max_chapter=15)
  def clean_html(src) # nie znam biblioteki czyszczacej html wiec robie to regexem
    src.gsub(/&nbsp;/, ' ').gsub(/(<[^>]+>)/, '').gsub(/[ ]{2,}/, ' ').gsub(/&#/, '').gsub(/&quot;/, '"')
  end   
  @lines = []
  1.upto(max_chapter) do |sura|
    url = sprintf('http://www.planetaislam.com/koran/%s/%03d.htm', @przeklad, sura)
    src = clean_html(open(url).read)
    rows = src.scan(/(\d+?)[. ]([^\d]+)/) # wyluskuj wersety i numery rozdzialow
    rows.each do |row|
      @lines << sprintf('%s:%s %s', sura, row[0], row[1].gsub(/[\s]{2,}/, ' ').strip)
    end
    f = File.new("#{@przeklad}.txt", "w") # zapisuje sobie do pliku
    @lines.each { |line| f.write("#{line}\n") }                               
  end
end
def loaded_db
  @conn.list_tables.include?(@przeklad) # Sprawdzam czy juz istnieje tabela z danymi.
end
def load_db
  @conn.query("DROP TABLE IF EXISTS #{@przeklad}")
  @conn.query("CREATE TABLE #{@przeklad} (
    id int(10) unsigned NOT NULL auto_increment,
    sura varchar(255) NOT NULL,
    nr int(10) unsigned NOT NULL,
    verse text NOT NULL,
    PRIMARY KEY (id)
    ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci")
  @conn.query("SET CHARACTER SET latin2")
  @lines.each do |line|
    sura, nr, verse = line.scan(/(\d+?):(\d+?) (.+)/)[0]
    verse.gsub!(/'/,"''") # uzywam topornego sql bo cos mi metoda prepare nie dziala, moze zla biblioteka?
    @conn.query("INSERT INTO #{@przeklad} (sura, nr, verse) VALUES(#{sura}, #{nr}, '#{verse}')")
  end
end  
def search_result(phrase)
  @conn.query("SET CHARACTER SET cp1250")
  sql = "SELECT * FROM " + @przeklad + " WHERE verse LIKE '%" + phrase + "%' "
  c = @conn.query(sql)
  while row = c.fetch_row
    puts sprintf("Sura %s,%s %s\n", row[1], row[2], row[3])
  end
end

if $0 == __FILE__
  if !loaded_db          
    download
    load_db
  end
  search_result('kobieta')
end

A oto ostateczna wersja w Pythonie:

#!python

"""
Wyszukiwarka do Koranu

Przykłady uzycia:
    python koran.py --txt zabijajcie
    python koran.py --sura=ix
    python koran.py --sura=9 --nr=4
    python koran.py --sura=IX --nr=4    

Wymagania:
    interpreter Pythona 2.4 + standardowe biblioteki
    pythonowska biblioteka MySQLdb
    serwer MySQL 4.1

Autor:
    Jarosław Zabiełło http://zabiello.com
"""

import HTMLParser, os, re, urllib2, sys
from optparse import OptionParser
import MySQLdb

class Koran(object):
    dbname ="koran"
    przeklad = 'bielawski'
    lines = []

    def __init__(self):
        self.conn = MySQLdb.connect(
            host='localhost',
            port=3308,
            user='login',
            passwd='hasło',
            db=self.dbname)

    def download(self, max_chapter=15):
        """Sciagam dane z internetu i je troszke oczyszczam z nadmiarowego kodu html"""
        def html_clean(src):
            x = HtmlStripper()
            x.feed(src)
            return src = x.get_fed_data()            
        self.lines = []
        for i in xrange(1,max_chapter):
            url = 'http://www.planetaislam.com/koran/%s/%03d.htm' % (self.przeklad, i)
            src =  html_clean(urllib2.urlopen(url).read()) # pobrany caly plik
            rows = re.findall(r'(\d+?)[. ]([^\d]+)', src) # wyluskuj wersety i numery rozdzialow            
            for row in rows:
                self.lines.append('%s:%s %s' % (i, row[0], re.sub(r'[\s]{2,}', ' ', row[1]).strip()))
        self.lines = [unicode(line, 'iso-8859-2').encode('utf-8') for line in self.lines]
        f = open(self.przeklad+'.txt', 'w')  # zapisuje sobie kopie na dysk
        for line in self.lines:
            f.write('%s\n' % line)
        f.close()

    def loaded_db(self):
        """Sprawdzam czy juz istnieje tabela z danymi."""
        c = self.conn.cursor()
        c.execute("SHOW TABLES")
        rows = c.fetchall()
        if self.przeklad in [row[0] for row in rows]:
            c.execute("SELECT COUNT(*) FROM %s" % self. przeklad)
            return c.fetchone()[0]
        return False

    def load_db(self):
        "Zakladam ze stworzono baze o nazwie self.dbname i laduje dane do tabeli self.przeklad"
        sqls = (
            "DROP TABLE IF EXISTS %s" % self.przeklad,
            '''CREATE TABLE %s (
                id int(10) unsigned NOT NULL auto_increment,
                sura varchar(255) NOT NULL,
                nr int(10) unsigned NOT NULL,
                verse text NOT NULL,
                PRIMARY KEY (id)
            ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci''' % self.przeklad)
        c = self.conn.cursor()
        for sql in sqls: # odtwarzam baze
            c.execute(sql)
        c.execute("SET CHARACTER SET UTF8")
        sql = "INSERT INTO "+self.przeklad+" (sura, nr, verse) VALUES (%s,%s,%s)"
        for line in self.lines:
            found = re.match(r'(\d+?):(\d+?) (.+)', line)
            if found:
                c.execute(sql, found.groups())
        c.close()

    def set_i18n(self, cursor, phrase=None):
        if sys.platform == 'win32': # zakladam ze bede wyswietlal wyniki w okienku konsoli, czyli kodowanie dosowe
            if phrase:
                phrase = unicode(phrase, 'cp1250').encode('cp852')
            cursor.execute("SET CHARACTER SET cp852")
        elif sys.platform == 'linux':
            cursor.execute("SET CHARACTER SET latin2")
        return phrase

    def search_result(self, phrase):
        """Wyszukuje wersetow wg prostej zasady: znadz wszystkie wersety w ktorych gdziekolwiek
        wystepuje fraza phrase. Dla wygody dodalem obsluge polskich znakow spod konsoli dosowej
        i linuksa."""
        c = self.conn.cursor()
        phrase = self.set_i18n(c, phrase)
        sql = "SELECT * FROM " + self.przeklad + " WHERE verse LIKE '%" + phrase + "%' "
        print sql
        c.execute(sql)
        rows = c.fetchall()
        c.close()
        print "Znaleziono %d wersetow:\n" % len(rows)
        for row in rows:
            print "Sura %s,%s %s\n" % (int_to_roman(int(row[1])), row[2], row[3])

    def show_verse(self, sura, nr=0, context=0):
        c = self.conn.cursor()
        self.set_i18n(c)
        if sura.isalpha():
            sura = roman_to_int(sura)
        if sura and nr: # wyswietl pojedyncza sure
            sql = "SELECT verse FROM " + self.przeklad + " WHERE sura=%s AND nr=%s"
            c.execute(sql, (sura, nr))
            verse = c.fetchone()[0]
            print "Sura %s,%s %s\n" % (int_to_roman(int(sura)), nr, verse)
        else: # wyswietl cala sure
            sql = "SELECT nr, verse FROM " + self.przeklad + " WHERE sura=%s ORDER BY nr"
            c.execute(sql, [sura])
            rows = c.fetchall()
            for row in rows:
                print "Sura %s,%s %s\n" % (int_to_roman(int(sura)), row[0], row[1])
        c.close()

class HtmlStripper(HTMLParser.HTMLParser):
    """src: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440481"""
    def __init__(self):
        self.reset()
        self.fed = []
    def handle_data(self, d):
        self.fed.append(d)
    def get_fed_data(self):
        return ''.join(self.fed)

def int_to_roman(input):
    """src: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81611"""
    if type(input) != type(1):
        raise TypeError, "expected integer, got %s" % type(input)
    if not 0 < input < 4000:
        raise ValueError, "Argument must be between 1 and 3999"   
    ints = (1000, 900,  500, 400, 100,  90, 50,  40, 10,  9,   5,  4,   1)
    nums = ('M',  'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
    result = ""
    for i in range(len(ints)):
        count = int(input / ints[i])
        result += nums[i] * count
        input -= ints[i] * count
    return result

def roman_to_int(input):
    """src: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81611"""
    if type(input) != type(""):
        raise TypeError, "expected string, got %s" % type(input)
    input = input.upper()
    nums = ['M', 'D', 'C', 'L', 'X', 'V', 'I']
    ints = [1000, 500, 100, 50,  10,  5,   1]
    places = []
    for c in input:
        if not c in nums:
            raise ValueError, "input is not a valid roman numeral: %s" % input
    for i in range(len(input)):
        c = input[i]
        value = ints[nums.index(c)]
        try:
            nextvalue = ints[nums.index(input[i +1])]
            if nextvalue > value:
                value *= -1
        except IndexError:
            pass
        places.append(value)
    sum = 0
    for n in places:
        sum += n
    return sum

if __name__ == '__main__':
    appl = Koran()
    if not appl.loaded_db():
        print 'Pobieram dane z internetu....'
        appl.download()
        print 'Wkladam dane do bazy...'
        appl.load_db()
    parser = OptionParser()
    parser.add_option('--txt', dest='txt', help='wyszukiwana fraza')
    parser.add_option('--sura', dest='sura', help='numer Sury (liczba dziesietna lub rzymska)')
    parser.add_option('--nr', dest='nr', help='numer wersetu dla Sury')
    (options, args) = parser.parse_args()
    if options.txt:
        appl.