Potokowanie (czyli piping) w Pythonie
Potokowanie
Potokowanie w Linux to przekazywanie wyników jednego programu do następnego zamiast wysyłać je na ekran.
Przykładowo w poniższym ls zamiast wyświetlać listę plików przekazuje ją do sort, która sortuje listę w odwrotnej kolejności i przekazuje do head, która wyświetla pierwszy 5 wierszy.
$ ls | sort -r | head -5
Można to jeszcze połączyć z przekierowaniem wyjścia do pliku > lub plik do wejścia <
$ sort -r < input.txt | head > output.txt
lub podać < na początku (co działa w Bash ale nie w Fish, które używam)
$ < input.txt sort -r | head > output.txt
W ten sposób z kilku małych programów można zbudować coś większego.
Potokowanie w Python
Program w Pythonie też można wykorzystać w takim potokowaniu.
Wystarczy, że pobiera on dane ze standardowego wejścia sys.stdin za pomocą sys.stdin.readline() (ewentualnie sys.stdin.read(), sys.stdin.readlines()) lub input() a wyniki przekazuje na standardowe wyjście sys.stdout za pomocą sys.stdout.writelines() (ewentualnie sys.stdout.write()) lub print().
Oprócz tego program może wymagać obsługi przerwania SIGPIPE, które jest wysyła przykładowo przez head aby poinformować, że rezygnuje z pobierania reszty danych z potoku.
Fakt, że potokownie działa z input() i print() sprawia, że program który uruchomić samodzielnie i
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) while True: try: # pobieranie kolejnej lini (z usunięciem "\n") # aż do pojawienia sie EOF (End Of File) line = input() except EOFError: break # lub exit(0) # miejsce na modyfikowanie lini # wypisanie lini z dodanie "\n" print(line)
bez komentarzy
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) while True: try: line = input() except EOFError: break # lub exit(0) # miejsce na modyfikowanie lini print(line)
Z wykorzystaniem sys.stdin.readline() wymaga sprawdzania czy linia jest pusta zamiast łapania wyjątku EOFError. Ponieważ readline() zwraca linię wraz z końcowym n więc wygodniejsze może być użycie sys.stdout.write() zamiast print(..., end='') choć sys.stdout.write() wymaga samodzielnego konwertowania elementów na string i dodawania spacji między elementami .
dlugosc = len(line) print('Długość linii:', dlugosc, ' znaków w', line, end='') # tak to się odbywa wewnątrz `print()` sys.stdout.write('Długość linii:') sys.stdout.write(' ') # spacje między elementami sys.stdout.write(str(dlugosc)) # ręczna zamiana na string sys.stdout.write(' ') # spacje między elementami sys.stdout.write('znaków w') sys.stdout.write(' ') # spacje między elementami sys.stdout.write(line) sys.stdout.write(end) # przejscie do mowej linii # tak można to sobie skrócić sys.stdout.write('Długość linii: ') # spacje między elementami sys.stdout.write(str(dlugosc)) # ręczna zamiana na string sys.stdout.write(' znaków w ') # spacje między elementami sys.stdout.write(line) # line zawiera już `\n` # lub nawet sys.stdout.write(f'Długość linii: {dlugosc} znaków w {line}') # a dla długich linii można pominąć wstawianie linii do `f''` sys.stdout.write(f'Długość linii: {dlugosc} znaków w ') sys.stdout.write(line)
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) import sys while True: line = sys.stdin.readline() if not line: break # miejsce na modyfikowanie lini sys.stdout.write(line)
Z wykorzystaniem samego sys.stdin' i `for nie wymaga nawet sprawdzania pustej linii.
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) import sys for line in sys.stdin: # miejsce na modyfikowanie lini sys.stdout.write(line)
Przykłady
Numerowanie linii - modyfikacja jest po wypisaniu linii a nie przed
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) number = 1 while True: try: line = input() except EOFError: break print(number, line) number += 1
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) import sys number = 0 while True: line = sys.stdin.readline() if not line: break number += 1 sys.stdout.write(f'{number} ') sys.stdout.write(line) #sys.stdout.write(f'{number} {line}')
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) import sys number = 0 for line in sys.stdin: number += 1 sys.stdout.write(f'{number} ') sys.stdout.write(line) #sys.stdout.write(f'{number} {line}')
Uwagi
Uwaga: sys.stdin.readline() będzie pobierać po jednej lini (i przekazywać dalej) a sys.stdin.read(), sys.stdin.readlines() będzie czekać na całość danych co może wymagać wciśkania Ctrl+D dla potwierdzenia końca danych.
---
I tu właście mógłby być koniec wpisu.
TODO:
- więcej przykładów
- co z Ctrl+C ? except KeyboardException: ?
- odnośnik do potokowania do DataFrame w Pandas czyli "data science command line tools". Powiązanie z książką
- Data Science at the Command Line (GitHub) i artykułem 7 Command-Line Tools for Data Science.
- czytanie z pliku lub potoku gry podany parametr -
- wykorzystanie potokowanie wewnątrz kodu Pythona przy wywoływaniu zewnętrznych programów
- (Komendy shella w Pythonie)
Rozpoznanie potokowania
sys.stdin.isatty() pozwala rozpoznać czy standardowe wejście jest przypisane do terminala (True) czy też potokowane (False).
Aby zobaczyć wynik na ekranie trzeba przekierować print() na standardowe wyjście błędu.
import sys print('przekierowanie stdin :', not sys.stdin.isatty(), file=sys.stderr) print('przekierowanie stdout:', not sys.stdout.isatty(), file=sys.stderr)
$ script.py przekierowanie stdin : False przekierowanie stdout: False $ ls | script.py przekierowanie stdin : True przekierowanie stdout: False $ script.py | head przekierowanie stdin : False przekierowanie stdout: True $ ls | script.py | head przekierowanie stdin : True przekierowanie stdout: True $ script.py < /dev/urandom przekierowanie stdin : True przekierowanie stdout: False $ script.py < /dev/urandom przekierowanie stdin : False przekierowanie stdout: True $ script.py < /dev/urandom > /dev/null przekierowanie stdin : True przekierowanie stdout: True
W przypadku sprawdzania także stderr potrzeba przekierować print() do pliku i potem go wyświetlić.
import sys fh = open('wynik.txt', 'w') print('przekierowanie stdin :', not sys.stdin.isatty(), file=fh) print('przekierowanie stdout:', not sys.stdout.isatty(), file=fh) print('przekierowanie stderr:', not sys.stderr.isatty(), file=fh) fh.close()
$ script.py ; cat wynik.txt przekierowanie stdin : False przekierowanie stdout: False przekierowanie stderr: False $ script.py < /dev/urandom ; cat wynik.txt przekierowanie stdin : True przekierowanie stdout: False przekierowanie stderr: False $ script.py > /dev/null ; cat wynik.txt przekierowanie stdin : False przekierowanie stdout: True przekierowanie stderr: False $ script.py < /dev/urandom > /dev/null ; cat wynik.txt przekierowanie stdin : True przekierowanie stdout: True przekierowanie stderr: False
W przypadku przekierowania stderr do stdout kolejność przekierowań ma znaczenie
$ script.py 2>&1 > /dev/null ; cat wynik.txt przekierowanie stdin : False przekierowanie stdout: True przekierowanie stderr: False $ script.py > /dev/null 2>&1 ; cat wynik.txt przekierowanie stdin : False przekierowanie stdout: True przekierowanie stderr: True
Lub trzeba stosować &> dla przekierowania obu
$ script.py &> /dev/null ; cat wynik.txt przekierowanie stdin : False przekierowanie stdout: True przekierowanie stderr: True
Parametry
Program może także przyjmować argumenty w tradycyjny sposób za pomoca sys.argv i wykorzystywać moduły takie jak standardowy argparse.
Poniższa przeróbka pozwala numerować linie od podanej wartości
import signal signal.signal(signal.SIGPIPE, signal.SIG_DFL) import sys number = 0 if len(sys.argv) > 1: try: number = int(sys.argv[1]) except sys.stderr(f'Parametr musi być liczbą całkowitą: {sys.argv[1}\n') exit(0) for line in sys.stdin: number += 1 sys.stdout.write(f'{number} ') sys.stdout.write(line)
Numeracja linii zacznie się od wartości 5
$ ls | script.py 5 | head
