In [1]:
# setup
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

Skrypty i własne moduły

Istotą języków programowania jest tworzenie zamkniętej całości - narzędzia ktróre można uruchamiać w celu realizacji zadań do którego to narzędzie zostało opracowane. Język Python jest językiem skryptowym, w związku z tym te narzędzia naywa się skryptami. Tworzenie własnych skryptów - programóœ jest istotą programowania. Jeżeli jednak kod zamknie sie w formie skryptu istnieje kolejność zrozumienia:

  • jak skrypt jest zorganizowany
  • jak ponownie wykorzystać gotowe fragmenty kodu dostępne w skryptach
  • jak użytkownik skryptu może się komunikować ze skryptem i sterować jego działaniem

W najprostrzym modelu skrypt może być sekwencją kodu, w którym wpisano wszystkie zmienne sterujące są wpisane na sztwyno. Taki skrypt jest jednorazowego użytku i przeznaczony jest do zautomatyzowania procdur obliczeniowych. Zaletą takich rozwiązań jest że taki skrypt jest przeznaczony dla autora i nie wymaga dodatkowych procedur zabezpieczających czy sprawdzających.

Parametry skryptu

Jeżeli skrypt jest przeznaczony do użytku przez osoby trzecie musi być wyposażony w narzędzia pozwalające na przekazywanie parametrów do skryptu. Parametry skryptu to wartości oddzielane spacjami, które występują po nazwie skryptu. Argumenty przekazane do skryptu są przechowywane w zmiennej sys.arg w formie listy, której pierwszym elementem sys.arg[0] jest nazwa skryptu. Pozostałe elementy to przekazane parametry.

Zawartość pliku arg.py

#!/bin/python3
import sys
print(sys.argv)
print(len(sys.argv))

$ python3 arg.py dane 3 5 2000

['arg.py', 'dane', '3', '5', '2000']
5

Powyższe rozwiązanie jest półśrodkiem: wymaga znajomośc roli poszczególnych pozycji argumentów skryptu, ich ograniczeń, akceptowanych typów, nie zabezpiecza również przed sytuacją podania błędnych argumentów. Pozwala jednak przechwycić wartości, skonwertować je do wymaganego typu i przekazać do dalszych częśći skryptu.

import sys

dane=sys.argv[1]
liczba_wierszy = int(sys.argv[2])
liczba_kolumn = int(sys.argv[3])
prog = float(sys.argv[4])

print("dane: {0:%<s}, liczba wierszy: {1:%<d}, \
    liczba kolumn: {2:%<d}, próg wartości:  {3:%<f}". 
    format(dane,liczba_wierszy,liczba_kolumn,prog))

Moduł argparse

Narzędziem pozwalającym na zbudowanie uniwersalnego interface użytkownika (w trybie tekstowym) skryptu jest moduł argparse z biblioteki standardowej. Moduł ten pozwala zbudować parser argumentów i jednocześnie dostarczyć podstawowy system pomocy dla skryptu.

Polecenie utworzenia nowego parsera ma wiele opcji, z których na tym etapie istotne jest tylko jeden:

  • description - zawierający opis co dany skrypt wykonuje
import argparse

# parser
parser = argparse.ArgumentParser(description='File process text file and print summary.')
args = parser.parse_args()

Ostatnim poleceniem parsera jest funkcja .parser_args(), która analizuje porawność wprowadzonych argumentów

Parser nie ma sensu jeżeli nie przetwarza żadnych argumentów. Polecenie .add_argument(name/flags, [paramteres]) pozwala dodać argumenty wejściowe. Najważniejsze opcje to:

  • name/flag - nazwa argumentu pozycyjnego lub argumentu opecjonalnego
  • default - wartość domyślna
  • type - typ danych. Może to być jeden z wbudowanych typów danych lub funkcja, która jest wykonywana na wprowadzonym argumencie (np. open())
  • help - opis co dany argument robi
  • choices - lista opcji do wyboru

Więcej na temat definiowania argumentów można przeczytać w dokumentacji modułu.

[https://docs.python.org/3/library/argparse.html]

import argparse

# parser
parser = argparse.ArgumentParser(description='File process text file and print summary.')
parser.add_argument('file',
    help='file to process')
parser.add_argument('--rows', default=0, type=int,
    help='Numbers of rows to process, 0, to all')
parser.add_argument('--cols', default=-1, type=int,
    help='Numbers of columns to process, 0 to all')
parser.add_argument('--thresh', default=0, type=float,
    help='Threshold to accept values')
parse.add_argument('--ext', action='store_true',
    help='Calculate extended statistics')
args = parser.parse_args()
print(args)
$ python3 arg.py --help
usage: arg.py [-h] [--rowskip ROWSKIP] [--cols COLS] [--thresh THRESH] file

File process text file and print summary.

positional arguments:
  file               file to process

optional arguments:
  -h, --help         show this help message and exit
  --rows ROWS        Numbers of rows to process, 0, to all
  --cols COLS        Numbers of columns to process, 0 to all'
  --thresh THRESH    Threshold to accept values
  --ext              Calculate extended statistics

Jeżeli nie wiemy jakie parametry możemy prekazać do skryptu możemy użyć flagi --help. W tej sytuacji skrypt wyświetli nam jakie parametry możemy przekazać i jaka jest ich rola. Jeżeli parametry mają wartości domyślne należy to zaznaczyć w opisie.

$ python3 arg.py --rowskip=1 --thresh=10 --cols=20 dane.txt
Namespace(cols=20, file='dane.txt', rows=1, thresh=10.0)

Parametry, które mają wartości domyślne nie muszą być przekazywane do skryptu. Opcja typ sprawdza, czy przekazany argument jest odpowiedniego typu. Jeżeli nie będzie parser zgłosi błąd.

$ python3 arg.py --rowskip=1 --thresh=10 --cols=20.1 dane.txt
usage: arg.py [-h] [--rows ROWS] [--cols COLS] [--thresh THRESH] file
arg.py: error: argument --cols: invalid int value: '20.1'

Note: argumenty pozycyjne i opcjonalne

Argumenty przekazywane do skryptu dzielą się na trzy grupy:

  • pozycyjne - są zawsze wymagane i muszą znajdować się w określonym miejscu, najczęściej jest to nazwa zbioru wejściowego
  • opcjonalne - są definiowane przez nazwę poprzedzoną -- lub - w wersji skkróconej, nie muszą być wymagane, ale można zaznaczyć że są, mogą posiadać wartości domyślne, odgórnie zdefinowaną listę wyboru, a w zaawansowanych opcjach liczbę argumentów, czy rodzaj akcji
  • flagi logiczne - bezparametrowe flagi, które wyłącznie włączają lub wyłączają jaką opcję. W przykładzie powyżej taką rolę pełni --ext

Wielokrotne użycie kodu ze skryptów

W pracy zawodowej programiści czy analitycy danych rzadko zajmują się bardzo szerokim spektrum zagadnień. Najczęściej ludzie specjalizują się w mniej lub bardziej wąskiej dziedzinie aktywności. Oznacza to, że wiele rozwiązań - kodu raz napisanego, przetestowanego i zapisanego w formie funkcji może być wielokrotnie wykorzystywanych również w innych skryptach. Z tego powodu w praktyce nie pisze się skryptów jako ciągu poleceń, ale dzieli się go na logicznie zdefiniowane funkcje, które następnie wywołuje się w części głównej skryptu. Prawidłowo napisany skrypt powinien mieć następującą strukturę:

  1. Fragmenty kodu, które chcemy uwspólnić powinny być wydzielone w funkcje
  2. Następnie definiujemy główną część skryptu zaczynającą się od if __name__ == "__main__": jest to sprawdzenie czy zmienna wbudowana __name__ zawiera wartość __main__ oznaczającą że jest to główny blok skryptu.
  3. Funkcja jest wywoływana i wykonywana
  4. Wykonywane są kolejne fragmenty kodu, niezwiązane z funkcją.

skrytpt skr.py:

#!/usr/bin/python3
def moja_funkcja(): #1
    print("Moja funkcja została wykonana")

if __name__ == "__main__": #2
    moja_funkcja() #3
    print("Kolejne fragmenty kodu poza funkcją..") #4

>>> $ python3 skr.py
Moja funkcja została wykonana
Kolejne fragmenty kodu..

Jeżeli chcemy teraz użyć kodu moja_funkcja w innym skrypcie, wystarczy ją zaimportować i uruchomić:

skrytpt inny_skr.py:

#!/usr/bin/python3
from skr import moja_funkcja # bez rozszerzenia i nawiasów

if __name__ == "__main__":
    moja_funkcja() # wywołanie
    print("A to zupełnie inny sktypt..")

>>> $ python3 inny_skr.py
Moja funkcja została wykonana
to zupełnie inny sktypt..

Tworzenie własnych modułów

Moduł jest zbiorem zmiennych oraz funcji, które zostały pogrupowane w jeden zbiór. W najprostrzej wersji, jeśli zapiszemy w jednym pliku utworzone przez nas funkcje, zmiene i klasy – stworzymy własny moduł. Do funkcji i zmiennych mamy dostęp taki sam jak do innych modułów - poprzez zaimportowanie całego modułu lub import pojedynczych funkcji. Listę dostępnych funkcji możemy uzyskać wykonując znane już polecenie dir na nazwie modulu:

import mojmodul
dir(mojmodul)
...

Jeżeli jednak nasz potencjalny moduł staje się rozbudowany, zasadnym jest podział go na kilka plików, z których każdy będzie miał zindywidualizowaną funkcjonalność. W takiej sytuacji tworzymy katalog, który będzie nazwą naszego modułu, następnie umieszczamy w nim plik __init.py__ oraz pliki z nazwami submodułów zawierających funkcje. Taki katalog możemy nazywać pakietem (ang. package):

.
└── mojpakiet
    ├── __init__.py
    └── mojmodul.py

Dostęp do funkcji z takiego pakietu można zrealizować na trzy sposoby:

import mojpakiet
mojpakiet.mojmodul.moja_funckcja(...)

from mojpakiet import mojmodul
mojmodul.moja_funckcja(...)

from mojpakiet.mojmodul import moja_funkcja
moja_funckcja(...)

Note: dostęp do modułów

Jeżeli nasze moduły/pakiety przechowujemy w jednym miejscu i nie jest to biblioteka systemowa ani katalog z którego uruchamiamy skrypt należy w skrypcie wskazć lokalizację tego katalogu poprzez dołączenie ścieżki dostępu do modułów. Jeżeli jednak skrypt, który wykorzystuje nasze funkcje zamierzamy rozpowszechniać należy go rozpowszechniać wraz z naszym modułem.

sys.path.append(os.path.join(os.path.expanduser("~"),"moje_moduly")