Filtrowanie

6 grudnia 2016 23:00
Piotr Bajsarowicz
python development filtrowanie

Wstęp

Realizując aplikację TO DO, dążymy do spełnienia podstawowych funkcjonalności frameworka Django. Wykład drugi zakończył się na implementacji klasy Note, jej managera (NotesManager) oraz metod pozwalających na utworzenie oraz pobranie pojedynczego wystąpienia (instancji) lub wielu wystąpień klasy Note. W tym artykule dowiemy się, w jaki sposób rozszerzyć funkcjonalność aplikacji o filtrowanie wyników.

Dotychczasowy stan aplikacji

Na początku zaimportujmy klasę Note oraz metodę do inicjalizacji danych

from drugi_wyklad_json import Note, load_initial_data

"Załadujmy" dane (deserializacja)

load_initial_data()

I... zaczynamy.

Możemy wyświetlić wszystkie notatki,

>>> Note.objects.all()
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona,
ID: 2, Board: in progress, Message: Zrobic zadanie domowe,
ID: 6, Board: in progress, Message: TEST NOTE,
ID: 3, Board: done, Message: Zapisac sie na kurs Pythona,
ID: 4, Board: to do, Message: Zrobic tutorial na PyPile,
ID: 5, Board: to do, Message: Przyjsc na wyklad PyPily]

pobrać pierwszą,

>>> first_note = Note.objects.all()[0]
>>> first_note.message
u'Nauczyc sie Pythona'

czy utworzyć nową.

note = Note.objects.create(board='to do', message='test')

Sprawdźmy, czy rzeczywiście lista wszystkich została rozszerzona o nowo utworzoną

>>> Note.objects.all()
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona,
ID: 2, Board: in progress, Message: Zrobic zadanie domowe,
ID: 6, Board: in progress, Message: TEST NOTE,
ID: 3, Board: done, Message: Zapisac sie na kurs Pythona,
ID: 4, Board: to do, Message: Zrobic tutorial na PyPile,
ID: 5, Board: to do, Message: Przyjsc na wyklad PyPily,
ID: 7, Board: to do, Message: test]

oraz czy jej message jest zgodny z dodanym

>>> note.message
'test'

Możemy również sprawdzić id obiektu (wygenerowany automatycznie)

>>> note.id
7

Tę samą instancję powinniśmy otrzymać wykonując metodę get (na menadżerze klasy Note) ze zwróconym id

>>> Note.objects.get(idx=note.id)
ID: 7, Board: to do, Message: test

A co jeśli spróbujemy pobrać notatkę, która nie istnieje?

>>> Note.objects.get(100)

---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-11-f96721664100> in <module>()
----> 1 Note.objects.get(100)


/Users/pypila/drugi_wyklad_json.py in get(self, idx)
     22             if note.id == idx:
     23                 return note
---> 24         raise IndexError('Note with ID {} doesn\'t exist'.format(idx))
     25 
     26     def all(self):


IndexError: Note with ID 100 doesn't exist

Sprawdźmy czy dodanie nowej notatki zostało odzwierciedlone w pliku notes.json (serializacja)

>>> import io
>>> import json
>>> with io.open('notes_data/notes.json', 'r', encoding='utf-8') as notes:
    notes = notes.readlines()
>>> print json.dumps(notes, indent=2)
[ 
    { 
        "message": "Nauczyc sie Pythona",  
        "id": 1,  
        "board": "in progress" 
    },  
    { 
        "message": "Zrobic zadanie domowe",  
        "id": 2,  
        "board": "in progress" 
    },  
    { 
        "message": "TEST NOTE",  
        "id": 6,  
        "board": "in progress" 
    },  
    { 
        "message": "Zapisac sie na kurs Pythona",  
        "id": 3,  
        "board": "done" 
    },  
    { 
        "message": "Zrobic tutorial na PyPile",  
        "id": 4,  
        "board": "to do" 
    },  
    { 
        "message": "Przyjsc na wyklad PyPily",  
        "id": 5,  
        "board": "to do" 
    },  
    { 
        "message": "test",  
        "id": 7,  
        "board": "to do" 
    } 
]

Potrzebna wiedza - argumenty pozycyjne i nazwane (args vs kwargs)

Poniżej znajdziesz przykłady wykorzystania argumentów pozycyjnych i nazwanych.

Więcej informacji: https://docs.python.org/2.7/tutorial/controlflow.html#more-on-defining-functions

Domyśle wartości argumentów

Zaimplementujmy prostą metodę wyświetlającą text repeat razy.

def yolo(text, repeat=3):
    for _ in xrange(repeat):
        print text

Metoda yolo przyjmuje dwa argumenty - text i repeat. Drugi z nich posiada domyślną wartość, co czyni go argumentem opcjonalnym. Dzięki temu, metodę yolo możmy wykonać z jednym argumentem, i możemy zrobić to na dwa sposoby: korzystając z argumentu pozycyjnego

>>> yolo('hehe')
hehe
hehe
hehe

lub nazwanego

>>> yolo(text='hihi')
hihi
hihi
hihi

Argument text nie ma domyślnej wartości, dlatego wywołanie metody yolo, bez podania argumentów, zakończy się błędem

>>> yolo()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-4-9ab8c01b5154> in <module>()
----> 1 yolo()


TypeError: yolo() takes at least 1 argument (0 given)

Domyślna wartość repeat może zostać nadpisana podczas wywołania

>>> yolo(text='hoho', repeat=2)
hoho
hoho

Zdefinijmy inną metodę. Podobnie jak powyżej, będzie ona przyjmowała dwa argumenty - tym razem będą to new_name oraz names. Drugi z nich ma domyślą wartość, którą jest lista z nazwiskami ['Andrzej Duda', 'Beata Kempa']. Funkcjonalność realizowana przez metodę list_of_names_v1 polega na dodaniu wartości podanej jako pierwszy argument (new_name) do listy names - trywialne, prawda?

def list_of_names_v1(new_name, names=['Andrzej Duda', 'Beata Kempa']):
    names.append(new_name)

    return names

Spróbujmy wykonać metodę podając tylko wymagany argument new_name

>>> list_of_names_v1('Donald Trump')
['Andrzej Duda', 'Beata Kempa', 'Donald Trump']

Okej, oczekiwany efekt. Spróbujmy raz jeszcze

>>> list_of_names_v1('Hillary Clinton')
['Andrzej Duda', 'Beata Kempa', 'Donald Trump', 'Hillary Clinton']

Skąd pojawił się tam 'Donald Trump'? Huh? Dzieje się tak, ponieważ domyśle wartości są "obliczane"/"szacowane" tylko raz, i dzieje się to w momencie definicji funkcji. Oznacza to, że w przypadku wykonania metody list_of_names_v1 kilkukrotnie, z domyślną wartością argumentu names, nowe wartości new_name będą dopisywane do już istniejącej listy. Innymi słowy, kiedy domyślną wartością jest zmienny/niestały (mutable) obiekt (np. lista), lub instancja większości klas, wykonywanie operacji zmieniających wartość takiego argumentu będzie miała wpływ na kolejne wykonania tej metody (np. jak w naszym przypadku - poprzez akumulację przekazywanych argumentów). Co jeśli chcemy tego uniknąć? Rozwiązanie jest banalne (więcej informacji - http://docs.python-guide.org/en/latest/writing/gotchas/):

def list_of_names_v2(new_name, names=None):
    if names is None:
        names = ['Andrzej Duda', 'Beata Kempa']
    names.append(new_name)

    return names

i wywołanie

>>> list_of_names_v2('Donald Trump')
['Andrzej Duda', 'Beata Kempa', 'Donald Trump']
>>> list_of_names_v2('Hillary Clinton')
['Andrzej Duda', 'Beata Kempa', 'Hillary Clinton']

args (argumenty pozycyjne)

Python pozwala nam wywoływać metody korzystając z pozycji argumentów w definicji.

def yolo(text, repeat=3):
    for _ in xrange(repeat):
        print text
>>> yolo('args')
args
args
args

kwargs (argumenty nazwane)

Ponadto, możemy określić w sposób jawny (nazwany), jakiemu argumentowi jaką wartość chcemy przypisać

>>> yolo(text='kwargs')
kwargs
kwargs
kwargs

args vs kwargs

Prześledźmy kilka przykładów i spróbujmy znaleźć różnice i ograniczenia

>>> yolo('args & kwargs', repeat=5)
args & kwargs
args & kwargs
args & kwargs
args & kwargs
args & kwargs
>>> yolo(repeat=5, 'args & kwargs')
File "<ipython-input-16-845e7875b0ca>", line 1
 yolo(repeat=5, 'args & kwargs')
SyntaxError: non-keyword arg after keyword arg

To nic zaskakującego - skoro argumenty pozycyjne, jak sama nazwa wskazuje, bazują na pozycji argumentów w definicji metody, podawanie ich w kolejności różnej od tej zdefiniowanej, nie ma najmniejszego sensu. Stąd otrzymujemy błąd informujący nas o tym, iż argumenty nazwane nie mogą znajdować się przed argumentami pozycyjnymi.

A co jeśli wykonamy metodę bez podawania żadnego argumentu?

>>> yolo()
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-18-9ab8c01b5154> in <module>()
----> 1 yolo()


TypeError: yolo() takes at least 1 argument (0 given)

Ponownie bez zaskoczenia. A jeśli podamy argument, który nie istnieje?

>>> yolo(unknown=5)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-19-dc7520d57ea1> in <module>()
----> 1 yolo(unknown=5)


TypeError: yolo() got an unexpected keyword argument 'unknown'

Ten wynik również nie powinien nikogo dziwić. Ale... czy istnieje przypadek, w którym ostatnie dwa wywołania nie zakończą się błędem?

Samowolność argumentów (*args, **kwargs)

Czasami, definiując metodę, chcemy jej zachowanie uzależnić od ilości podanych argumentów:

  • pozycyjnych
def yolo_args(*args):
    return args
>>> yolo_args('1', 2, 'Trzy')
('1', 2, 'Trzy')
>>> yolo_args(one='1', two=2, three='Three')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-22-111d8585aebd> in <module>()
----> 1 yolo_args(one='1', two=2, three='Three')


TypeError: yolo_args() got an unexpected keyword argument 'one'
  • nazwanych
def yolo_kwargs(**kwargs):
    return kwargs
>>> yolo_kwargs('1', 2, 'Trzy')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-24-73870c840db5> in <module>()
----> 1 yolo_kwargs('1', 2, 'Trzy')


TypeError: yolo_kwargs() takes exactly 0 arguments (3 given)

>>> yolo_kwargs(one='1', two=2, three='Three')
{'one': '1', 'three': 'Three', 'two': 2}
  • pozycyjnych i nazwanych
def yolo_args_kwargs(*args, **kwargs):
    return {
        'args': args,
        'kwargs': kwargs
    }
>>> yolo_args_kwargs('1', 2, 'Trzy')
{'args': ('1', 2, 'Trzy'), 'kwargs': {}}
>>> yolo_args_kwargs(one='1', two=2, three='Three')
{'args': (), 'kwargs': {'one': '1', 'three': 'Three', 'two': 2}}
>>> yolo_args_kwargs('1', 2, 'Trzy', one='1', two=2, three='Three')
{'args': ('1', 2, 'Trzy'), 'kwargs': {'one': '1', 'three': 'Three', 'two': 2}}
>>> yolo_args_kwargs('1', one='1', 2, two=2, 'Trzy', three='Three')
File "<ipython-input-30-8013c12ecb7a>", line 1
  yolo_args_kwargs('1', one='1', 2, two=2, 'Trzy', three='Three')
SyntaxError: non-keyword arg after keyword arg

Rozpakowywanie argumentów (*args, **kwargs)

A co jeśli chcielibyśmy wywołać metodę z argumentami, które znajdują się już w liście (list) lub krotce (tuple)? Prześledźmy zachowanie na przykładzie w budowanej w język Python metody range. Powiedzmy, że chcemy uzyskać listę składającą się z cyfr od 3 do 5. Standardowo możemy zrobić wywołując metodę range z argumentem start równym 3 oraz stop równym 5

>>> range(3, 6)
[3, 4, 5]

Wyżej wspomnieliśmy o wywołaniu metody, z argumentami wcześniej zdefiniowanymi w liście/krotce. Sprawdźmy działanie

>>> args = [3, 6]
>>> range(args)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-35-cf3b519380c5> in <module>()
----> 1 range(args)


TypeError: range() integer end argument expected, got list.

Czyli jednak to nieprawda? Nic bardziej mylnego. Błąd wynika z naszego wywołania. Dzieje się tak, ponieważ implementacja metody range pozwala na wykonanie jej z pierwszym argumentem będącym liczbą całkowitą, podczas gdy w naszym przypadku, pierwszy podany argument równy jest liście [3, 6]. To co musimy zrobić to wypakować tę listę.

>>> range(*args)
[3, 4, 5]

Dzięki operatorowi * wartości z listy zostały wypakowane - odpowiednio argument start otrzymał wartość 3 a stop wartość 6.

Prześledźmy inne przykłady:

def list_of_names_v2(new_name, names=None):
    if names is None:
        names = ['Andrzej Duda', 'Beata Kempa']
    names.append(new_name)

    return names
>>> args = ['Donald Trump', ['Donald Tusk']]
>>> list_of_names_v2(*args)
['Donald Tusk', 'Donald Trump']

Wypakować możemy również słownik - w tym przypadku nazwa klucza reprezentuje argument nazwany.

kwargs = {
    'new_name': 'Donald Trump',
    'names': ['Donald Tusk']
}
>>> list_of_names_v2(**kwargs)
['Donald Tusk', 'Donald Trump']

Możemy użyć jednocześnie dwóch wypakowań

>>> list_of_names_v2(*['Donald Trump'], **{'names': ['Donald Tusk']})
['Donald Tusk', 'Donald Trump']

Powyższe przykłady wydają się nie mieć większego sensu, ponieważ obrazują wyłącznie samą funkcjonalność wypakowywania a nie jej moc, która to objawia się m.in. tym, iż dzięki wypakowywaniu, możemy wywołać dokładnie tę samą metodę, uzyskując zupełnie inny wynik - i to dzięki mocy operatora *, użytego jedno lub dwukrotnie (kłamstwo - m.in. dzięki * - wszystko zależy od implementacji samej metody; wykorzystania *args i/lub **kwargs) . Prześledźmy przykłady:

def identity(**kwargs):
    if 'first_name' in kwargs:
        print 'Imie: {}'.format(kwargs['first_name'])
    if 'last_name' in kwargs:
        print 'Nazwisko: {}'.format(kwargs['last_name'])
    if 'age' in kwargs:
        print 'Wiek: {}'.format(kwargs['age'])
>>> identity('John')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-45-7f766438f739> in <module>()
----> 1 identity('John')


TypeError: identity() takes exactly 0 arguments (1 given)

Definicja pozwala na wykonanie metody identity z dowolną liczbą argumentów nazwanych, podczas gdy my wykonaliśmy ją z jednym argumentem pozycjnym, stąd błąd. Natomiast wykonanie metody bez żadnych argumentów, w tym przypadku zakończmy się bez błędów

>>> identity()

Zmieńmy definicję metody - pozwólmy na przekazanie dowolnej liczby argumentów zarówno pozycyjnych jak i nazwanych

def identity(*args, **kwargs):
    if 'first_name' in kwargs:
        print 'Imie: {}'.format(kwargs['first_name'])
    if 'last_name' in kwargs:
        print 'Nazwisko: {}'.format(kwargs['last_name'])
    if 'age' in kwargs:
        print 'Wiek: {}'.format(kwargs['age'])
>>> identity('John')

>>> identity()

>>> identity(first_name='Andrzej')
Imie: Andrzej
>>> identity(first_name='Andrzej', last_name='Duda', age=44)
Imie: Andrzej
Nazwisko: Duda
Wiek: 44
>>> identity(age=44, last_name='Duda', first_name='Andrzej')
Imie: Andrzej
Nazwisko: Duda
Wiek: 44

Do tej pory podawanie argumentów pozycjnych co prawda nie prowadziło do błędów, ale było bezcelowe, gdyż wynik działania metody zależał wyłącznie od argumentów nazwanych. Zmieńmy to

def identity(*args, **kwargs):
    print 'Imie: {}'.format(args[0])
    print 'Nazwisko: {}'.format(args[1])
    print 'Wiek: {}'.format(args[2])
>>> identity('Andrzej', 'Duda', 44)
Imie: Andrzej
Nazwisko: Duda
Wiek: 44
>>> identity(44, 'Duda', 'Andrzej')
Imie: 44
Nazwisko: Duda
Wiek: Andrzej

Filtrowanie ⇒ ZADANIE DOMOWE

W tym miejscu wkrótce pojawi się opis implementacji filtrowania.

Zgodnie z umową, ta część jest zadaniem domowym. Na rozwiązania czekamy do piątku (09.12.2016) do godziny 23:59.

Zadanie polega na implementacji 3 metod:

  • count - zwracająca liczbę notatek,
  • get - zwracająca pojedynczą instancję (wystąpienie) notatki (klasy Note), spełniających kryteria podane w argumentach,
  • filter - zwracająca listę (preferowany Queryset) instancji (wystąpień) notatki (klasy Note), spełniających kryteria podane w argumentach.

Mockup: https://goo.gl/Dfjk0R

Poniżej znajdują się przykładowe wywołania metod.

Przegląd funkcjonalności

>>> from trzeci_wyklad_json import Note, load_initial_data
>>> notes_queryset = Note.objects.all()
>>> notes_queryset
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona, ID: 2, Board: in progress, Message: Zrobic zadanie domowe, ID: 6, Board: in progress, Message: TEST NOTE, ID: 3, Board: done, Message: Zapisac sie na kurs Pythona, ID: 4, Board: to do, Message: Zrobic tutorial na PyPile, ID: 5, Board: to do, Message: Przyjsc na wyklad PyPily]
>>> [note for note in notes_queryset]
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona,
ID: 2, Board: in progress, Message: Zrobic zadanie domowe,
ID: 6, Board: in progress, Message: TEST NOTE,
ID: 3, Board: done, Message: Zapisac sie na kurs Pythona,
ID: 4, Board: to do, Message: Zrobic tutorial na PyPile,
ID: 5, Board: to do, Message: Przyjsc na wyklad PyPily]
>>> print notes_queryset
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona, ID: 2, Board: in progress, Message: Zrobic zadanie domowe, ID: 6, Board: in progress, Message: TEST NOTE, ID: 3, Board: done, Message: Zapisac sie na kurs Pythona, ID: 4, Board: to do, Message: Zrobic tutorial na PyPile, ID: 5, Board: to do, Message: Przyjsc na wyklad PyPily]
>>> notes_queryset.count()
6
>>> Note.objects.all().get(id=1)
ID: 1, Board: in progress, Message: Nauczyc sie Pythona
>>> Note.objects.get(id=1)
ID: 1, Board: in progress, Message: Nauczyc sie Pythona
>>> Note.objects.get(id=987654321)
---------------------------------------------------------------------------

    IndexError                                Traceback (most recent call last)

    <ipython-input-10-3112708cbd97> in <module>()
    ----> 1 Note.objects.get(id=987654321)


    /Users/pypila/trzeci_wyklad_json.py in get(self, *args, **kwargs)
         28             return notes[0]
         29         else:
    ---> 30             raise IndexError('Note doesn\'t exist or returned more then one entry')
         31 
         32 


    IndexError: Note doesn't exist or returned more then one entry

>>> Note.objects.get(board='in progress')
---------------------------------------------------------------------------

    IndexError                                Traceback (most recent call last)

    <ipython-input-11-4ef10dc71a9a> in <module>()
    ----> 1 Note.objects.get(board='in progress')


    /Users/pypila/trzeci_wyklad_json.py in get(self, *args, **kwargs)
         28             return notes[0]
         29         else:
    ---> 30             raise IndexError('Note doesn\'t exist or returned more then one entry')
         31 
         32 


    IndexError: Note doesn't exist or returned more then one entry
>>> Note.objects.filter(board='in progress')
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona, ID: 2, Board: in progress, Message: Zrobic zadanie domowe, ID: 6, Board: in progress, Message: TEST NOTE]
>>> notes_in_progress = Note.objects.filter(board='in progress')
>>> notes_in_progress
[ID: 1, Board: in progress, Message: Nauczyc sie Pythona, ID: 2, Board: in progress, Message: Zrobic zadanie domowe, ID: 6, Board: in progress, Message: TEST NOTE]
>>> first_note = notes_in_progress.get(id=1)
>>> first_note
ID: 1, Board: in progress, Message: Nauczyc sie Pythona
>>> type(notes_in_progress)
trzeci_wyklad_json.NotesQueryset
>>> type(first_note)
trzeci_wyklad_json.Note
>>> notes_in_progress.get(message='Nauczyc sie Pythona')
ID: 1, Board: in progress, Message: Nauczyc sie Pythona
>>> Note.objects.create(board='to do', message='test')
    Note.objects.create(board='to do', message='test')
    Note.objects.create(board='in progress', message='test')
ID: 9, Board: in progress, Message: test
>>> test_notes = Note.objects.filter(message='test')
>>> print test_notes.count()
3
>>> test_notes
[ID: 7, Board: to do, Message: test, ID: 8, Board: to do, Message: test, ID: 9, Board: in progress, Message: test]
>>> to_do_test_notes = Note.objects.filter(message='test', board='to do')
>>> print to_do_test_notes.count()
2
>>> to_do_test_notes
[ID: 7, Board: to do, Message: test, ID: 8, Board: to do, Message: test]

WAŻNE

Cel kursu to nauka podstaw realizacji aplikacji webowych, z wykorzystaniem języka Python. Nie skupiamy się na samej składni języka - tą można poznać korzystając z jednego z popularnych kursów. Dlatego to na czym nam zależy, to dobre zrozumienie budowy poszczególnych "komponentów" frameworka webowego. Jeśli nie miałeś wcześniej styczności z aplikacjami webowymi, to naturalne jest to, że nie wszystko jest od razu jasne. Dlatego ogromna prośba - w przypadku jakichkolwiek pytań nie wahaj się pytać. Nam naprawdę zależy na przekazaniu wiedzy.

W przypadku pytań prosimy o kontakt na jeden z poniższych adresów:

  • artur.grochowski@stxnext.com
  • piotr.bajsarowicz@stxnext.com

Podkreślmy - nie ma głupich pytań, są tylko głupie odpowiedzi.

Źródła i przydatne linki

Notebooki z zajęć:

Comments