Kontynuujemy poznawanie Pythona i jego możliwości. W tym odcinku poznamy kilka rzeczy, głównie związanych z pakowaniem i rozpakowywaniem, choć nie tylko, wypełnimy sobie także pewne luki w wiedzy.

Do dzieła.

Zmienna pusta

Znamy już pętlę for. Wiemy, że możemy iterować po np. każdej literze danego napisu:

for letter in msg:
    print(letter.upper())

# H
# E
# L
# L
# O

Możemy też przechodzić po liczbach w jakimś zakresie:

for num in range(1,6):
    print(num, num ** 2)

# 1 1
# 2 4
# 3 9
# 4 16
# 5 25

Liczby od 1 do 5 wypisane oraz do kwadratu podniesione. A co, jeżeli chcemy np. 6 razy wypisać „Hello World”?

for num in range(6):
    print("Hello World")

# Hello World
# Hello World
# Hello World
# Hello World
# Hello World
# Hello World

Tylko jak widać mamy tutaj zmienną num, z którą nic nie robimy. Porównajmy to z tym:

for num in range(6):
    print(num)

# 0
# 1
# 2
# 3
# 4
# 5

Tutaj wypisujemy zmienną num, jest nam zatem potrzebna. Wypisując „hello world” 6 razy – nie jest potrzebna. Nawet IDE powinno wyświetlać tę zmienną na szaro, jako nieużywaną.

Co możemy zrobić? Użyć zmiennej pustej:

for _ in range(6):
    print("Hello World")

# Hello World
# Hello World
# Hello World
# Hello World
# Hello World
# Hello World

Jest to tylko dobra praktyka, natomiast gdy jakiejś zmiennej nie potrzebujemy, chcemy tylko wykonać pętlę 6 razy niekoniecznie z tej liczby korzystając, używamy takiej zmiennej pustej „_”.

Rozpakowanie listy i krotki

Napiszmy sobie prostą funkcję, która dodaje do siebie dwie liczby. Nazwijmy ją sum:

def sum(a, b):
    return a + b

print(sum(2,2))
#4

Swoją drogą add byłoby lepszą nazwą, sum „przysłania” wbudowaną funkcję sum, ale nie to jest teraz ważne.

Spróbujmy przekazać do tej funkcji argumenty w postaci listy:

def sum(a, b):
    return a + b

print(sum([2,2]))
#TypeError: sum() missing 1 required positional argument: 'b'

Mamy error, przekazaliśmy jeden argument (choć na zdrowy rozum dwa), jedną listę. Brakuje argumentu b. I w ogóle nasza funkcja nie jest przygotowana na coś takiego. Ona wie co zrobić z dwoma liczbami, nie z jedną listą.

Możemy oczywiście zmienić naszą funkcję, jej logikę:

def sum(lst):
    return lst[0] + lst[1]

print(sum([2,2]))
#4

Ale nie o to tutaj chodzi. Chcemy „rozpakować” naszą listę do dwóch zmiennych. Jak tego dokonać? Do tego służy operator gwiazdki:

lst = [2,2]
print(lst)
print(*lst)
# [2, 2]
# 2 2

Jak widać gwiazdka przed listą „rozpakowywuje” listę do pojedynczych elementów. Spróbujmy zatem z naszą małą funkcją dodającą:

def sum(a, b):
    return a + b

print(sum(2,2))
print(sum(*[2,2]))
# 4
# 4

Jak widać, rozpakowana lista to jak podawanie argumentów po przecinku. Do rozpakowania służy operator „*”.

Rozpakować możemy też tuplę:

def sum(a, b):
    return a + b

my_lst = [2,2]
my_tuple = (2,2)

print(sum(2,2))
print(sum(*my_lst))
print(sum(*my_tuple))
# 4
# 4
# 4

Rozpakowanie słownika

Napiszmy sobie funkcję, która przyjmuje wartości red, green i blue (domyślnie ustawione na 0) i zwraca odpowiednio formatowany napis:

def rgb(red=0,green=0,blue=0):
    return f"rgb({red}, {green}, {blue})"

print(rgb(blue=255, green=255, red=255))
# rgb(255, 255, 255)

Kolejność zamieniłem, bo kolejność ważna nie jest, gdy mamy argumenty podawane w sposób nazwa=wartość. Jako że wszystkie są domyślne, możemy sobie niektóre darować:

def rgb(red=0,green=0,blue=0):
    return f"rgb({red}, {green}, {blue})"

print(rgb(blue=255, green=255))
# rgb(0, 255, 255)

No dobrze, a teraz chcemy, aby wrzucić do wywołania funkcji słownik zawierający nasze argumenty.

def rgb(red=0,green=0,blue=0):
    return f"rgb({red}, {green}, {blue})"

my_dict = {
    "red": 255,
    "green" : 200,
    "blue" : 100
}
print(rgb(my_dict))
# rgb({'red': 255, 'green': 200, 'blue': 100}, 0, 0)

Tutaj ciekawa sprawa, błędu nie ma, ponieważ używamy argumentów domyślnych. Tylko IDE podkreśla, że funkcja oczekuje typu liczbowego, a dostała słownik.

Co tu się stało? Słownik został potraktowany jako argument pierwszy, red, zaś green i blue zostały wydrukowane z wartością 0, domyślną. Oczywiście nie o to nam chodziło.

My chcemy ten słownik wypakować. I do rozpakowania słownika służy nam operator „**”:

def rgb(red=0,green=0,blue=0):
    return f"rgb({red}, {green}, {blue})"

my_dict = {
    "red": 255,
    "green" : 200,
    "blue" : 100
}
print(rgb(**my_dict))
# rgb(255, 200, 100)

Teraz otrzymaliśmy to, czego chcieliśmy, czyli rozpakowanie słownika i wrzucenie argumentów w postaci klucz=wartość do naszej funkcji. Kolejność argumentów w słowniku nie ma znaczenia w tym wypadku:

def rgb(red=0,green=0,blue=0):
    return f"rgb({red}, {green}, {blue})"

my_dict = {
    "blue" : 100,
    "red": 255,
    "green" : 200,

}
print(rgb(**my_dict))
# rgb(255, 200, 100)
print(rgb(blue=100, red=255, green=200))
# rgb(255, 200, 100)

Oba zapisy znaczą to samo, tylko w pierwszym wypakowujemy słownik używając znaku **

Ćwiczenie – rozpakuj listę i słownik

Przy pomocy rozpakowania listy chcemy wypisać imiona funkcją print. Imiona mają być oddzielone przecinkiem zaś po ich wypisaniu mamy dostać enter i napis „END OF LINE”. Na początek jak to zrobić w ogóle:

print("Bob", "John", "Jane", sep=", ", end="\nEND OF LINE")
# Bob, John, Jane
# END OF LINE

Funkcja print przyjmuje dowolną ilość argumentów, tutaj nasze imiona, oraz argumenty kluczowe (klucz=wartość), tutaj sep, czyli jak oddzielać poszczególne elementy, oraz end, czyli co wypisać na koniec (enter i end of line).

Zróbmy sobie listę imion i tak je wypiszmy:

names = ["Bob", "John", "Jane"]
print(*names, sep=",", end="\nEND OF LINE")
# Bob,John,Jane
# END OF LINE

Rozpakowanie listy imion jako prostych argumentów i dwa argumenty słownikowe, sep oraz end, które już sobie „z palca” musimy podać.

Teraz postarajmy się osiągnąć to samo, ale przy pomocy listy i słownika.

names = ["Bob", "John", "Jane"]
args = {
    "sep" : ", ",
    "end" : "\nEND OF LINE"
}
print(*names, **args)
# Bob,John,Jane
# END OF LINE

Jak widać zrobiliśmy i listę prostych argumentów i słownik z argumentami klucz=wartość, jedno i drugie wypakowaliśmy używając odpowiednio znaku „*” oraz „**”.

Działa.

Pakowanie – przeciwieństwo rozpakowania

Argumenty możemy też pakować do listy. Do naszej funkcji możemy przekazać grupę argumentów oddzielonych przecinkiem, które zostaną spakowane do jednej listy. Robi się to tak:

def pack_args(*args):
    print(args)
    print(type(args))

pack_args(1,2,3)
# (1, 2, 3)
# <class 'tuple'>

Ten sam znak, ale w definicji funkcji, w nawiasach, oznacza spakowanie tych argumentów. W zasadzie nie do listy, ale tupli, jak się możemy o tym przekonać.

Argumenty słownikowe też pakować możemy. Zwyczajowo nazywamy je kwargs (keyword arguments):

def pack_kwargs(**kwargs):
    print(kwargs)
    print(type(kwargs))
    
    
pack_kwargs(name="John", age=30)
# {'name': 'John', 'age': 30}
# <class 'dict'>

Jak widać podaliśmy argumenty słownikowe w formie klucz=wartość i zostały one spakowane do jednego słownika kwargs.

Te rzeczy (to jest pakowanie) bywają przydatne, gdy nie wiemy ile i jakie argumenty mogą trafić do naszej funkcji. Chcemy jednak, aby działała zawsze, niezależnie od ilości tych argumentów.

Print to przykład takiej funkcji. Innym przykładem może być funkcja, która przyjmuje dowolną ilość argumentów i je sumuje:

def my_sum(*args):
    sum = 0
    for num in args:
        sum += num
    return sum

print(my_sum(1,2,3)) #6

Tutaj nie wiemy ile argumentów zechce podać użytkownik. Ale chcemy, aby wszystkie zostały do siebie dodane. Pakujemy je więc do tupli args, następnie w pętli przechodzimy po wszystkich i dodajemy do siebie.

Rozpakowanie do zmiennych

Przypomnijmy sobie enumerate, które daje nam tuplę zawierającą indeks oraz element:

msg = "hello"
for idxel in enumerate(msg):
    print(idxel)
# (0, 'h')
# (1, 'e')
# (2, 'l')
# (3, 'l')
# (4, 'o')

Tuplę zawierającą indeks i element możemy sobie rozpakować do dwóch zmiennych:

msg = "hello"
for idx, el in enumerate(msg):
    print(idx, el)
# 0 h
# 1 e
# 2 l
# 3 l
# 4 o

Robimy to, podając odpowiednią ilość zmiennych po przecinku. Podobnie możemy robić z każdą tego typu tuplą, np. ze słownikowym items, które zwraca tuplę klucz-wartość:

my_dict = {
    "name" : "John",
    "age" : 30
}
for keyval in my_dict.items():
    print(keyval)

for key, val in my_dict.items():
    print(key, val)

# ('name', 'John')
# ('age', 30)
# name John
# age 30

Możemy też tak przypisywać elementy jakiejś listy do zmiennych:

first, last = ["Bob", "John"]
print(first)
print(last)
# Bob
# John

Możemy też do jednej zmiennej złapać pierwszy element, a do drugiej spakować całą resztę:

first, *rest = ["Bob", "John", "Jane"]
print(first)
print(rest)
# Bob
# ['John', 'Jane']

Możemy nawet złapać na przykład pierwszy element, ostatni zaś wszystko w środku spakować do listy:

first, *middle, last = ["Bob", "John", "Jane", "Jim"]
print(first)
print(middle)
print(last)
# Bob
# ['John', 'Jane']
# Jim

Tak samo możemy rozpakować napis:

first, *middle, last = "kajak"
print(first)
print(middle)
print(last)
print(first == last)
# k
# ['a', 'j', 'a']
# k
# True

Tutaj pierwsza i ostatnia litera lądują do zmiennych first i last, zaś do middle pakujemy środek. Porównujemy też, czy pierwsza i ostatnia są takie same.

Zadanie – sprawdź, czy tekst jest palindromem

Dla przećwiczenia tej zabawy z rozpakowywaniem zrobimy sobie zadanie, którego celem jest napisanie funkcji sprawdzającej, czy tekst jest palindromem.

Palindrom to tekst, który zapisany wspak wygląda tak samo jak zapisany normalnie. Przykładem tego jest „kajak”, „anna”, „oko”. A także, z programistycznego punktu widzenia, ciąg pusty „” oraz jednoliterowy napis, na przykład „a” (jakby nie patrzeć od przodu i tyłu wygląda tak samo).

No dobrze, zobaczmy jak możemy wyciągnąć pierwszą i ostatnią literę (którą będziemy porównywać) oraz środek z takich wyrazów jak „oko”, „anna” czy „kajak”:

first, *middle, last = "kajak"
print(first)
print(middle)
print(last)
# k
# ['a', 'j', 'a']
# k

W przypadku kajaka mamy dwie litery k oraz środek, który raz jeszcze trzeba będzie poddać temu samemu procesowi, by porównać a oraz a. Jak wygląda oko?

first, *middle, last = "oko"
print(first)
print(middle)
print(last)
# o
# ['k']
# o

Mamy dwie litery 'o’ oraz jedną literę 'k’ w liście middle, literę, której już z niczym porównywać nie będziemy, bo nie ma z czym.

A jak wygląda „anna”?

first, *middle, last = "anna"
print(first)
print(middle)
print(last)
# a
# ['n', 'n']
# a
first, *middle, last = ["n", "n"]
print(first)
print(middle)
print(last)
# n
# []
# n

Na początku dwie litery 'a’, które porównamy ze sobą, jako środek dwie litery „n”. Potem dwie litery „n” oraz pusta już lista middle.

Na podstawie tego możemy już mieć jakiś ogląd jaką logikę chcemy zastosować w naszej funkcji. Osobiście bardzo tu mi brakuje pętli do-while, która najpierw wykonuje akcję, zaś później sprawdza warunek. Żeby bowiem tak sprawdzić palindrom musimy:

  • Rozbić słowo na pierwszą literę, ostatnią literę i środek
  • Porównać pierwszą z ostatnią, jeżeli nie są takie same – FAŁSZ
  • Jeżeli środek nie jest pusty ani nie zawiera tylko jednego elementu (jak oko, gdzie środek to „k”) kontynuować pętlę.
  • Teraz nasz środek rozbijamy na pierwszą i ostatnią literę oraz środek i porównujemy
def is_palindrome(text):
    first, *middle, last = text
    if first != last:
        return False
    while len(middle) > 1:
        first, *middle, last = middle
        if first != last:
            return False
    return True

print(is_palindrome("oko"))
print(is_palindrome("anna"))
print(is_palindrome("kajak"))
print(is_palindrome("hello"))
# True
# True
# True
# False

Nie mamy w Pythonie pętli do-while, która najpierw coś wykona zaś później sprawdzi warunek. Radzimy sobie jak możemy.

Pierwsza i ostatnia litera tekstu zostaje przypisana do zmiennych first i last, zaś środek spakowany do listy middle.

Porównujemy, czy pierwsza i ostatnia litera są takie same. Jeżeli nie są – Fałsz, koniec funkcji.

Jeżeli nasz środek ma długość większą niż 1 (środek „oko” warunku nie spełnia, środek „kajak” już tak) to dalej wybieramy pierwszą i ostatnią literę z naszego środka teraz, zaś do middle pakujemy środek. Środek środka, jakkolwiek by to nie brzmiało.

Porównujemy pierwszą i ostatnią literę, jeżeli się różnią – fałsz. Jeżeli nie to kontynuujemy pętlę. Jeżeli pętla się skończy, zwracamy prawdę.

A zatem w przypadku „oko” pierwsza i ostatnia litera zostają na początku funkcji porównane, zaś jednoelementowy środek „k” nawet naszej pętli nie uruchomi.

W przypadku „kajak” litery k i k zostaną porównane. Następnie ze środka wybieramy „a” oraz „a” zostawiając w środku „j”. Po porównaniu jednoelementowy środek „j” przestanie spełniać warunek pętli.

W przypadku „anna” jest jeszcze inaczej. Porównanie a z a, środek „nn”. Porównanie „n” z „n”, środek pusta lista. Pusta lista ma długość 0, czyli mniejszą niż 1. Pętla chodzić przestaje, zwracamy prawdę.

Warto to sobie na spokojnie przeanalizować, bo ten skrypt wcale trudny nie jest. Natomiast powinniśmy do niego dodać jeszcze jedno sprawdzenie, to jest czy długość tekstu jaki dostaliśmy na początku to nie jest tekst pusty „” albo jeden znak np. „a”. Bo te teksty też są palindromami, od początku i na wspak wyglądają tak samo. Zatem:

def is_palindrome(text):
    if len(text) < 2:
        return True
    first, *middle, last = text
    if first != last:
        return False
    while len(middle) > 1:
        first, *middle, last = middle
        if first != last:
            return False
    return True


print(is_palindrome(""))
print(is_palindrome("A"))

# True
# True