Python - wprowadzenie

12 - Funkcje, moduły i pakiety

Funkcje

Funkcja to wydzielona część programu, która wykonuje określone zadania. Dotychczas używaliśmy wielu istniejących w Pythonie funkcji (np. print() czy len()), teraz przyszedł czas, aby nauczyć się pisać własne. Nazwy funkcji rządzą się podobnymi zasadami jak wspomniane wcześniej nazwy zmiennych, ważne jest, aby jasno wskazywały na swoje przeznaczenie. Pewne szczególne przypadki opiszę później.

Tworzenie funkcji przynosi wiele korzyści:

  • kod jest bardziej przejrzysty
  • łatwiej jest modyfikować kod
  • funkcję można wywoływać wielokrotnie w programie - unika się powtarzania tego samego kodu.

Funkcje bez argumentów i zwracanych wartości

Zacznijmy od bardzo prostego przykładu:

def witaj():
    print("Witaj Świecie!")

witaj()
Witaj Świecie!

Jak widać, definiowanie funkcji zaczynamy od słowa kluczowego def, następnie podajemy nazwę funkcji oraz parę nawiasów okrągłych, oraz dwukropek. Podobnie jak w przypadku np. konstrukcji if, czy while, blok kodu, który zawiera funkcja, jest określony przez wcięcie. Zauważ, że funkcję zdefiniowaliśmy w naszym kodzie przed jego użyciem. Spróbuj zrobić odwrotnie:

probna_funkcja()

def probna_funkcja():
    print("Kolejność ma znaczenie!")
-----------------------------------------------------

NameError           Traceback (most recent call last)

<ipython-input-16-874e15bd829d> in <module>
----> 1 probna_funkcja()
      2 
      3 def probna_funkcja():
      4     print("Kolejność ma znaczenie!")

NameError: name 'probna_funkcja' is not defined

Tak, kolejność ma znaczenie. W powyższym kodzie wywołaliśmy funkcję, zanim została ona zdefiniowana, zatem nie mogła być użyta.

Funkcje przyjmujące jeden argument

Naszym pierwszym przykładem była bardzo prosta funkcja, która ani nie przyjmowała, ani nie zwracała żadnych wartości. Zmodyfikujmy nieco kod:

def witaj(imie):
    print(f"Witaj {imie}!")

imie_uzytkownika = input("Podaj swoje imię: ")
witaj(imie_uzytkownika)
Podaj swoje imię:  Hammurabi

Witaj Hammurabi!

Zwróć uwagę, że w definicji funkcji, w parze nawiasów, znalazła się nazwa zmiennej (parametr), która jest następnie wykorzystywana w kodzie funkcji. Przy wywołaniu funkcji, w parze nawiasów przekazujemy do niej dane (argument(y)). Może to być nazwa zmiennej przechowującej wartość, także inna niż ta, użyta w definicji funkcji. Można też przekazać dane, tu ciąg znaków, bezpośrednio, np. witaj("Hammurabi"). Powyższa funkcja przyjmowała dane, ale ich nie zwracała. Zmieńmy to:

def witaj(imie):
    powitanie = f"Witaj {imie}!"
    return powitanie

imie_uzytkownika = input("Podaj swoje imię: ")
tekst = witaj(imie_uzytkownika)
print(tekst)
Podaj swoje imię:  Atahualpa

Witaj Atahualpa!

Słowo kluczowe return odpowiada za przekazanie następującej po nim wartości ,,na zewnątrz’’ funkcji, gdzie może być wykorzystana, w powyższym kodzie przypisana do zmiennej tekst. Kod ten można skrócić:

def witaj(imie):
    return f"Witaj {imie}!"

imie_uzytkownika = input("Podaj swoje imię: ")
print(witaj(imie_uzytkownika))
Podaj swoje imię:  Atahualpa

Witaj Atahualpa!

Zamiast użycia zmiennej w funkcji, użyliśmy po słowie kluczowym return wyrażenie, które tworzyło łańcuch znaków, następnie zwracany przez funkcję. Także w ostatniej linii, zamiast zmiennej umieściliśmy wywołanie funkcji witaj() zwracający łańcuch, który od razu jest przekazywany funkcji print(). Idąc tym tropem, w zasadzie można też kod jeszcze bardziej skrócić:

def witaj(imie):
    return f"Witaj {imie}!"

print(witaj(input("Podaj swoje imię: ")))
Podaj swoje imię:  Atahualpa

Witaj Atahualpa!

Kod jest krótszy, ale zrobił się mniej czytelny.

Spróbujmy sprawdzić, co zwraca funkcja, która nie powinna nic zwrócić:

def czarna_dziura(cokolwiek):
    print(f"Czarna dziura wchłonęła: {cokolwiek}")

obiekt = "gwiazda"
rezultat = czarna_dziura(obiekt)
print(f"""Do czarnej dziury wpadł(a): {obiekt}, 
wydostał(a) się: {rezultat}""")
print(type(rezultat))
Czarna dziura wchłonęła: gwiazda
Do czarnej dziury wpadł(a): gwiazda, 
wydostał(a) się: None
<class 'NoneType'>

Jak widać, został zwrócony obiekt None typu NoneType. Jest to szczególny obiekt, nie należy go mylić np. z False, czy wartością 0.

Funkcje przyjmujące wiele argumentów

Jak już wspomniałem w lekcji ,,03 - Łańcuchy znaków - cz. I’’, funkcja może przyjmować więcej niż jeden argument. Oczywiście, należy to uwzględnić, wymieniając w definicji odpowiednie parametry funkcji:

def witaj(imie, przydomek):
    powitanie = f"Witaj {imie} zwany {przydomek}!"
    return powitanie

imie_uzytkownika = input("Podaj swoje imię:     ")
przydomek_uzytkownika = input("Podaj swój przydomek: ")
tekst = witaj(imie_uzytkownika, przydomek_uzytkownika)
print(tekst)
Podaj swoje imię:      Ragnar
Podaj swój przydomek:  Lodbrok

Witaj Ragnar zwany Lodbrok!

Zauważ, że odpowiednie argumenty powinny być podawane w tej samej kolejności, jak odpowiadające im parametry funkcji. Inaczej zostaną błędnie przypisane. Zamieńmy miejscami imie_uzytkownika i przydomek_uzytkownika przy wywołaniu funkcji:

def witaj(imie, przydomek):
    powitanie = f"Witaj {imie} zwany {przydomek}!"
    return powitanie

imie_uzytkownika = input("Podaj swoje imię:     ")
przydomek_uzytkownika = input("Podaj swój przydomek: ")
tekst = witaj(przydomek_uzytkownika, imie_uzytkownika)
print(tekst)
Podaj swoje imię:      Kazimierz
Podaj swój przydomek:  Wielki

Witaj Wielki zwany Kazimierz!

Można też używać nazw parametrów przy przekazywaniu wartości do funkcji, wtedy nie jest konieczne zachowanie kolejności.

def witaj(imie, przydomek):
    powitanie = f"Witaj {imie} zwany {przydomek}!"
    return powitanie

imie_uzytkownika = input("Podaj swoje imię:     ")
przydomek_uzytkownika = input("Podaj swój przydomek: ")
tekst = witaj(przydomek = przydomek_uzytkownika, imie = imie_uzytkownika)
print(tekst)
Podaj swoje imię:      Kazimierz
Podaj swój przydomek:  Wielki

Witaj Kazimierz zwany Wielki!

Wartości domyślne

Przy definiowaniu funkcji można przypisać parametrom wartości domyślne. Jeśli przy wywołaniu funkcji nie zostaną jej przekazane odpowiednie dane, zostaną użyte wartości domyślne. UWAGA: w tym przypadku nie używaj spacji ani przed, ani po znaku =:

def witaj(imie, przydomek='Szary'):
    powitanie = f"Witaj {imie} zwany {przydomek}!"
    return powitanie

imie_uzytkownika = input("Podaj swoje imię:     ")
przydomek_uzytkownika = input("Podaj swój przydomek (jeśli nie chcesz podać, wciśnij Enter): ")

if len(przydomek_uzytkownika) > 0:
    # Użytkownik podał przydomek
    tekst = witaj(imie_uzytkownika, przydomek_uzytkownika)
else:
    # Użytkownik nie podał przydomku
    tekst = witaj(imie_uzytkownika)
print(tekst)
Podaj swoje imię:      Gandalf
Podaj swój przydomek (jeśli nie chcesz podać, wciśnij Enter):  

Witaj Gandalf zwany Szary!

Funkcje przyjmujące dowolną liczbę argumentów

Czasem nie jesteśmy w stanie z góry określić ile argumentów będzie przekazanych do funkcji. W takim przypadku definiujemy jeden parametr poprzedzony znakiem *. Taki parametr często w Pythonie nazywa się *args, choć nie jest to nazwa obowiązkowa. Stwórzmy prostą funkcję sumującą kwadraty podanych liczb, przy czym może ich być dowolnie wiele:

def sumuj_kwadraty(*args):
    print(args)
    suma_kwadratow = 0
    for liczba in args:
        suma_kwadratow += liczba ** 2
    print(f"Suma kwadratów = {suma_kwadratow}")

sumuj_kwadraty(1, 2, 3)
sumuj_kwadraty(10, 20, 30, 40, 50, 60)
(1, 2, 3)
Suma kwadratów = 14
(10, 20, 30, 40, 50, 60)
Suma kwadratów = 9100

W takiej sytuacji argumenty są zapisane w funkcji w krotce, zatem w prosty sposób możemy je uzyskać i wykorzystać w kodzie. Można też zdefiniować określone parametry oraz parametr przyjmujący dowolną liczbę argumentów. W takim przypadku należy ten ostatni umieścić na końcu.

def sumuj_kwadraty(opis, *liczby):
    suma_kwadratow = 0
    for liczba in liczby:
        suma_kwadratow += liczba ** 2
    print(f"Dla: {opis}, suma kwadratów = {suma_kwadratow}")

sumuj_kwadraty('Liczby pierwsze', 1, 3, 5, 7, 11, 13)
sumuj_kwadraty('Dziesiątki', 10, 20, 30, 40)
Dla: Liczby pierwsze, suma kwadratów = 374
Dla: Dziesiątki, suma kwadratów = 3000

Przekazywanie argumentów w postaci par klucz-wartość

Innym sposobem przekazywania niekreślonej z góry liczby argumentów, jest przekazywanie ich w formie par klucz-wartość. Wtedy, przed nazwą parametru umieszcza się dwie gwiazdki **. W takiej sytuacji często używa się nazwy **kwargs, choć podobnie jak w przypadku *args, nie jest to obowiązkowe. Podane pary są przechowywane, co nie powinno być zaskakujące, w słowniku:

def opisz_organizm(organizm, **kwargs):
    print(f"\n---{organizm}---")
    for klucz, wartosc in kwargs.items():
        print(f"{klucz}: {wartosc}")
        
opisz_organizm('kukurydza',
               Królestwo="rośliny",
               Klad="rośliny nasienne",
               Klasa="okrytonasienne",
               Rząd="wiechlinowce",
               Rodzina="wiechlinowate",
               Rodzaj="kukurydza")
opisz_organizm('człowiek',
               Królestwo="zwierzęta",
               Typ="strunowce",
               Podtyp="kręgowce",
               Gromada="ssaki",
               Rząd="naczelne",
               Rodzina="człowiekowate",
               Rodzaj="człowiek",
               Gatunek="człowiek rozumny",)
---kukurydza---
Królestwo: rośliny
Klad: rośliny nasienne
Klasa: okrytonasienne
Rząd: wiechlinowce
Rodzina: wiechlinowate
Rodzaj: kukurydza

---człowiek---
Królestwo: zwierzęta
Typ: strunowce
Podtyp: kręgowce
Gromada: ssaki
Rząd: naczelne
Rodzina: człowiekowate
Rodzaj: człowiek
Gatunek: człowiek rozumny

Wiele zwracanych wartości

Funkcja może także zwracać wiele wartości:

def oblicz(liczba_1, liczba_2):
    suma = liczba_1 + liczba_2
    roznica= liczba_1 - liczba_2
    return suma, roznica

suma_liczb, roznica_liczb = oblicz (20, 10)
print(f"{suma_liczb=}, {roznica_liczb=}")
suma_liczb=30, roznica_liczb=10

Komentarze opisujące funkcje - docstrings

Warto jeszcze wspomnieć o opisywaniu działania funkcji. Powinno się to robić w formie komentarza zamkniętego w trzech podwójnych cudzysłowach (""") z każdej strony, umieszczonym zaraz na początku treści funkcji. Taki opis nazywa się docstring. Może mieć jedną linię:

def sumuj_kwadraty(opis, *liczby):
    """Sumuje kwadraty podanych liczb."""
    suma_kwadratow = 0
    for liczba in liczby:
        suma_kwadratow += liczba ** 2
    print(f"Dla: {opis}, suma kwadratów = {suma_kwadratow}")

Może też być wielolinijkowy. W takim przypadku pierwsza linia powinna być krótkim opisem funkcji (nie powinna przekraczać 80 znaków), po niej powinna się znaleźć pusta linia, a następnie dalszy opis.

def sumuj_kwadraty(opis, *liczby):
    """Sumuje kwadraty podanych liczb. 
    
    Przyjmuje łańcuch znaków (opis) oraz dowolną liczbę liczb.
    Liczby są kolejno podnoszone do kwadratu a otrzymane
    wartości są sumowane.
    
    Opis i suma kwadratów są drukowane.  
    
    Parametry:
        opis:    Opis podanych danych
        *liczby: liczby, na których przeprowadzane są obliczenia
    """
    suma_kwadratow = 0
    for liczba in liczby:
        suma_kwadratow += liczba ** 2
    print(f"Dla: {opis}, suma kwadratów = {suma_kwadratow}")

Opis funkcji można pobrać umieszczając po kropce __doc__ (po dwa podkreślniki z każdej strony!). Przy okazji warto wspomnieć, że używając __name__, możemy uzyskać nazwę funkcji:

print(f"Opis funkcji '{sumuj_kwadraty.__name__}()':")
print(sumuj_kwadraty.__doc__)
Opis funkcji 'sumuj_kwadraty()':
Sumuje kwadraty podanych liczb. 
    
    Przyjmuje łańcuch znaków (opis) oraz dowolną liczbę liczb.
    Liczby są kolejno podnoszone do kwadratu a otrzymane
    wartości są sumowane.
    
    Opis i suma kwadratów są drukowane.  
    
    Parametry:
        opis:    Opis podanych danych
        *liczby: liczby, na których przeprowadzane są obliczenia

Można też użyć funkcji help():

help(sumuj_kwadraty)
Help on function sumuj_kwadraty in module __main__:

sumuj_kwadraty(opis, *liczby)
    Sumuje kwadraty podanych liczb. 
    
    Przyjmuje łańcuch znaków (opis) oraz dowolną liczbę liczb.
    Liczby są kolejno podnoszone do kwadratu a otrzymane
    wartości są sumowane.
    
    Opis i suma kwadratów są drukowane.  
    
    Parametry:
        opis:    Opis podanych danych
        *liczby: liczby, na których przeprowadzane są obliczenia

Zwróć uwagę na różnice w obu przypadkach.

Więcej na temat konwencji stosowanych w pisaniu docsring-ów można przeczytać np. pod adresami:

Typy argumentów i zwracanych wartości

W powyższych przykładach jako argumentów, a także zwracanych wartości, używaliśmy dość prostych typów danych jak liczby czy łańcuchy znaków. Możemy także używać bardziej złożonych struktur danych. Przy okazji wywołamy napisane przez nas funkcje z innych funkcji:

def srednia(dane):
    """Oblicza średnią liczb."""
    return sum(dane)/len(dane)

def wariancja(dane):
    """Oblicza wariancję dla liczb."""
    suma_kwadratow_roznic = 0
    srednia_liczb = srednia(dane)
    for liczba in dane:
        suma_kwadratow_roznic += (liczba - srednia_liczb)**2
    return suma_kwadratow_roznic/len(dane)

def oblicz(dane):
    """Opisuje serię liczb."""
    wyniki = {
        'suma': sum(dane),
        'najmniejsza': min(dane),
        'największa': max(dane),
        'liczba liczb' : len(dane),
        'srednia': srednia(dane),
        'wariancja': wariancja(dane)  
    }
    return wyniki
    
lista = [9, 4, 12, 11, 11, 9, 8, 10]

wyniki = oblicz(lista)

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")
suma         = 74
najmniejsza  = 4
największa   = 12
liczba liczb = 8
srednia      = 9.25
wariancja    = 5.4375

Moduły

Tworzenie i import modułu

Funkcje, jak napisałem powyżej, pozwalają na dzielenie kodu na mniejsze elementy, które mogą być wykorzystane. Na razie jednak wciąż pozostajemy w obrębie jednego pliku z kodem. W przypadku dłuższych programów, warto podzielić go na więcej mniejszych plików, ale tak, żeby można było korzystać z kodu zapisanego w innych plikach. Temu właśnie służy tworzenie modułów.

Użyjmy powyższego kodu, aby pokazać jak to działa. Podzielmy go na dwa pliki i nieco zmodyfikujmy. Oba zapisz w jednym katalogu.

Plik 1: obliczenia.py:

"""Moduł zawierający funkcje do obliczeń."""

def srednia(dane):
    """Oblicza średnią liczb."""
    return sum(dane)/len(dane)

def wariancja(dane):
    """Oblicza wariancję dla liczb."""
    suma_kwadratow_roznic = 0
    srednia_liczb = srednia(dane)
    for liczba in dane:
        suma_kwadratow_roznic += (liczba - srednia_liczb)**2
    return suma_kwadratow_roznic/len(dane)

def oblicz(dane):
    """Opisuje serię liczb."""
    wyniki = {
        'suma': sum(dane),
        'najmniejsza': min(dane),
        'największa': max(dane),
        'liczba liczb' : len(dane),
        'srednia': srednia(dane),
        'wariancja': wariancja(dane)  
    }
    return wyniki

Plik2: statystyka.py (nie przepisuj znaków ①, ② ):

"""Prosta analiza listy liczb."""
# import modułu obliczenia
import obliczenia 

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = obliczenia.oblicz(lista) 

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

Po uruchomieniu pliku statystyka.py pojawia się wynik:

suma         = 74
najmniejsza  = 4
największa   = 12
liczba liczb = 8
srednia      = 9.25
wariancja    = 5.4375

Teraz program został podzielony na dwa osobne pliki. Funkcje srednia(), wariancja() oraz oblicz() przenieśliśmy do osobnego modułu - pliku o nazwie statystyka.py. Na jego początku umieściliśmy komentarz, wyjaśniający co w tym module się znajduje. W drugim pliku, o nazwie statystyka.py znajduje się pozostała część kodu, ten plik uruchamiamy, jako główny punkt programu. Na początku znajduje się komentarz, później (①) instrukcja import, po której umieściliśmy nazwę importowanego modułu. Odpowiada on nazwie pliku modułu, bez przedłużenia .py. Funkcję znajdującą się w module wywołujemy w linii oznaczonej ②. Zauważ, że najpierw znajduje się nazwa modułu, później nazwa funkcji.

Import wybranych funkcji z modułu

Jeśli potrzebujemy w głównym kodzie jedną lub tylko kilka funkcji z modułu, można to zrobić tak:

from obliczenia import oblicz, srednia

W takim przypadku, wywołując te funkcje, nie musimy odwoływać się do nazwy modułu:

"""Prosta analiza listy liczb."""

# import modułu obliczenia
from obliczenia import oblicz, srednia

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = oblicz(lista)

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

print(f"Średnia: {srednia(lista)}")
suma         = 74
najmniejsza  = 4
największa   = 12
liczba liczb = 8
srednia      = 9.25
wariancja    = 5.4375
Średnia: 9.25

Przy takim podejściu nalezy zwracać uwagę, czy nazwy funkcji z importowanego modułu nie pokrywają się z nazwami w pliku, w którym dokonujemy importu. W takim przypadku zostanie wywołana funkcja z pliku głównego.

Można tego uniknąć, używając aliasu (zob. niżej)

Import wszystkich funkcji z modułu

Jeśli chcemy zaimportować wszystkie funkcje z modułu, nie musimy ich wszystkich wymieniać. Wystarczy zastosować znak *:

"""Prosta analiza listy liczb."""
# import modułu obliczenia
from obliczenia import *

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = oblicz(lista)

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

print(f"Średnia:   {srednia(lista)}")
print(f"Wariancja: {wariancja(lista)}")
suma         = 74
najmniejsza  = 4
największa   = 12
liczba liczb = 8
srednia      = 9.25
wariancja    = 5.4375
Średnia:   9.25
Wariancja: 5.4375

Import modułu z użyciem aliasu

Czasem importujemy cały moduł, ale jego używamy innego modułu o tej samej nazwie, albo nazwa jest długa i chcemy oszczędzić czas na jej wpisywaniu, lub z innych powodów chcielibyśmy jego nazwę zmienić. W takim przypadku użyjemy słowa kluczowego as do utworzenia aliasu (①), czyli nazwy, pod którą będzie występował w kodzie (②):

"""Prosta analiza listy liczb."""
# import modułu obliczenia
import obliczenia as obl 

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = obl.oblicz(lista) 

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

Aliasu można też użyć dla importowanej funkcji. Pozwala to m.in. uniknąć konfliktu nazw, kiedy funkcje o tej samej nazwie występują w pliku głównym i module, lub też w różnych modułach:

from nazwa_modulu import nazwa_funkcji as alias_funkcji

Wtedy wywołujemy zaimportowaną funkcję, używając jej aliasu.

Import wielu modułów

Kiedy będziesz tworzyć coraz bardziej złożone programy, może się okazać, że plik z modułem zawiera wiele funkcji o bardzo różnych zastosowaniach. W takim wypadku warto podzielić je na wiele modułów, zawierających funkcje podzielone ,,tematycznie’’, np. oddzielnie umieścić funkcje do obliczeń statystycznych, oddzielnie do wizualizacji danych a w kolejnym module zapisać funkcje potrzebne do komunikacji z użytkownikiem. Nie tylko ułatwia to kontrolę nad kodem i zwiększa jego przejrzystość, ale pozwala łatwo wykorzystać poszczególne moduły w innych programach.

Kiedy importujemy wiele modułów, możemy wpisywać kolejne polecenia import, ale można też umieszczać nazwy modułów po przecinku:

import modul_1
import modul_2
import modul_3, modul_4

Pakiety

Własne pakiety

Moduły można też umieścić w osobnych katalogach. Katalog z plikami .py tworzy pakiet. Taki katalog może zawierać kolejne katalogi z plikami zawierającymi kod Pythona. Dla celów dydaktycznych rozpatrzmy jednak prosty przykład.

W katalogu, w którym są zapisane pliki statystyka.py i obliczenia.py utwórz katalog moduly i przenieś do niego katalog obliczenia.py. Teraz zmodyfikuj nieco plik statystyka.py (①, ②):

"""Prosta analiza listy liczb."""
# import modułu obliczenia
import moduly.obliczenia 

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = moduly.obliczenia.oblicz(lista) 

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

Wywołanie funkcji w linii ② wymaga wpisania wielu znaków, zatem łatwiej będzie stworzyć alias (①, ②):

"""Prosta analiza listy liczb."""
# import modułu obliczenia
import moduly.obliczenia as obl 

lista = [9, 4, 12, 11, 11, 9, 8, 10]
# wywołanie funkcji oblicz z modułu obliczenia
wyniki = obl.oblicz(lista) 

for klucz, wartosc in wyniki.items():
    print(f"{klucz: <12s} = {wartosc}")

Pakiety z Biblioteki Standardowej Pythona

W poprzedniej lekcji użyliśmy polecenia import copy. Najwyraźniej zaimportowaliśmy moduł, nie był on jednak częścią pisanego przez nas programu. Przy instalacji Pythona, zainstalowaliśmy także Standardową Bibliotekę Pythona (ang. Python Standard Library). Zawiera ona mnóstwo modułów przydatnych w bardzo różnych sytuacjach. Dokumentację można znaleźć pod adresem https://docs.python.org/pl/3.8/library/index.html, krótki przewodnik znajduje się na stronie https://docs.python.org/pl/3.8/tutorial/stdlib.html.

W następnej lekcji przyjrzymy się wybranym modułom i funkcjom dostępnym w Standardowej Bibliotece Pythona.

Jeszcze trochę o funkcjach

Modyfikacja obiektów przekazanych funkcji

Przekazując funkcji takie obiekty jak listy, należy zwrócić uwagę na pewien potencjalny problem. Zobaczmy to na przykładzie. Powiedzmy, że mamy jakąś sekwencję DNA początkową, przechowywaną jako lista kolejnych znaków oznaczających zasady. Piszemy funkcję, która ma za zadanie dodanie podanej zasady i zwrócenie kopii listy. Sprawdźmy działanie kodu

sekwencja = ['A', 'T', 'G', 'C']
def dodaj_nukleotyd(sekw, nukleotyd):
    sekw.append(nukleotyd)
    return sekw
sekwencja_dluzsza = dodaj_nukleotyd(sekwencja, 'G')

print(sekwencja_dluzsza)
['A', 'T', 'G', 'C', 'G']

Wygląda na to, że wszystko działa zgodnie z założeniami, ale sprawdźmy, jaka jest zawartość sekwencji początkowej sekwencja:

print(sekwencja)
['A', 'T', 'G', 'C', 'G']

Jak widać, początkowa lista została także zmodyfikowana. Dlaczego tak jest, omawialiśmy wcześniej. Zwróć uwagę, że jeśli przekazujemy tego typu obiekty do funkcji jako argument, to może ona zostać zmodyfikowana wewnątrz funkcji, a efekty tego są widoczne także poza nią. Zastanów się, jak można zmodyfikować powyższy kod tak, żeby nie modyfikował listy sekwencja.

Można to zrobić np. tak:

sekwencja = ['A', 'T', 'G', 'C']
def dodaj_nukleotyd(sekw, nukleotyd):
    # kopiowanie listy i przypisanie kopii do nazwy sekw
    sekw = sekw[:]
    sekw.append(nukleotyd)
    return sekw
sekwencja_dluzsza = dodaj_nukleotyd(sekwencja, 'G')

print(sekwencja_dluzsza)
print(sekwencja)
['A', 'T', 'G', 'C', 'G']
['A', 'T', 'G', 'C']

Można też użyć metodę copy() (lub, jeśli jest taka potrzeba, deepcopy()):

sekwencja = ['A', 'T', 'G', 'C']
def dodaj_nukleotyd(sekw, nukleotyd):
    # kopiowanie listy i przypisanie kopii do nazwy sekw
    sekw = sekw.copy()
    sekw.append(nukleotyd)
    return sekw
sekwencja_dluzsza = dodaj_nukleotyd(sekwencja, 'G')

print(sekwencja_dluzsza)
print(sekwencja)
['A', 'T', 'G', 'C', 'G']
['A', 'T', 'G', 'C']

Przekazywanie funkcji do funkcji

Funkcja może przyjąć jako argument każdy obiekt, a ponieważ w Pythonie w zasadzie wszystko jest obiektem, jest nim także funkcja. Można zatem przekazać funkcji także funkcję jako argument:

def przywitaj_swiat():
    print("Witaj Świecie!")

def przywitaj_uzytkownika():
    imie = input("Podaj imię: ")
    print(f"Witam użytkownika o imieniu: {imie}")
    
def przywitaj(funkcja):
    funkcja()

przywitaj(przywitaj_swiat)
przywitaj(przywitaj_uzytkownika)
    
Witaj Świecie!

Podaj imię:  Mirmił

Witam użytkownika o imieniu: Mirmił

Zauważ, że przekazując funkcję do innej funkcji, używamy tylko jej nazwy, bez pary nawiasów.

Jeśli funkcja, którą wywołujemy wewnątrz funkcji, wymaga przekazania argumentów, musimy je przekazać jako oddzielne argumenty:

def podaj_dlugosc_lancucha(lancuch):
    """Funkcja podaje liczbę znaków w łańcuchu."""
    print(f"Długość łańcucha \"{lancuch}\" to {len(lancuch)} znaków.")

def podaj_sume_cyfr(liczba):
    """Funkcja podaje sumę cyfr w liczbie."""
    liczba_string = str(liczba)
    cyfry = list(liczba_string)
    suma = 0
    for c in cyfry:
        suma += int(c)
    print(f"Suma cyfr {cyfry} to: {suma}")

def podaj_dane(funkcja, argument):
    print(f"Nazwa funkcji: {funkcja.__name__}")
    print(funkcja.__doc__)
    funkcja(argument)

podaj_dane(podaj_dlugosc_lancucha, "Lubię programować.")
print("-------------")
podaj_dane(podaj_sume_cyfr, 12345)
Nazwa funkcji: podaj_dlugosc_lancucha
Funkcja podaje liczbę znaków w łańcuchu.
Długość łańcucha "Lubię programować." to 18 znaków.
-------------
Nazwa funkcji: podaj_sume_cyfr
Funkcja podaje sumę cyfr w liczbie.
Suma cyfr ['1', '2', '3', '4', '5'] to: 15

Można oczywiście przekazywać także wiele argumentów, zwłaszcza jeżeli nie wiemy z góry, ile ich jest, używając *args i **kwargs.

Zadanie

Napisz program, który będzie składał się z dwu plików:

  1. Moduł sekwencje.py, który będzie zawierał następujące funkcje:

    • Zwracającą komplementarną sekwencję RNA dla danej sekwencji DNA
    • Zwracającą w formie słownika udział procentowy każdego rodzaju zasady w sekwencji DNA.
    • Zwracającą liczbę kodonów STOP (UAG, UAA lub UGA) w komplementarnej sekwencji RNA
    • Drukującą (ta funkcja nic nie zwraca) kodony w komplementarnej sekwencji RNA, tak aby między nimi był drukowany znak -, przy czym jeśli ostatnie zasady nie tworzą kompletnego kodonu to - nie jest drukowana.
  2. Plik główny programu (opisz_sekwencje.py) w którym znajduje się kod, który jest odpowiedzialny za pobranie od użytkownika sekwencji oraz funkcja do której jest przekazywana sekwencja i która wywołuje wszystkie funkcje z modułu sekwencje.py oraz odpowiednio drukuje uzyskane informacje na temat sekwencji. Udziały procentowe zasad powinny być drukowane z dokładnością do dwóch miejsc dziesiętnych.

Przykładowa sesja użytkownika:

Podaj sekwencję DNA: AGGATTAAAGGGCCCCATTATAAAGGGTTTCACA

.......................................................

DNA:                AGGATTAAAGGGCCCCATTATAAAGGGTTTCACA
Komplementarny RNA: UCCUAAUUUCCCGGGGUAAUAUUUCCCAAAGUGU
Udziały zasad:
        A: 35.29%
        T: 23.53%
        G: 23.53%
        C: 17.65%
Liczba kodonów STOP: 1
Kodony:
UCC-UAA-UUU-CCC-GGG-GUA-AUA-UUU-CCC-AAA-GUG-U

Przykładowe rozwiązanie

Plik sekwencje.py

# Moduł z funcjami do opisu sekwencji DNA
def komplementarny_rna(dna):
    """Zwraca komplementarą sekwencję RNA dla DNA"""
    komplementarne = {
        'A': 'U',
        'T': 'A',
        'G': 'C',
        'C': 'G'
    }
    rna = []
    for zasada in dna:
        rna.append(komplementarne[zasada])
    return ''.join(rna)

def udzial_zasad(dna):
    """Zwraca udziały % zasad w sekwencji w formie słownika"""
    dlugosc = len(dna)

    udzialy = {
        'A': dna.count('A')/dlugosc*100,
        'T': dna.count('T')/dlugosc*100,
        'G': dna.count('G')/dlugosc*100,
        'C': dna.count('C')/dlugosc*100,
    }
    return udzialy

def liczba_stop(dna):
    """Oblicza liczbe kodonów STOP w komplementarnym RNA"""
    rna = komplementarny_rna(dna)
    pierwsze = range(0, len(rna), 3)
    l_stop = 0
    kodony_stop = ('UAG', 'UAA', 'UGA')
    for zasada in pierwsze:
        kodon = rna[zasada:zasada+3]
        if kodon in kodony_stop:
            l_stop += 1
    return l_stop

def drukuj_kodony(dna):
    """Drukuje kodony w komplementarnym RNA"""
    rna = komplementarny_rna(dna)
    pierwsze = range(0, len(rna), 3)
    wydruk = ''
    for zasada in pierwsze:
        # Kolejne 3 zasady (lub mniej, jeśli kończy się sekwencja)
        kolejne = rna[zasada:zasada+3]
        wydruk +=  kolejne
        if len(kolejne) == 3:
            wydruk += '-'
    print(wydruk)

Plik opisz_sekwencje.py

"""Program opisuje podaną sekwencję DNA"""
import sekwencje as sek

def opisz(dna):
    kompl_rna = sek.komplementarny_rna(dna)
    print('\n.......................................................\n')
    print(f'DNA:                {dna}')
    print(f'Komplementarny RNA: {kompl_rna}')
    print('Udziały zasad:')
    for k, w in sek.udzial_zasad(dna).items():
        print(f'\t{k}: {w:5.2f}%')

    print(f'Liczba kodonów STOP: {sek.liczba_stop(dna)}')
    print('Kodony:')
    sek.drukuj_kodony(dna)

dna = input('\nPodaj sekwencję DNA: ').upper()
opisz(dna)
Last updated on 12 Dec 2022
Published on 12 Dec 2022