W tym odcinku bardziej skupimy się na utrwalaniu zagadnień już poznanych, niż na zdobywaniu nowych umiejętności, więc można go sobie spokojnie pominąć, jeżeli nie potrzebujemy utrwalenia.
A utrwalimy sobie rzeczy takie jak konwersje typów, rekurencja oraz dekoratory, wykonując jedno zadanie.
Do dzieła
Zadanie – policz cyfry w liczbie
Zobaczmy taki oto kod:
msg = "123"
print(len(msg))
#3
Jak widać liczba „123” zapisana jako napis, może być wrzucona do funkcji len, która bezbłędnie poda nam długość tego napisu, która jest również ilością jego cyfr.
Czy możemy to samo zrobić z typem liczbowym?
num = 123
print(len(num))
#TypeError: object of type 'int' has no len()
Nie. Typ liczbowy nie podlega ani indeksowaniu ani nie ma swojej „długości”, nie może być argumentem funkcji len.
To jak te cyfry policzyć? Wersja dla „najbardziej ambitnych”:
num = 123
digits = 0
while num:
digits +=1
num = num // 10
print(digits)
#3
Wersja dużo łatwiejsza:
num = 123
digits = len(str(num))
print(digits)
#3
Zamieniamy liczbę na tym znakowy i podajemy jej długość. Wersja w funkcji:
def count_digits(num):
return len(str(num))
print(count_digits(123))
print(count_digits(1111))
#
# 3
# 4
Podajemy ilość cyfr. Funkcja źle zadziała, jeżeli przekażemy jej typ float:
def count_digits(num):
return len(str(num))
print(count_digits(1.5))
print(count_digits(123))
print(count_digits(1111))
# 3
# 3
# 4
Cyfry są dwie, natomiast funkcja ewidentnie pokazuje 3. Bo taka jest ilość znaków, z kropką licząc.
Policz cyfry – float
Stwórzmy funkcję, która przyjmie liczbę typu float i poda nam ilość cyfr przed przecinkiem i po przecinku jako wartość zwróconą.
Jak liczymy cyfry?
num = 123
print(len(str(num)))
# 3
A w jaki sposób możemy zamienić typ float na napis i „rozbić” go na wartość przed przecinkiem i po przecinku?
num = 1.55
num_str = str(num)
print(num_str)
# 1.55
before, after = num_str.split(".")
print(before, after)
print(len(before), len(after))
# 1 55
# 1 2
Napiszmy sobie dwie funkcje, jedną do typu int, drugą do float:
def num_digits_int(num):
return len(str(num))
def num_digits_float(num):
num_str = str(num)
before, after = num_str.split(".")
return len(before), len(after)
print(num_digits_int(123))
print(num_digits_float(12.3))
# 3
# (2, 1)
Działa. Wersja poniżej również zadziała:
def num_digits_int(num):
return len(str(num))
def num_digits_float(num):
num_str = str(num)
before, after = num_str.split(".")
return num_digits_int(before), num_digits_int(after)
print(num_digits_int(123))
print(num_digits_float(12.3))
# 3
# (2, 1)
Wprowadzenie prostej rekurencji
Rekurencja to taka funkcja, która wywołuje samą siebie. Wprowadzimy bardzo prostą, ale też ciekawą rekurencję do naszej funkcji. Postaramy się bowiem te dwie funkcje zbić w jedną, która jeżeli otrzyma typ float – poda ilość liczb przed i po przecinku, jeżeli typ int albo str – ilość cyfr.
W zasadzie typ str też może zawierać liczbę float np. „1.23” więc będziemy każdą liczbę zamieniać na str i testować na obecność kropki.
To brzmi groźniej niż w rzeczywistości wygląda. Oto funkcja
def num_digits(num):
if "." in str(num):
before, after = str(num).split(".")
return num_digits(before), num_digits(after)
return len(str(num))
print(num_digits(123))
print(num_digits(1.55))
print(num_digits("12.3"))
print(num_digits("123"))
# 3
# (1, 2)
# (2, 1)
# 3
Nasza funkcja przyjmuje coś (float, int, str), co ma być jakąś liczbą. Sprawdza czy w zamienionym na str num nie ma kropki.
Zakładamy, że nie ma, daliśmy „123” albo 123. Idziemy dalej, zwracamy długość zamienionego na napis num, czyli liczbę cyfr.
Jeżeli natomiast podaliśmy 1.5 albo „1.5”, czyli ta kropka tam jest (dla pewności musimy zamienić num na typ str, bo o ile w „1.5” sprawdzić obecność kropki możemy, o tyle w 1.5 już nie)…
Jeżeli podaliśmy coś z kropką, czyli typ float, to rozbijamy go na wartość przed i po kropce (konwertując na string, bo o ile „1.5” tak rozbić możemy, o tyle 1.5 już nie)…
Rozbijamy na wartość przed i po kropce i zwracamy rekurencyjne wywołanie w funkcji samej siebie na wartości przed kropką i po kropce.
Z tymi rozbitymi wartościami funkcja już sobie radzi i zwraca dla każdego return len(str(num), i to ląduje jako return wewnątrz bloku if.
Pisanie o tym sprawia, że funkcja wydaje się być bardziej zagmatwana, niż jest w rzeczywistości, więc spójrzmy na nią raz jeszcze i się zastanówmy, czy rozumiemy:
def num_digits(num):
if "." in str(num):
before, after = str(num).split(".")
return num_digits(before), num_digits(after)
return len(str(num))
print(num_digits(1.55))
#(1, 2)
1.55 ląduje w funkcji num digits. Zamieniamy na napis, i sprawdzamy, czy zawiera kropkę. Zawiera.
Robimy zmienne before i after do których wrzucamy zamieniony na napis num, przecięty po kropce. Warto dodać, że str(num) zwraca nam kopię num zamienioną na string, nie modyfikuje oryginału, dlatego tę konwersję robimy dwa razy (pierwszy raz to porównanie czy kropka znajduje się w kopii num zamienionej na napis)
Zwracamy wywołanie policz liczby na 1 oraz na 55, po przecinku.
Funkcja bezbłędnie wylicza nam ilość cyfr w „1” oraz „55” i je odpowiednio podaje.
Użycie dekoratora
Dla czystego przećwiczenia jak działają dekoratory całą tę logikę przeniesiemy sobie do dekoratora. Funkcja ma podawać ilość cyfr dla typu bez przecinka (kropki) albo ilość cyfr przed i po przecinku (kropce) dla typu zmiennoprzecinkowego.
Natomiast sama funkcja ma mieć logikę uproszczoną do:
def num_digits(num):
return len(str(num))
No to piszemy nasz dekorator:
def handle_floats(fn):
def wrapper(num):
if "." in str(num):
before, after = str(num).split(".")
return fn(before), fn(after)
return fn(num)
return wrapper
@handle_floats
def num_digits(num):
return len(str(num))
print(num_digits(123))
print(num_digits(1.55))
# 3
# (1, 2)
Funkcja dekoratora, przyjmująca fn (funkcję dekorowaną). Wewnątrz wrapper, który zwrócimy. Wrapper z taką samą ilością argumentów co funkcja dekorowana.
Jeżeli „.” znajduje się w argumencie num (jego kopii, zamienionej na napis dla pewności) to rozbijamy liczbę (jej kopię na napis zamienioną) na wartości przed i po kropce.
Zwracamy wywołanie funkcji na wartościach przed i po kropce.
I innym wypadku zwracamy wywołanie funkcji na argumencie num. Funkcja ta świetnie sobie z takimi „bezkropkowymi” argumentami radzi.
I jak widać działa jak tego oczekujemy.
To było takie ćwiczenie na rozgrzewkę. Powinniśmy już mieć jako takie pojęcie o konwersjach różnych typów, rekurencji (funkcja, która wywołuje samą siebie) oraz tego, jak działają dekoratory i co możemy dzięki nim osiągnąć.