Operacje wejścia-wyjścia w Rubim
(15 komentarzy)W kategoriach: Ruby , Ruby tutorial , Techblog / 04 lipca 2007 [21:44:19]
Tagi technorati: Ruby input output programming stdin stdout tutorial
W tej części tutoriala nauczymy się korzystać z wejścia i wyjścia na poziomie daleko wykraczającym poza programy "Hello World!"...
Kernel
Moduł Kernel dostarcza nam podstawowe metody związane z obsługą wejścia i wyjścia. Oto i one:
putc- wyświetla znak o kodzie ASCII podanym za argument; zwrócmy uwagę, że dodanie do kodu wielokrotności liczby 256 wyświetli taki sam znak(97..99).each do |c| putc c end (354..356).each do |c| putc c endCo na wyjściu da nam:
abcbcd
puts- wyświetla tekst podany za argument, dodając na koniec znak końca linii; jeśli argumentem jest tablica, wyświetla każdy element, oddzielając je separatorem linii; jeżeli którykolwiek z elementów zawiera już separatory, metoda usuwa jeden z nichputs "Hello World!" puts ["To\n", "tylko\n\n", "test"];Wyjście:
Hello World! To tylko test
print- wypisuje obiekt (lub obiekty) na standardowe wyjście; odziela każdy z nich łańcuchem określonym w zmiennej magicznej$,(która domyślnie ma wartośćnil); dodaje na koniec Stringa wynikowego zawartość$\(domyślnienil)print "a", 1, [2, '4'], "b", "\n" $, = ", " $\ = "\n" print "a", 1, [2, '4'], "b"Wyjście:
a124b a, 1, 2, 4, b
Zwróćmy uwagę, że po ustawieniu zmiennej
$\nie musimy dorzucać znaku\nna koniec sekwencji obiektów.printf- działa tak samo, jak standardowa funkcja języka C o takiej samej nazwieopen- otwiera plik (jeśli parametr nie rozpoczyna się znakiem|) lub potok podprocesu.Poniższy program na dwa sposoby realizuje odczyt pierwszej linii pliku:
# 1 f = open "C:\\plik.txt" puts f.gets # 2 open "C:\\plik.txt" do |f| puts f.gets endRozpoczynając parametr metody
openznakiem potoku, otwieramy podproces z możliwością śledzenia jego wyjścia oraz zapisu danych na jego wejście. Napiszemy teraz program, który wywołuje systemowe poleceniedir(lub jakiekolwiek inne) i wyświetli wynik jego działania na ekranie.open("|dir") do |f| while ln = f.gets do puts ln end endWyniku działania, ze względów bezpieczeństwa, nie podam :-).
Operujemy na plikach
Można to robić w sposób klasyczny lub blokowy:
# 1
f = File.open "C:\\plik.txt"
while ln = f.gets do puts ln end
f.close
# 2
File.open "C:\\plik.txt" do |f|
while ln = f.gets do puts ln end
end
Przewaga tego drugiego jest dokładnie widoczna, gdy zwracany jest wyjątek. Pierwszy sposób może wtedy ominąć metodę zamykającą plik. Stosując podejście blokowe, możemy kodować (i spać) spokojnie.
Zapis do pliku realizujemy za pomocą metod puts, print itp. obiektu klasy File, które działają tak samo, jak ich imiennicy z modułu Kernel.
Używamy iteratorów
Iteratory to potężne narzędzie (pisałem o nich w poprzedniej części tutoriala), zaimplementowano takowe również do operacji wejścia i wyjścia.
Stwórzmy przykładowy plik o następującej zawartości:
Pierwsza linia Druga linia A to jest trzecia
Napiszemy teraz program, który, z wykorzystaniem iteratorów, odczyta kolejno wszystkie znaki (oddzielając każdy minusem) i każdą linię pliku, informując o odczytanym wierszu.
File.open "C:\\plik.txt" do |f|
f.each_byte { |c| putc c; print "-" }
end
puts # Przejście do nowego wiersza
File.open "C:\\plik.txt" do |f|
f.each_line { |ln| puts "Napotkałem na wiersz: #{ln}" }
end
Output:
P-i-e-r-w-s-z-a- -l-i-n-i-a-- -D-r-u-g-a- -l-i-n-i-a-- -A- -t-o- -j-e-s-t- -t-r-z-e-c-i-a- Napotkałem na wiersz: Pierwsza linia Napotkałem na wiersz: Druga linia Napotkałem na wiersz: A to jest trzecia
Nic nie stoi na przeszkodzie, byśmy sami wybrali sobie separator linii, gdy domyślny (\n) nam nie odpowiada.
File.open "C:\\plik.txt" do |f|
f.each_line(" ") { |ln| puts ln }
end
Pierwsza linia Druga linia A to jest trzecia
Jak widać w tym przypadku, za separator linii obraliśmy sobie spację.
Aby URL plikiem się stał
To żaden problem. Wystarczy dodać jedną linię kodu, aby można było traktować adresy internetowe jak zwykłe pliki. Poniższy program wyświetli kod HTML wyszukiwarki Google.
require 'open-uri'
open("http://google.com") do |f|
puts f.readlines
end
Prawda, że piękne?
thion
04 lipca 2007, 21:46:34Taaak, ten blog znowu żyje! :-)
RazorJack
04 lipca 2007, 21:50:51Bo i ja żyję – sesja się skończyła, a ja odespałem swoje. ;]
Liorithiel
05 lipca 2007, 14:36:18Takie małe pytanie: czy w rubym iteratory można obsługiwać tak bardziej „niskopoziomowo”? W pythonie można zrobić:
for linia in file(’/etc/passwd’): print linia
ale można też:
iterator = file(’/etc/passwd’).iter()
try: while True: print iterator.next()
except StopIteration: pass
Liorithiel
05 lipca 2007, 14:36:58oj, wcięło białe znaki… ale chyba widać o co chodzi
RazorJack
05 lipca 2007, 23:49:39Czy ja wiem, czy takie podejście można nazwać niskopoziomowym? Grunt, że się da:
Można też zastosować
for, wtedy kod wygląda tak:Oczywiście można powiedzieć, że to nie to samo, co zaprezentowany przez Ciebie
file, ale zawsze można napisać alias doIO.readlines. Tyle że chyba nie ma sensu klonować innego języka, skoro natywnie w Rubim mamy równie zwięzłe, ale inne sposoby osiągnięcia takiego czytania z pliku. ;)Liorithiel
06 lipca 2007, 00:17:42Chodzi mi o samą ideę iteratorów. Nie staram się klonować pythona w rubym. Wiem tylko, że w pythonie mogę zrobić coś takiego:
from itertools import cycle # cycle - iterator, który def proces1(): # fibonacci a,b=0,1 while True: print a a,b = b,a+b yield None def proces2(): a=2 while True: print a a*=2 yield None def proces3(): while True: print 'Trzeci proces' yield None # każdy z procesów jest iteratorem, który będzie zwracał za każdym razem None procesy = [proces1(), proces2(), proces3()] for aktywny in cycle(procesy): aktywny.next()I to wypisze na przemian linijki z różnych iteratorów. To coś w stylu procesów współbieżnych, tyle że same się zrzekają co jakiś czas władzy nad procesorem (za pomocą yielda)... Ale każdy proces ma własne zmienne lokalne itd… Całkiem przydatne na przykład w aplikacjach webowych, gdzie jednym takim procesem reprezentujesz sesję dla jednego użytkownika, trzymasz w nim stan sesji… a jak użytkownik kliknie, proces jest przywracany do życia przez odpowiednią procedurę sterującą tak, jakby nic się nie stało między wysłaniem jednego html do użytkownika, a przyjściem wypełnionego formularza.
Liorithiel
06 lipca 2007, 00:21:33Aha, i do tego właśnie przydaje się niskopoziomowa obsługa. Ciekawy jestem po prostu, jak by coś takiego w rubym wyglądało.
lopex
06 lipca 2007, 00:25:03Paradoksalnie for to jeszcze wyższy „poziom” ponieważ używa on protokołu each. Nie używają go natomiast while i until:
f = open(„foo”)
s = nil;puts s while s = f.gets
Można też posłużyć się enumeratorami (podobnie jak pythonowe generator expressions nie materializują list)
require ‘enumerator’
enum = open(„foo”).to_enum
enum.each{|e|puts e}
W 1.9 są już mocno zintegrowane i powyższe sprowadzi się do:
enum = open(„foo”).each
enum.each{|e|puts e}
Choć w tym przypadku są oczywiście wydumane bo IO#each też nie materializuje.
RazorJack
06 lipca 2007, 19:40:39(Komentarz zmodyfikowany 06.07.2007 o 19:42)
W Rubim oczywiście istnieją zaawansowane możliwości tworzenia i manipulowania iteratorami oraz blokami. Niżej wklejam na szybko napisany kod, który na przemian, cyklicznie wywołuje bloki kodu, które po każdym wywołaniu potrafią kontynuować swoje działanie (zachowują wartości swoich zmiennych).
Nie znalazłem w dokumentacji Rubiego metod, które pozwalają na tworzenie cyklów z jakiejś kolekcji, więc napisałem na szybko swoją implementację. Zdaję sobie sprawę, że pod wieloma względami jest ułomna i nieelastyczna, ale tu chodzi o szybki przykład pokazujący, że taka możliwość istnieje także w Rubim. ;-)
PS. Czuję się sprowokowany i biorę się za pisanie tutoriala o blokach, procach itp. :P
Liorithiel
06 lipca 2007, 22:12:34Te proce wyglądają trochę magicznie, napisz o tym ;-)
Widzę tu funkcję Cycle i klasę Cycle, nie gryzą się? Domyślam się, że ‘initialize’ to domyślna nazwa konstruktora?
RazorJack
06 lipca 2007, 22:32:43Napiszę. Jutro na pewno tutorial wyjdzie na świat. :)
Co do
Cycle– jest to zwykła klasa. Metoda (nie funkcja! :) )Cycleto tak naprawdę kolejna metoda modułu Kernel i jej definicja mogłaby wyglądać tak:module Kernel def Cycle #... end endco byłoby synonimem do tego, co napisałem w poprzednim commencie (metodę można także wywołać poprzez
Kernel.Cycle…). Dzięki temu nic się ze sobą nie gryzie.I tak,
initializeto nazwa konstruktora. :)A co do proców – dają sporo możliwości. W tym przypadku pozwalają skonwertować blok kodu na obiekt, by potem przechować go np. w tablicy, a później – wywołać.
Liorithiel
06 lipca 2007, 22:38:11Dobrze… to chyba wiem o co chodzi z tym proc. Tylko takie pytanie żeby sprawdzić:
def proces(): print 'Za'; yield None print 'każdym'; yield None print 'razem'; yield None for i in xrange(10): print i; yield None print 'coś'; yield None print 'innego'na ruby? Bo te proce to tylko generują kawałek kodu, a zmienne ‘lokalne’ to po prostu zmienne z domknięcia?
RazorJack
07 lipca 2007, 02:26:35Ciekawe zastosowanie tego pythonowskiego
yield None. Nie słyszałem o podobnej konstrukcji w Rubim, chyba nie da się (bez obchodzenia naokoło) napisać czegoś podobnego. Ale… czy to źle? To chyba raczej ciekawostka, niż niezbędnik. A ciekawsze zastosowania typu „porprocesy” z własnymi zmiennymi można zaimplementować także w Rubim, tyle, że w nieco inny sposób.Co do samych proców, to konwertują one blok na obiekt. Blok (domknięcie) charakteryzuje się tym, że zapamiętuje kontekst, w jakim był wywołany. Stąd jego zdolność do „pamiętania” zmiennych.
Liorithiel
08 lipca 2007, 22:50:12Podawałem już przykład: serwisy webowe. Kiedyś do każdego połączenia serwery odpalały osobne procesy, które były bardzo proste. Teraz kombinuje się na wszystkie sposoby żeby tego nie robić, bo to mało efektywne. Takie mechanizmy jak coroutines (http://en.wikipedia.org/wiki/Coroutine) pozwalają wrócić do takich metod, bez utraty efektywności. Co więcej, zapis jest prosty, więc programista jest w stanie zrozumieć na raz większy kawałek kodu. Inny przykład: programy GUI bez tych denerwujących modalnych okienek. Gdyby Gajim był pisany z wykorzystaniem coroutines (choćby tak prostych jak w cpythonie, bo gdzie im tam do możliwości takiego LISPa czy choćby stackless-pythona!), miałby pewnie ze dwa razy mniej kodu i cztery razy mniej błędów.
apohllo
01 grudnia 2007, 21:42:53Wiem, że sprawa już może nico nieświeża, ale być może ktoś jeszcze zajrzy do tych komentarzy.
To o co pytał Liorithiel w Rubym jak najbardziej istnieje i są to kontynuacje – klasa Continuation