Python

Wprowadzenie do funkcji, klas i wyjątków.

24 listopada 2015 21:00
Paweł Kucmus

Python jako język - część 2

Ten wpis jest kontynuacją pierwszej części wprowadzenia do Pythona.

Funkcje

Funkcja jest to zdefiniowany i nazwany blok kodu. Idealnie jedna funkcja powinna wykonywać jedno zadanie.

>>> def repeat(text, exclaim=False):
...     """
...     Returns the given string 3 times.
...     If exclaim is true, adds exclamation marks.
...     """
...     result = text * 3
...     if exclaim:
...         result = result + '!!!'
...     return result

Powyższa funkcja składa się z:

  • deklaracji samej funkcji jak i przyjmowanych przez nią parametrów
  • doc stringa opisującego co dana funkcja robi
  • ciała funkcji - kodu przez nią wykonywanego
  • instrukcji return zwracającej wartość wyprodukowaną wyżej

Argumenty funkcji

W argumentach przekazujemy dane do funkcji. Argumenty mogą zawierać wartości domyślne.

>>> def moja_funkcja(pierwszy, drugi, niewymagany='domyslna'):
...     print pierwszy
...     print drugi
...     print niewymagany

wywoływanie funkcji odbyć się może na wiele sposobów:

>>> moja_funkcja('wartość1', 'wartość2')
wartość1
wartość2
domyslna

>>> moja_funkcja('wartość1', 'wartość2', 'wartość3')
wartość1
wartość2
wartość3

>>> moja_funkcja(drugi='wartość2', niewymagany='wartość3', pierwszy='wartość1')
wartość1
wartość2
wartość3

Możemy przygotować naszą funkcję na odbieranie wszelkich argumentów - nie tylko tych przewidzianych z góry:

def moja_funkcja2(*args, **kwargs):
    print args
    print kwargs

W tym momencie wszystkie nazwane argumenty przyjdą w słowniku znajdującym się pod zmienną kwargs, a pozycyjne argumenty znajdują się w liście args.

>>> moja_funkcja2(osma='wartosc8')
[]
{'osma': 'wartosc8'}

>>> moja_funkcja2(1, 2, osma='wartosc8')
[1, 2]
{'osma': 'wartosc8'}

Generatory

Generator to funkcja, która zwraca iterator tworzący swoje elementy w leniwy sposób, tzn dopiero wtedy gdy zostanie o konkretny element zapytany. Dla porównania mamy dwie (poza tym przykładem bezużyteczne) funkcje zwracające litery podanego słowa.

def giveletters(word):
    letters = []
    for letter in word:
        letters.append(letter)
    return letters

def generateletters(word):
    for letter in word:
        yield letter

Widoczną różnicą jest to jak obie funkcje zwracają wartości. giveletters posiada return, który ją natychmiast kończy zaś generateletters posługuje się yield która zwraca wartość ale i zapamiętuje stan generatora którym jest generateletters. Dlatego za każdym razem gdy użyjemy naszego generatora pętla for będzie w kolejnym kroku iteracji.

>>> print giveletters('Ewa')
['E', 'w', 'a']

>>> letters = generateletters('Ewa')
>>> print letters.next()
'E'
>>> print letters.next()
'w'
>>> print letters.next()
'a'
>>> print letters.next()
StopIteration

Instrukcje posługujące się generatorami wiedzą, że dana instancja generatora nie zwróci już nic więcej po wyjątku StopIteration.

Klasy

Temat klas jest zbyt obszerny żeby nie poświęcić mu miesięcy nauki. Postaram się jednak zobrazować podstawy.
Klasa definiowana słowem class jest czymś co opisuje nam pewną "rzecz". Ową rzeczą może być coś z "prawdziwego świata" np. pies, samochód, zbiór uczestników tego kursu czy sam uczestnik, piosenka, komputer. Obiekt to z kolei instancja danej klasy. W momencie wywołania (inicjalizacji klasy) otrzymujemy jej instancję. Dla przykładu: wystarczy nam jeden opis - klasa - do zdefiniowania samochodu, ale samych samochodów możemy mieć mnóstwo:

>>> class Car(object):
...     pass
...
>>> opel_dawida = Car()
>>> skoda_taty = Car()

>>> print opel_dawida

Oba obiekty są samochodami ale każdy z nich może mieć inne atrybuty (cechy):

>>> opel_dawida.brand = 'Opel'
>>> opel_dawida.engine_size = '1.6'
>>> skoda_taty.brand = 'Skoda'

Obiekty mogą też posiadać metody, spróbujmy z bardziej skomplikowanym przykładem (tu zacznijcie używać edytora, zapiszcie plik pod nazwą cars.py):

class Engine(object):

    is_running = False

    def __init__(self, size, category):
        self.size = size
        self.category = category

    def start(self):
        self.is_running = True

class Car(object):

    def __init__(self, brand, engine_size, engine_category):
        self.engine = Engine(engine_size, engine_category)
        self.brand = brand

    def is_running(self):
        return self.engine.is_running

    def start(self):
        self.engine.start()

Wyjaśniając powyższe krok po kroku:

  • deklarujemy klasę Engine reprezentującą silnik samochodowy.
  • klasa ta ma domyślny atrybut is_running określający czy silnik jest na chodzie
  • __init__ to metoda nazywana konstruktorem, wywoływana jest w momencie wywoływania klasy (Engine())
  • konstruktor przyjmuje parametry świadczące o wielkości i rodzaju silnika, zapisuje je jako atrybuty
  • metoda start ustawia nasz atrybut is_running tak, że wiemy, że silnik jest na chodzie
  • deklarujemy klasę Car reprezentującą samochód
  • konstruktor przyjmujący trzy parametry, tworzący instancję silnika owego samochodu (na podstawie ostatnich dwóch parametrów) i zapisuje markę samochodu.
  • metoda is_running sprawdza czy samochód jest na chodzie sprawdzając czy silnik jest włączony

Sprawdźmy teraz jak działa nasz kod (uruchomcie interpreter z miejsca, w którym znajduje się plik cars.py):

>>> from cars import Engine, Car
>>> opel_dawida = Car('Opel', '1.6', 'petrol')
>>> skoda_taty = Car('Skoda', '2.0', 'petrol')
>>> print opel_dawida.engine.size
'1.6'
>>> print opel_dawida.is_running()
False
>>> print skoda_taty.is_running()
False
>>> opel_dawida.start()
>>> print opel_dawida.is_running()
True

Mamy dwa różne obiekty będące samochodami, każde z nich posiada własny silnik. Oba silniki jak i samochody mają niezależny od siebie stan.

Wyjątki

Do tej pory na pewno nie raz napotkaliście wyjątek. Najprawdopodobniej były to SyntaxError mówiący o błędnym zapisie kodu lub IndentationError wskazujący na błąd wcięcia. Nawet jeśli nasz kod jest składniowo poprawny może powodować błędy - wyjątki. Każdy wyjątek jest śmiertelny dla naszego kodu, można jednak obsługiwać wyjątki w wyznaczony przez siebie sposób co pokarzę później. Przykładowe wystąpienie wyjątku w konsoli:

>>> if True print 'mama'
  File "<stdin>", line 1
    if True print 'mama'
                ^
SyntaxError: invalid syntax

Błędny kod zostanie wyświetlony w tak zwanym traceback, a strzałka ^ wskaże z pewną dokładnością miejsce wystąpienia błędu.

Mamy dostępnych wiele wbudowanych wyjątków, np:

>>> 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

>>> some_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'some_var' is not defined

>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

Więcej znadziecie tutaj: https://docs.python.org/2/library/exceptions.html#bltin-exceptions

Obsługa wyjątków

Python daje nam możliwość obsługi wyjątków. Dzieje się to za pomocą instrukcji try i except. Najprostszym przykładem będzie złapanie wszystkich błędów (nie róbcie tego w przyszłości to generalnie zły pomysł).

>>> def divide(num, divider):
...      print 'starting...'
...      result = num / divider
...      print 'finishing...'
...      return result
... 
>>> print divide(1, 1)
starting...
1
finishing...
>>> print divide(1, 0)
starting...
ZeroDivisionError: integer division or modulo by zero

W powyższym przypadku przy wystąpieniu błędu nie wykona się cały kod. Do was należy decyzja co czy kod następujący po wyjątku jest istotny czy nie.

>>> def divide(num, divider):
...      print 'starting...'
...      try:
...           result = num / divider
...      except:
...           result = 'ups... something went wrong'
...      print 'finishing...'
...      return result
... 
>>> print divide(1, 1)
starting...
1
finishing...
>>> print divide(1, 0)
starting...
'ups... something went wrong'
finishing...

Zwiększając kontrolę (i przewidując co może się stać), obsługujemy kontretne błędy:

>>> def divide(num, divider):
...      print 'starting...'
...      try:
...           result = num / divider
...      except ZeroDivisionError:
...           result = 'you can\'t divide by zero...'
...      except TypeError:
...           result = 'make sure you pass integers'
...      print 'finishing...'
...      return result
...

>>> print divide(1, 0)
starting...
you can't divide by zero...
finishing...

>>> print divide(1, 'a')
starting...
make sure you pass integers
finishing...

Własne wyjątki

Często chcemy wiedzieć więcej o błędach, które się zdarzają, a wbudowane wyjątki nie bardzo pasują do tego co się dzieje. Na szczęście możemy budować własne. Wyjątki powinny dziedziczyć z Exception (wracając do cars.py:

class NoFuelException(Exception):
    pass

class FuelOverflow(Exception):
    pass


class Engine(object):

    is_running = False

    def __init__(self, size, category):
        self.size = size
        self.category = category

    def start(self, fuel=0):
        if not fuel:
            raise NoFuelException('an engine need\'s fuel to start')
        self.is_running = True


class Car(object):

    tank_cap = 50
    fuel = 0

    def __init__(self, brand, engine_size, engine_category):
        self.engine = Engine(engine_size, engine_category)
        self.brand = brand

    def is_running(self):
        return self.engine.is_running

    def refuel(self, fuel):
        total_fuel = self.fuel + fuel
        if total_fuel > self.tank_cap:
            raise FuelOverflow('too much fuel')
        self.fuel = total_fuel

    def start(self):
        try:
            self.engine.start(self.fuel)
        except NoFuelException:
            print 'can\'t start the engine: have no fuel'

Doczytaj o finally i else przy obsłudze wyjątków.

Comments