Wprowadzenie do klas w pythonie

30 listopada 2016 12:00
Patryk Szymański
python development

Klasy

Czym są klasy?

Przy opisywaniu klas musimy skupić się na dwóch głównych pojęciach: klasy i instancje klas zwane obiektami. W skrócie klasa jest "opisem" jak ma wyglądać struktura danych obiektów oraz zbiorem metod służących do manipulowania. a instancja klasy czyli obiekt jest zbiorem zależnych od siebie danych. Na przykład klasę można określić jako formę na ciasto. Nie jest ciastem sama w sobie, zawiera tylko ogólne informacje o kształcie ciasta.

Przykładowa klasa:

class Cake(object):
    size = '20/20'

    def __init__(self, flavor, layers, icing):
        self.flavor = flavor
        self.layers = layers
        self.icing = icing

Jak widzimy każde stworzone ciasto (obiekt) będzie miało takie same wymiary 20/20 jako ale już smak, ilość warstw jak i polewa bedą się różnić zależnie od obiektu.

Metoda _init_ jest specjalną metodą, tzw konstruktorem, uruchamianą zawsze gdy tworzona jest nowa instancja klasy czyli w naszym przypadku nowe ciasto z podanym smakiem, ilością warstw i polewą. Pierwszym argumentem tej funkcji jest self. Jest to specjalny argument przekazujący informację nad którą instancją klasy aktualnie pracujemy.

Przykładowy obiekt:

banana_cake = Cake(flavor='banan', layers=2, icing='chocolate')
banana_cake.flavor
>>> 'banan'
banana_cake.layers
>>> 2
banana_cake.icing
>>> 'chocolate'
banana_cake.size
>>> '20/20'

Jak widać wykorzystując klasę Cake stworzyliśmy nowy obiekt czyli bananowe ciasto które ma swój właśny smak, polewę i dwie warstwy. W ten sam sposób możemy stworzyć wiele innych (obiektów) ciast wykorzystując formę (klasę) Cake.

Zaczynamy skrypt!

W tym zadaniu będziemy potrzebować dwóch klas NotesManager i Note. Zacznijmy jednak od wymaganych importów:

# coding: utf-8
from __future__ import unicode_literals

import json
DATABASE = 'notes_data/notes.json'
BOARDS = ['to do', 'in progress', 'done']

Następnie stwórzmy klasę NotesManager:

class NotesManager(object):

    def __init__(self):
        self.all = []

    def add(self, idx, board, message):
        self.all.append(Note(idx=idx, board=board, message=message))

Oraz klasę Note:

class Note(object):
    objects = NotesManager()

    def __init__(self, idx, board, message):
        self.id = int(idx)
        self.board = board
        self.message = message

Jak widać obiekt klasy NotesManager został przypisany do atrybutu objects klasy Note. Takie rozwiązanie zostało użyte dla maksymalnego przybliżenia aktualnej struktury skryptu do struktury użytej w freameworku Django który poznacie na następnych lekcjach. W add oraz __init__ została użyta zmienna idx zamiast id by zapobiec nadpisywaniu wbudowanej funkcji pythonowej id() W tym momencie mamy podstawową działającą wersję naszego skryptu.

Test działania klas

By sprawdzić poprawność działania naszego kodu spróbujmy dodać notatkę. By to zrobić na końcu pliku dodajemy:

Note.objects.add(idx=1, board='to do', message='test')

Teraz dodając print Note.objects.all możemy zobaczyć naszą pierwszą instancję klasy czyli obiek: [<__main__.Note object at 0x7f0b31b0b9d0>].

Jak widać sam obiekt nie wiele nam mówi. Dane danej instancji klasy możemy podejrzeć w bardzo prosty sposób. Najpierw wyciągamy instancję klasy z listy: data = Note.objects.all[0]. Następnie możemy podejrzeć wprowadzone przez nas dane wpisując:

print data.id
print data.board
print data.message

Ładowanie danych z pliku

Nie chcemy za każdym razem sami ręcznie dodawać wszyskich notatek. By tego uniknać musimy skorzystać z dobrze nam znanej bazy danych z pliku notes.json

Wykorzystując wiedzę z ostatnich lekcji napiszmy funkcję load_initial_data() której zadaniem jest otworzenie i odczytanie danych z pliku json oraz dodanie notatek używając notes.objects.add()

ROZWIĄZANIE

def load_initial_data():
    with open(DATABASE, 'r') as database_file:
        json_data = json.load(database_file, encoding='utf-8')

    for item in json_data:
        Note.objects.add(
            idx=item['id'],
            board=item['board'],
            message=item['message'],
        )

Test poprawności ładowania danych z pliku

By sprawdzić czy load_initial_data działa poprawnie na końcu pliku dopiszmy:

load_initial_data()
print Note.objects.all

i odpalmy nasz skrypt. W terminalu powinna pojawić się lista obiektów:

[
    <__main__.Note object at 0x7f3c8a7cf9d0>,
    <__main__.Note object at 0x7f3c8a7cfa10>,
    <__main__.Note object at 0x7f3c8a7cfa50>,
    <__main__.Note object at 0x7f3c8a7cfa90>,
    <__main__.Note object at 0x7f3c8a7cfad0>,
    <__main__.Note object at 0x7f3c8a7cfb10>,
    <__main__.Note object at 0x7f3c8a7cfb50>,
    <__main__.Note object at 0x7f3c8a7cfb90>,
    <__main__.Note object at 0x7f3c8a7cfbd0>,
]

Jeśli wszystko się zgadza gratulacje! Możemy przejść do rozwijania możliwości naszych klas Note oraz NotesManager.

Pobieranie wybranej notatki

By móc pobrać jedną wybraną instancję notatki musimy dodać metodę get do NotesManager.

Napiszmy metodę get przyjmującą argumenty self oraz idx. Metoda ta ma wyszukać notatkę o id podanym w argumencie idx i zwrócić notatkę. Jeśli notatka o danym id nie istnieje używając raise zwracamy wyjątek IndexError() z informacją, że notatka o danym id nie istnieje.

ROZWIĄZANIE

def get(self, idx):
    for note in self.all:
        if note.id == idx:
            return note
    raise IndexError('Note with ID {} don\'t exist'.format(idx))

__str__ czyli obiekt zrozumiały dla człowieka

Poprzednie zadania pokazały, że print pojedynczego obiektu nie zwraca nam żadnych przydatnych informacji. Na szczęście możemy to łatwo zmienić wykorzystując metodę __str__. By to zrobić dodajmy poniższy kod na końcu klasy Note:

def __str__(self):
    return 'ID: {}, Board: {}, Message: {}'.format(
        self.id,
        self.board,
        self.message
    )

By przetestować działanie tej metody należy dodać linijkę print note.objects.all[0] na samym końcu pliku:

load_initial_data()
print Note.objects.all
print Note.objects.all[0]

Jak widać po uruchomieniu skryptu lista print Note.objects.all pokazuje dane tak samo jak wcześniej (do zmiany tego służy analogiczna metoda __repr__) za to print Note.objects.all[0] pokazuje nam ID: 1, Board: in progress, Message: Nauczyc sie Pythona zgodnie z założeniami metody __str__.

Tworzenie nowej notatki

By stworzyć nową notatkę wykorzystamy funkcję __generate_id z poprzedniej lekcji. By to zrobić dodajmy poniższy kod do NotesManager:

def __generate_id(self):
    return max(note.id for note in self.all) + 1

Następnie również w NotesManager musimy stworzyć metodę create przyjmującą argumenty board oraz message która stworzy nowy obiekt Note z wygenerowanym id, uruchomi metodę save() z klasy Note (o której za chwilę) i doda nowo utworzoną notatkę do self.all

ROZWIĄZANIE

def create(self, board, message):
    note = Note(
        idx=self.__generate_id(),
        board=board,
        message=message
    )
    note.save()
    self.all.append(note)

Ostatnim krokiem jest utworzenie nowej metody save w klasie Note. Metoda ta najpierw musi sprawdzić czy został podany message. Jeśli nie istnieje zwraca ValueError() z odpowiednią informacją. Następnie sprawdza czy podany board istnieje. Jeśli nie zwraca ValueError() z odpowiednią informacją. Następnie otwiera plik bazy danych i pobiera wszystkie istniejące notatki. Dzięki temu, że mamy wszystkie istniejące notatki możemy sprawdzić id notatki. Sprawdzamy czy podane id jest typu liczbą całkowitą (int) oraz czy nie istnieje notatka o takim samym id. Jeśli wystąpi jeden z przypadków ValueError() z odpowiednią informacją.

Gdy już sprawdziliśmy poprawność danych czas zapisać nową notatkę. By to zrobić dodajemy dane nowej notatki do listy istniejących notatek. Otwieramy plik bazy danych w trybie zapisu i zapisujemy zaktualizowaną listę notatek do pliku.

ROZWIĄZANIE

def save(self):
    with open(DATABASE, 'r') as database_file:
        data = json.load(database_file, encoding='utf-8')
    data.append({
        'id': self.id,
        'message': self.message,
        'board': self.board
    })
    with open(DATABASE, 'w') as database_file:
        json.dump(data, database_file, indent=4)

Zadanie domowe

Do metody save należy dodać walidację sprawdzającą czy: - użytkownik podał message, board i id - board który podał jest w liście BOARDS - podany id jest typu int - nie istnieje już notatka o podanym id.

ROZWIĄZANIE

def save(self):
    if not self.message:
        raise ValueError('Message is required')

    if self.board not in BOARDS:
        raise ValueError('Board "{}" doesn\'t exists'.format(self.board))

    if type(self.id) != int:
        raise ValueError('Note id "{}" is invalid'.format(self.id))

    with open(DATABASE, 'r') as database_file:
        data = json.load(database_file, encoding='utf-8')

    if self.id in [note['id'] for note in data]:
        raise ValueError('Note with id "{}" already exist'.format(self.id))

    data.append({
        'id': self.id,
        'message': self.message,
        'board': self.board
    })
    with open(DATABASE, 'w') as database_file:
        json.dump(data, database_file, indent=4)

Comments