Funkcje

Posted on 2019-02-17

Pisząc program szybko okazuje się, że całe bloki kodu da się wydzielić jako podprogramy. Jeżeli dodamy możliwość przekazywania parametrów, zagnieżdżania - dostajemy potężne narzędzie. Czas najwyższy na funkcje.

Pierwsze programy to parę prostych kroków - coś wczytujemy, może jakaś pętla. Z czasem sprawy zaczynają się komplikować, dodajemy coraz to nowe opcje, być może zmieniają się warunki. Może przykład więcej powie.

Firma wymaga ode mnie raportu z wynikami z rekrutacji na koniec miesiąca. Odpowiadam za część techniczną rekrutacji i mój raport musi się zgodzić z raportem rekruterki / rekrutera. Robienie tego ręcznie, pod koniec miesiąca przysparza masę kłopotów. Odkręcanie faktur, albo pamiętanie, żeby doliczyć w kolejnym miesiącu. Na szczęście programuję w Pythonie. Mam więc narzędzie, które przegląda katalogi z wynikami z rekrutacji, są tam arkusze Excela, w każdym opis osobnej rekrutacji. Skrypt otwiera taki raport, wyciąga wymagane informacje, tworzy zestawienie. Działa, nawet dobrze. Rzecz w tym, że zrobienie takiego narzędzia bez funkcji jest niewykonalne. W czym problem? Banalna sprawa - format zmienia się w czasie. Istotne informacje znajdują się w innych miejscach, a chcę mieć możliwość analizowania wszystkich formatów. Tylko funkcje.

Funkcja jako podprogram.

Jak wykonywany jest program, tworzone jest miejsce na dane używane przez ten program. Podobnie jest z funkcjami. Każde wykonanie potrzebuje miejsca na dane. Na szczęście Python dba o to za nas.

Funkcja, znaczy coś dostaje i coś zwraca?

Dokładnie tak! Funkcje mają parametry. I zwracają wynik działania. W programie, o którym pisałem można wyobrazić sobie funkcję, która jako parametr dostaje nazwę pliku, a jako wynik zwraca wiersz to miesięcznego raportu.

Funkcje wywołują funkcje

Idąc dalej taka ogólna funkcja musiałaby zawierać wszystkie, możliwe kombinacje. Dodanie kolejnych wersji mogłoby zepsuć istniejące rozwiązanie. Wolę zrobić tak, że ta ogólna funkcja wywołuje funkcje dedykowane każdemu z formatów. Jeżeli funkcja nie rozumie formatu - wtedy zwraca pustą informację. Ogólna funkcja wykonuje kolejne do czasu aż któraś zwróci poprawne dane - jeżeli się nie udało z żadną funkcją. Zły format pliku - nic tego nie będzie.

To może jakiś przydład?

Mówisz - masz. Poniższy kod przegląda podany katalog i podkatalogi w poszukiwaniu plików. Każdy podany plik wypisuje:

import os
PATH = "/home/mzaborow/Documents"

def proc_file(file_name):
    if file_name.endswith(".txt"):
        print("found text file " + os.path.join(PATH, file_name))
    else:
        print("found kind of file " + os.path.join(PATH, file_name))

for file in os.listdir(PATH):
    proc_file(file)

Zmienna PATH to parametr dla całego programu. Wpisz swój katalog - ja pracuję z Linuxem, więc tak wygląda mój katalog z dokumentami. Wpisz swój.

Słowo kluczowe def mówi, że definiujemy funkcję. proc_file to nazwa funkcji, a w nawiasach okrągłych jest lista parametrów. Jeżeli parametrów jest więcej niż jeden - oddzielone są przecinkami. W funkcji do parametrów mamy dostęp przez nazwę parametru - czyli file_name. Parametr jest przekazywany przy wywołaniu - ostatnia linijka programu.

Jeszcze jedno wyjaśnienie os.path.join. Moduł os, ma podmoduł path, gdzie znajdują się funkcje do operowania na ścieżkach. Jedną z nich jest join - łączy podane kawałki w ścieżkę do pliku stosując znaki oddzielające katalogi właściwe dla danego systemu.

Przeszukanie podkatalogów

Nie wiem jak u Was, ale ja to mam dużo katalogów z dokumentami. W pakiecie os jest funkcja walk. Dostajemy dostęp do wszystkich katalogów, list podkatalogów i plików:

import os
PATH = "/home/mzaborow/Documents"

def proc_file(file_name):
    if file_name.endswith(".txt"):
        print("found text file " + os.path.join(PATH, file_name))
    else:
        print("found kind of file " + os.path.join(PATH, file_name))

for dir, subdirs, files in os.walk(PATH):
    for name in files:
        proc_file(name)

No nie wiem. Dokładniej to wiem, że coś jest źle. To może drugie podejście, tym razem wypiszemy więcej informacji:

import os
PATH = "/home/mzaborow/Documents"

def proc_file(dir_name, file_name, subdirs):
    if file_name.endswith(".txt"):
        print("found text file " + os.path.join(dir_name, file_name))
    else:
        print("found kind of file " + os.path.join(dir_name, file_name))
    if subdirs:
        print("... and some subdirectoires: " + subdirs)

for dir, subdirs, files in os.walk(PATH):
    for name in files:
        proc_file(dir, name, subdirs)

Znowu coś? Tak ładnie wszystko wygląda, tyle ma wypisywać, a tu masz:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-9ef001156abe> in <module>()
     12 for dir, subdirs, files in os.walk(PATH):
     13     for name in files:
---> 14         proc_file(dir, name, subdirs)
     15

<ipython-input-7-9ef001156abe> in proc_file(dir_name, file_name, subdirs)
      8         print("found kind of file " + os.path.join(dir_name, file_name))
      9     if subdirs:
---> 10         print("... and some subdirectoires: " + subdirs)
     11
     12 for dir, subdirs, files in os.walk(PATH):

TypeError: must be str, not list

Na szczęście Python dokładnie mówi na czym polega problem i gdzie wystąpił. Tu sprawa wydaje się oczywista łączenie ciągów znaków przy pomocy operatora + (brzmi to strasznie poważnie) wymaga, żeby argumenty były ciągami znaków... A tu się okazało, że jeden jest listą. Zamiast subdirs trzeba wpisać: str(subdirs). W ten sposób wywołamy funkcję, która zamieni listę w ciąg znaków. U mnie działa.

Wywołanie z nazwanymi parametrami

Czasem wygodniej jest podać nazwę parametru. Załóżmy, czysto teoretycznie, że mamy funkcję, która dodaje użytkownika, i przyjmuje paramery: - nazwa użytkownika, - czy założyć katalog domowy? - czy hasło wygasa? - czy użytkownik musi zmienić hasło przy pierwszym logowaniu?

def create_user(user_name, create_home_dir, password_expires, force_password_reset):
    pass

Pierwsza sprawa pass. To jest trochę wytrych. Python wymaga, żeby funkja miała blok instrukcji - tutaj jest tylko przykład, ale zależy mi, żeby można go było wykonać. Dzięki pass mogę pokazać kawałek kodu, który się wykona, ale nic nie zrobi.

A teraz użycie:

create_user('mzaborow', True, False, True)

Mało to mówi, szczególnie jeżeli ktoś inny napisał, albo sami to zrobiliśmy, ale dawno temu. O wiele czytelniej jest napisać:

create_user('mzaborow', create_home_dir=True, password_expires=False, force_password_reset=True)

W ten sposób szansa na pomylenie się jest mniejsza, dokładnie wiadomo co zrobić jak chcemy zmienić któryś z parametrów. Inny przykład:

l = [1,5,3,9,7,15,6,8]
sorted(l, reverse=True)

Wiadomo czego tyczy się parametr True - funkcja sorted zwróci posortowaną listę, ale sortowanie będzie odwrotne.

Wynik działania funkcji

Do zwrócenia danych z funkcji służy return. Można przekazać wyrażenie, wywołanie innej funkcji, albo wartość. Przykład:

def a(b,c):
    return b+c

def c():
    return a(1,4)
c()

U mnie, zgodnie z oczekiwaniem wypisał 5.

Użycie return kończy wykonanie funkcji. Przykład:

def a(b,c):
    print("startig!")
    if (b == 4):
        print("not good b is 4")
        return

    print("b different then 4")
    print("so processing")
    return b+c
a(4,1)
a(3,2)

A co zwróci funkcja a w pierwszym przypadku, czyli jeżeli parametr b jest równy 4? Można wypisać wynik przy pomocy funkcji print.

Sprawa jasna - zrócane jest None.

Jest jeszcze jeden zaawansowany sposób zwracania danych z funkcji - yeld. Tym razem funkcja pamięta w którym miejscu jej wykonywanie się skończyło i ponowne wykonanie zacznie się od tego miejsca. Pewnie brzmi to mało spektakularnie, przykład powie więcej:

def a():
    l = [15, 9, 8, 7, 6, 5, 3, 1]
    for i in l:
        yield i

Tak zdefiniowana funkcja zwraca generator, czyli obiekt, który można przekazać do funkcji next, albo się po nim iterować pętlą for:

g = a()
next(g)
next(g)
next(g)

g = a()
for i in g:
    print(i)

Dalej mało spektakularne? Pomyśl, że lista może być nieskończona... jak ktoś prosi o kolejną wartość, jeżeli tylko mamy formułę - zawsze możemy coś zwrócić. Oszczędność miejsca, czytelność - same plusy. Kolejna miła cecha - standardowe funkcje rozumeją generatory i potrafią się nimi posługiwać. Spróbuj posortować a(). Nie zadziała z nieskończoną funkcją, ale to dlatego, że pamięć komputera jest skończona. Narzędzie nas tu nie ogranicza.

Funkcja w rozumieniu matematycznym

Kolejnym zagadnieniem, czasem poruszanym przy omawianiu funkcji jest rozbieżność między implementacją w komputerze, a tym, co mówi definicja matematyczna. W matematyce, dla każdego argumentu z dziedziny funkcji ma być określona dokładnie jedna wartość. W naszym programie oznacza to, że nie ma tzw. efektów uboczynch, nie używamy zmiennych globalnych, jak gates z przykładowego zadania. Funkcja operuje na parametrach i zwraca wynik. Z jednej strony to nakłada silne ograniczenia na funkcję, ale z drugiej strony - pozwala dużo założyć. Takie funkcje można wykonywać niezależnie, bo są niezależne. W innym wypadku musimy dopuścić ewentualność, że różne funkcje operują na wspólnych zmiennych globalnych i ich niezależne traktownie nie wchodzi w grę.

Wnioski.

Przy okazji omawiania funkcji pokazałem jak przejrzeć katalog, jego podkatalogi i pliki. Potrzebujecie przejrzeć pliki Excela, Worda? A może znaleźć duplikaty w zdjęciach. Do tego wszystkiego są dedykowane biblioteki - wystarczy zmienić funkcję proc_file - tak, żeby robiła to co chcecie. Narzędzie, do generowania raportów, o którym mówiłem na początku jest dostępne publicznie - report.py

Co dalej?

W kolejnym poście omawiam przestrzenie nazw. Łączenie funkcji w skomplikowane konstrukcje daje duże możliwości, ale też wymaga wiedzy.