Zanurkuj w Pythonie/Standardowy strumień wejścia, wyjścia i błędów

Standardowy strumień wejścia, wyjścia i błędów

edytuj

Użytkownicy Uniksa są już prawdopodobnie zapoznani z koncepcją standardowego wejścia, standardowego wyjścia i standardowego strumienia błędów. Ten podrozdział jest dla pozostałych osób.

Standardowe wyjście i strumień błędów (powszechnie używana skrócona forma to stdout i stderr) są strumieniami danych wbudowanymi do każdego systemu Unix. Kiedy coś wypisujemy, idzie to do strumienia stdout; kiedy wystąpi błąd w programie, a program wypisze informacje pomocne przy debugowaniu (jak traceback w Pythonie), to wszystko pójdzie do strumienia stderr. Te dwa strumienie są zwykle połączone z oknem terminala, na którym pracujemy, więc jeżeli program coś wypisuje, zobaczymy to na wyjściu, a kiedy program spowoduje błąd, zobaczymy informacje debugujące. (Jeśli pracujemy w systemie z okienkowym IDE Pythona, stdout i stderr domyślnie będą połączone z "interaktywnym oknem".)

Przykład. Wprowadzenie do stdout i stderr
>>> for i in range(3):
...     print 'Nurkujemy'             #(1)
Nurkujemy
Nurkujemy
Nurkujemy
>>> import sys
>>> for i in range(3):
...     sys.stdout.write('Nurkujemy') #(2)
NurkujemyNurkujemyNurkujemy
>>> for i in range(3):
...     sys.stderr.write('Nurkujemy') #(3)
NurkujemyNurkujemyNurkujemy
  1. Jak zobaczyliśmy w przykładzie 6.9, "Prosty licznik", możemy wykorzystać wbudowaną funkcje range, aby zbudować prostą pętlę licznikową, która powtarza pewną operację określoną liczbę razy.
  2. stdout jest obiektem plikopodobnym; wywołując jego funkcję write będziemy wypisywać na wyjście napis, który przekazaliśmy. W rzeczywistość, to właśnie funkcja print naprawdę robi; dodaje ona znak nowej linii do wypisywanego napisu, a następnie wywołuje sys.stdout.write.
  3. W tym prostym przypadku stdout i stderr wysyłają wyjście do tego samego miejsca: do IDE Pythona (jeśli jesteśmy w nim) lub do terminala (jeśli mamy uruchomionego Pythona z linii poleceń). Podobnie jak stdout, stderr nie dodaje znaku nowej linii za nas; jeśli chcemy, aby ten znak został dodany, musimy to zrobić sami.

Zarówno stdout i stderrobiektami plikopodobnymi, a które omawialiśmy w podrozdziale 10.1, "Abstrakcyjne źródła wejścia", lecz te są tylko do zapisu. Nie posiadają one metody read, tylko write. Jednak nadal są one obiektami plikopodobnymi i możemy do nich przypisać inny obiekt pliku lub obiekt plikopodobny, aby przekierować ich wyjście.

Przykład. Przekierowywanie wyjścia
[you@localhost kgp]$ python stdout.py
Nurkujemy
[you@localhost kgp]$ cat out.log
Ta wiadomość będzie logowana i nie zostanie wypisana na wyjście

(W Windowsie możemy wykorzystać polecenie type, zamiast cat, aby wyświetlić zawartość pliku.)

#-*- coding: utf-8 -*-
#stdout.py
import sys
 
print 'Nurkujemy'                                                           #(1)
saveout = sys.stdout                                                        #(2)
fsock = open('out.log', 'w')                                                #(3)
sys.stdout = fsock                                                          #(4)
print 'Ta wiadomość będzie logowana i nie zostanie wypisana na wyjście'     #(5)
sys.stdout = saveout                                                        #(6)
fsock.close()                                                               #(7)
  1. To zostanie wypisane w interaktywnym oknie IDE (lub w terminalu, jeśli skrypt został uruchomiony z linii poleceń).
  2. Zawsze, zanim przekierujemy standardowe wyjście, przypisujemy gdzieś stdout, dzięki temu, będziemy potem mogli do niego normalnie wrócić.
  3. Otwieramy plik do zapisu. Jeśli plik nie istnieje, zostanie utworzony. Jeśli istnieje, zostanie nadpisany.
  4. Całe późniejsze wyjście zostanie przekierowane do pliku, który właśnie otworzyliśmy.
  5. Zostanie to wypisane tylko do pliku out.log; nie będzie widoczne w oknie IDE lub w terminalu.
  6. Przywracamy stdout do początkowej, oryginalnej postaci.
  7. Zamykamy plik out.log.

Dodajmy, że w wypisywanym łańcuchu znaków użyliśmy polskich znaków, a ponieważ nie skorzystaliśmy z unikodu, więc napis ten zostanie wypisany w takiej samej postaci, w jakiej został zapisany w pliku Pythona (czyli wiadomość zostanie zapisana w kodowaniu utf-8). Gdybyśmy skorzystali z unikodu, musielibyśmy wyraźnie zakodować ten napis do jakiegoś kodowania za pomocą metody encode, ponieważ Python nie wie, z jakiego kodowania chce korzystać utworzony przez nas plik (plik out.log przypisany do zmiennej stdout).

Przekierowywanie standardowego strumienia błędów (stderr) działa w ten sam sposób, wykorzystując sys.stderr, zamiast sys.stdout.

Przykład. Przekierowywanie informacji o błędach
[you@localhost kgp]$ python stderr.py
[you@localhost kgp]$ cat error.log
Traceback (most recent line last):
File "stderr.py", line 6, in ?
   raise Exception('ten błąd będzie logowany)
Exception: ten błąd będzie logowany
#stderr.py
#-*- coding: utf-8 -*-
import sys
 
fsock = open('error.log', 'w')               #(1)
sys.stderr = fsock                           #(2)
raise Exception('ten błąd będzie logowany') #(3) (4)
  1. Otwieramy plik error.log, gdzie chcemy przechowywać informacje debugujące.
  2. Przekierowujemy standardowy strumień błędów, dzięki przypisaniu obiektu nowo otwartego pliku do sys.stderr.
  3. Rzucamy wyjątek. Zauważmy, że na ekranie wyjściowym nic nie zostanie wypisane. Wszystkie informacje traceback zostały zapisane w error.log.
  4. Zauważmy także, że nie zamknęliśmy jawnie pliku error.log, a nawet nie przypisaliśmy do sys.stderr jego pierwotnej wartości. To jest wspaniałe, że kiedy program się rozwali (z powodu wyjątku), Python wyczyści i zamknie wszystkie pliki za nas. Nie ma żadnej różnicy, czy stderr zostanie przywrócony, czy też nie, ponieważ program się rozwala, a Python kończy działanie. Przywrócenie wartości do oryginalnej, jest bardziej ważne dla stdout, jeśli zamierzasz później wykonywać jakieś inne operacje w tym samym skrypcie.

Ponieważ powszechnie wypisuje się informacje o błędach na standardowy strumień błędów, Python posiada skrótową składnie, która można wykorzystać do bezpośredniego przekierowywania wyjścia.

Przykład. Wypisywanie do stderr
>>> print 'wchodzimy do funkcji'
wchodzimy do funkcji
>>> import sys
>>> print >> sys.stderr, 'wchodzimy do funkcji' #(1)
wchodzimy do funkcji
  1. Ta skrótowa składnia wyrażenia print może być wykorzystywana do pisania do dowolnego, otwartego pliku, lub do obiektu plikopodobnego. W tym przypadku, możemy przekierować pojedynczą instrukcję print do stderr bez wpływu na następne instrukcje print.

Z innej strony, standardowe wejścia jest obiektem pliku tylko do odczytu i reprezentuje dane przechodzące z niektórych wcześniejszych programów. Prawdopodobnie nie jest to zrozumiałe dla klasycznych użytkowników Mac OS-a lub nawet dla użytkowników Windows, którzy nie mieli za wiele do czynienia z linią poleceń MS-DOS-a. Działa to w ten sposób, że konstruujemy ciąg poleceń w jednej linii, w taki sposób, że to co jeden program wypisuje na wyjście, następny w tym ciągu traktuje jako wejście. Pierwszy program prosto wypisuje wszystko na standardowe wyjście (bez korzystania ze specjalnych przekierowań, wykorzystuje normalną instrukcję print itp.), a następny program czyta ze standardowego wejścia, a system operacyjny udostępnia połączenie pomiędzy wyjściem pierwszego programu, a wyjściem kolejnego.

Przykład. Ciąg poleceń
[you@localhost kgp]$ python kgp.py -g binary.xml         #(1)
01100111
[you@localhost kgp]$ cat binary.xml                      #(2)
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
[you@localhost kgp]$ cat binary.xml | python kgp.py -g - #(3) (4)
10110001
  1. Jak zobaczyliśmy w podrozdziale 9.1, "Nurkujemy", polecenie to wyświetli ciąg ośmiu przypadkowych bitów, 0 i 1.
  2. Dzięki temu po prostu wypiszemy całą zawartość pliku binary.xml. (Użytkownicy Windowsa powinni wykorzystać polecenie type zamiast cat.)
  3. Polecenie to wypisuje zawartość pliku binary.xml, ale znak "|" (ang. pipe), oznacza, że standardowe wyjście nie zostanie wypisana na ekran. Zamiast tego, zawartość standardowego wyjścia zostanie wykorzystane jako standardowe wejście następnego programu, który w tym przypadku jest skryptem Pythona.
  4. Zamiast określać modułu (np. binary.xml), dajemy "-", który każe naszemu skryptowi wczytać gramatykę ze standardowego wejścia, zamiast z określonego pliku na dysku. (Więcej o tym, w jaki sposób to się dzieje w następnym przykładzie.) Zatem efekt będzie taki sam, jak w pierwszym poleceniu, gdzie bezpośrednio określamy plik gramatyki, ale tutaj zwróćmy uwagę na rozszerzone możliwości. Zamiast wywoływać cat binary.xml, moglibyśmy uruchomić skrypt, który by dynamicznie generował gramatykę, a następnie mógłby ją doprowadzić do naszego skryptu. Dane mogłyby przyjść skądkolwiek: z bazy danych, innego skryptu generującego gramatykę lub jeszcze inaczej. Zaletą tego jest to, że nie musimy zmieniać w żaden sposób kgp.py, aby dołączyć jakąś funkcjonalność. Jedynie, co potrzebujemy, to możliwość wczytania gramatyki ze standardowego wejścia, a całą logikę dodatkowej funkcjonalności możemy rozdzielić wewnątrz innego programu.

Więc w jaki sposób skrypt "wie", żeby czytać ze standardowego wejścia, gdy plik gramatyki to "-"? To nie jest żadna magia; to tylko właśnie prosty kod.

Przykład. Czytanie ze standardowego wejścia w kgp.py
def openAnything(source):
    if source == "-":    #(1)
        import sys
        return sys.stdin
 
    # spróbuj otworzyć za pomocą urllib (jeżeli źródłem jest http, ftp, lub URL)
    import urllib
    try:

# ... ciach ...
  1. Jest to funkcja openAnything z toolbox.py, którą wcześniej badaliśmy w podrozdziale 10.1, "Abstrakcyjne źródła wejścia”. Wszystko, co musimy zrobić, to dodanie trzech linii kodu na początku, aby sprawdzić, czy źródłem nie jest "-"; jeśli tak, to zwracamy sys.stdin. Naprawdę, to tylko tyle! Pamiętasz, stdin jest obiektem plikopodobnym z metodą read, więc pozostałą część kodu (w kgp.py, gdzie wywołujemy funkcję openAnything) w żaden sposób nie zmieniamy.