Kontynuujemy naukę Pythona i zagadnienia związanego z dekoratorami, które już poprzednio poznaliśmy, ale myślę, że warto je sobie przećwiczyć.

Dekoratory to funkcje, które przyjmują inną funkcję jako argument. Tworzą pewną funkcję wewnętrzną, tzw. wrapper, który może modyfikować zarówno argumenty przekazywane do funkcji dekorowanej, jak i jej wynik.

Przećwiczymy to sobie.

Przywitaj listę imion

Na początek napiszemy sobie funkcję, która przyjmuje listę imion i „wita się” z każdym podanym imieniem:

def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["John", "Bob", "Jim"])
# Hello John
# Hello Bob
# Hello Jim

Ta funkcja niczego nie zwraca, natomiast przyjmuje jeden argument lst, którym jest lista imion. Możemy ten argument udekorować. Na początek zobaczmy, co z tą funkcją może być nie tak:

def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["john", " Bob", "Jim", "Bob"])
# Hello john
# Hello  Bob
# Hello Jim
# Hello Bob

Po pierwsze, argumenty funkcji mogą być pisane małą literą. Nie chcemy tego. Po drugie, mogą zawierać niepotrzebne spacje. Też tego nie chcemy. Po trzecie, mogą zawierać duplikaty. To już nasze upodobanie, czy dla dwóch Bobów jedno „Hello” wystarczy, ale dla przećwiczenia i tego bym się pozbył.

Zacznijmy od napisania dekoratora, który sprawi, że każde imię będzie zapisane wielką literą:

def title_args(fn):
    def wrapper(lst):
        lst = [name.title() for name in lst]
        return fn(lst)
    return wrapper

@title_args
def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["john", " Bob", "Jim", "Bob"])
# Hello John
# Hello  Bob
# Hello Jim
# Hello Bob

Funkcja dekorująca title_args przyjmuje fn, funkcję dekorowaną. Funkcja wrapper przyjmuje jeden argument lst. Ten argument jest modyfikowany przed wywołaniem funkcji dekorowanej. Każde imię zostaje zapisane wielką literą i dopiero wtedy wywołujemy funkcję dekorowaną.

Dzięki temu wszystkie imiona zapisane mamy wielką literą. I nie musimy tego robić w funkcji greet_people. Argument lst, który do greet_people trafia jest wcześniej zmodyfikowany przez wrapper z funkcji dekorującej title_args.

Może i skomplikowane, ale jedyny sposób, aby się z tym oswoić, to napisać kolejne dekoratory. Nie chcemy zbędnych spacji przed wywołaniem naszej funkcji. Do tego użyjemy lstrip, które spacje po lewej stronie usuwa, na każdym argumencie:

def title_args(fn):
    def wrapper(lst):
        lst = [name.title() for name in lst]
        return fn(lst)
    return wrapper

def lstrip_args(fn):
    def wrapper(lst):
        lst = [name.lstrip() for name in lst]
        return fn(lst)
    return wrapper

@lstrip_args
@title_args
def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["john", " Bob", "Jim", "Bob"])
# Hello John
# Hello Bob
# Hello Jim
# Hello Bob

Tutaj funkcja lstrip_args przed wywołaniem naszej funkcji greet_people upewnia się, że każdy argument z naszej listy został pozbawiony niepotrzebnych spacji po lewej stronie, zanim przekaże te argumenty do greet_poeple.

Działa dobrze. Teraz postarajmy się jeszcze usunąć duplikaty. Funkcja dekoratora usuwającego duplikaty wygląda tak:

def enforce_unique_args(fn):
    def wrapper(lst):
        lst = list(set(lst))
        return fn(lst)
    return wrapper

Bierzemy listę argumentów i przed zawołaniem funkcji zmieniamy ją na set, zbiór (co usuwa duplikaty) następnie znowu na listę. Wołamy funkcję już bez duplikatów. Pytanie natomiast zachodzi, gdzie powinniśmy dodać nasz dekorator, w jakiej kolejności:

def enforce_unique_args(fn):
    def wrapper(lst):
        lst = list(set(lst))
        return fn(lst)
    return wrapper

def title_args(fn):
    def wrapper(lst):
        lst = [name.title() for name in lst]
        return fn(lst)
    return wrapper

def lstrip_args(fn):
    def wrapper(lst):
        lst = [name.lstrip() for name in lst]
        return fn(lst)
    return wrapper

@enforce_unique_args
@lstrip_args
@title_args
def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["john", " Bob", "Jim", "Bob"])
# Hello Bob
# Hello Bob
# Hello John
# Hello Jim

Wygląda, jakby nasz dekorator nie działał. Dzieje się tak, ponieważ dodaliśmy go PRZED lstrip_args. I rzeczywiście, ” Bob” i „Bob” to są różne elementy, zatem nasza funkcja odpowiadająca za wymuszenie unikalności argumentów przepuszcza i jednego i drugiego.

Nasz dekorator musimy dodać po lstrip_args, gdy będziemy mieli „Bob” i „Bob”. Wtedy duplikat zostanie usunięty:

def enforce_unique_args(fn):
    def wrapper(lst):
        lst = list(set(lst))
        return fn(lst)
    return wrapper

def title_args(fn):
    def wrapper(lst):
        lst = [name.title() for name in lst]
        return fn(lst)
    return wrapper

def lstrip_args(fn):
    def wrapper(lst):
        lst = [name.lstrip() for name in lst]
        return fn(lst)
    return wrapper


@lstrip_args
@enforce_unique_args
@title_args
def greet_people(lst):
    for name in lst:
        print(f"Hello {name}")

greet_people(["john", " Bob", "Jim", "Bob"])
# Hello Bob
# Hello Jim
# Hello John

Średnia ocen

Wykonajmy funkcję, która ma wyliczyć średnią ocen. Najpierw napiszmy sobie jej prostą implementację:

def avg_grade(grades):
    return sum(grades) / len(grades)

print(avg_grade([1,2,3,4,5,6]))
#3.5

Teraz chcemy dodać dekorator, który nieco nam tę funkcję ulepszy. Po pierwsze chcemy, aby w avg_grade mogły lądować tylko liczby z zakresu od 1 do 6, bo takie mogą być oceny.

def parse_grades(fn):
    def wrapper(grades):
        allowed_grades = [grade for grade in grades if grade > 0 and grade <= 6]
        return fn(allowed_grades)
    return wrapper
@parse_grades
def avg_grade(grades):
    return sum(grades) / len(grades)

print(avg_grade([1,2,3,4,5,6]))
#3.5
print(avg_grade([0, 1,2,3,4,5,6, 7]))
#3.5

Pozwalamy tylko na te oceny, które są większe od 0 i mniejsze od 7. Jak widać 0 i 7 nie przechodzi do funkcji avg_grades. To nie są prawidłowe oceny.

Teraz chcemy jeszcze pozwolić na oceny w formie tekstowej, czyli A,B,C,D,E,F oraz odpowiadające im oceny od 6 do 1. Tak to sobie zaprogramujmy:

def parse_grades(fn):
    def wrapper(grades):
        letter_grades_map = {
            "A" : 6,
            "B" : 5,
            "C" : 4,
            "D" : 3,
            "E" : 2,
            "F" : 1
        }
        letter_grades = [letter_grades_map[grade] for grade in grades if type(grade) == str and grade in "ABCDEF"]
        numeric_grades = [grade for grade in grades if type(grade) == int or type(grade) == float]
        numeric_grades = [grade for grade in numeric_grades if grade > 0 and grade < 7]
        filtered_grades = letter_grades + numeric_grades
        return fn(filtered_grades)
    return wrapper


@parse_grades
def avg_grade(grades):
    return sum(grades) / len(grades)

print(avg_grade([1,2,3,4,5,6]))
#3.5
print(avg_grade([0, "F",2,3.0,4,5,"A", 7]))
#3.5

Na początek tworzymy słownik tłumaczący litery od A do F na ich wartość numeryczną.

Potem zabieramy się za stworzenie listy tych „literowych” ocen. Dla każdej oceny, która jest typu str i mieści się w zakresie „ABCDEF” przepuszczamy ją przez słownik letter_grades_map i dostajemy jej wartość liczbową.

Dzięki temu w zmiennej letter_grades mamy wszystkie oceny „literowe”, już zamienione na ich liczbową wartość.

W zmiennej numeric_grades tworzymy listę tych ocen, które są liczbowe, czyli są albo typu int albo float (2 to int, 3.0 to float, dzięki temu moglibyśmy też 3.5 dać na przykład).

Jak już „odessaliśmy” do numeric grades wszystkie te, które są albo int albo float, to najwyższa pora jeszcze raz przefiltrować, tym razem odsysając te oceny, które są w zakresie innym niż od 1 do 6.

Koniec końców, w letter_grades mamy te oceny, które były zapisane literą, przetłumaczone na cyfrę. W numeric_grades te oceny, które były liczbami typu int lub float w zakresie od 1 do 6.

Tworzymy zmienną filtered_grades, gdzie dodajemy do siebie te dwie zmienne, tworząc listę, którą przekazujemy do funkcji.

Działa.

Myślę, że na tym ćwiczeniu warto zakończyć. Zrobiliśmy dwa porządne zadania z dekoratorami, teraz wystarczy je sobie tylko powoli, na spokojnie przeanalizować.