Dość proste zadanie, które nauczy nas kilku ciekawych funkcji wbudowanych oraz naprawdę wielu zagadnień języka Python takich jak:

  • pętla for
  • składanie list (list comprehension)
  • funkcje map i filter
  • funkcje lambda

Zadanie, które wykonamy polega na napisaniu funkcji, która usunie w tekście wszystkie spacje oraz inne znaki niealfanumeryczne i zwróci tekst zapisany małymi literami, tak odfiltrowany.

Sposób 1 – pętla for

Przeiterujemy sobie po pętli for i dodamy tylko te znaki, które są alfanumeryczne, zapisane małą literą. Taki tekst zwrócimy:

def change_text(s):
    output = ""
    for char in s:
        if char.isalnum():
            output += char.lower()
    return output

print(change_text("Anna anna"))
#annaanna

Tak wygląda nasza funkcja. Bierzemy argument s (od string, napis). Tworzymy zmienną output, pusty napis. W pętli przechodzimy po każdym znaku (char od character, znak, tak tę zmienną nazwałem) i tylko jeżeli ten znak jest alfanumeryczny to go dodajemy do zmiennej output, w wersji małą literą. Zmienną output zwracamy, funkcja działa jak należy.

Oczywiście są różne sposoby na osiągnięcie tego. Można się bawić z czymś takim, jak replace. Oto przykład:

print("Anna anna".replace(" ", ""))
#Annaanna

Replace zamienia spację na brak spacji w tym wypadku. Ale nasza funkcja robi to prościej, sprawdzając czy znak jest alfanumeryczny czy nie i dodając go tylko, kiedy jest.

def change_text(s):
    output = ""
    for char in s:
        if char.isalnum():
            output += char.lower()
    return output

print(change_text("A man, a plan, a canal, Panama"))
#amanaplanacanalpanama

Dzięki temu spacje i przecinki też mamy z głowy. Ale jeżeli chcemy się bawić w replace – możemy:

msg = "A man, a plan, a canal, Panama"
msg = msg.replace(" ", "")
print(msg)
#Aman,aplan,acanal,Panama
msg = msg.replace(",", "")
print(msg)
#AmanaplanacanalPanama
msg = msg.lower()
print(msg)
#amanaplanacanalpanama

Taką logikę moglibyśmy zastosować w naszej funkcji. Pamiętać jedynie musimy, aby zrobić replace dla każdego znaku, jakiego nie chcemy.

Można to odrobinę zautomatyzować:

msg = "$$$A man, a plan, a canal, Panama!!!"
for char in "$ !,":
    msg = msg.replace(char, "")
print(msg)
#AmanaplanacanalPanama
msg = msg.lower()
print(msg)
#amanaplanacanalpanama

Tutaj dla każdego podanego przez nas znaku (dolar, spacja, wykrzyknik, przecinek) funkcja zamieni go na „” czyli usunie z tekstu. Jest to jakieś rozwiązanie, natomiast jeżeli wygląda ono zbyt „dziwnie” to możemy sobie listy użyć, na to samo wyjdzie:

msg = "$$$A man, a plan, a canal, Panama!!!"
for char in ["$", " ", ",", "!"]:
    msg = msg.replace(char, "")
print(msg)
#AmanaplanacanalPanama
msg = msg.lower()
print(msg)
#amanaplanacanalpanama

Różnicy w zasadzie nie ma, no chyba, że chcemy za jednym zamachem usunąć więcej niż jeden znak, wtedy tylko lista. Oto przykład czegoś takiego:

msg = "https://$$$A man, a plan, a canal, Panama!!!"
for char in ["https://", "$", " ", ",", "!"]:
    msg = msg.replace(char, "")
print(msg)
#AmanaplanacanalPanama
msg = msg.lower()
print(msg)
#amanaplanacanalpanama

Wtedy jako pierwsze wykonuje się:

msg = "https://$$$A man, a plan, a canal, Panama!!!"
msg = msg.replace("https://", "")
print(msg)
#$$$A man, a plan, a canal, Panama!!!

A potem reszta. Tak się bawić możemy, ale myślę, że sposób podany na początku jest jak najbardziej dobry, dobrze określony warunek (znak jest alfanumeryczny) sprawia, że nie musimy wykonywać przesadnie wielu kroków w naszej logice. Tym niemniej, jeżeli ktoś lubi replace – może go używać. Istnieje wiele sposobów na rozwiązywanie tego samego problemu.

Sposób 2 – filter i map

Tekst bardzo łatwo zamienić na listę. A nasze zadanie to świetna sposobność, aby nauczyć się filter i map. Zamiana tekstu na listę:

msg = "Anna anna"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ' ', 'a', 'n', 'n', 'a']

Filter to taka funkcja, która bierze listę oraz funkcję, jaką ma wykonać, aby zwrócić odfiltrowane dane. Później jeszcze, niestety, musimy obiekt filter na powrót konwertować do listy, tak to w Pythonie wygląda:

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
print(list(filter(lambda char: char.isalnum(), lst)))
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Zamieniamy na listę. Następnie filter z funkcją lambda, która przyjmuje znak i zwraca sprawdzenie, czy ten znak jest alfanumeryczny, czy nie. Ten, który zwraca Prawdę nie zostaje odfiltrowany. Wykonujemy na liście lst, filter object zamieniamy z powrotem na listę. Odfiltrowane to, co odfiltrowane miało być.

Oczywiście, jeżeli kogoś przeraża lambda, możemy użyć funkcji zwykłej. Musimy tylko pamiętać, aby przekazać do filter jej nazwę (bez nawiasów) bo ona ma być zastosowana na każdym elemencie do sprawdzenia, czy przechodzi przez filter, nie zawołana. Oto przykład bez lambdy:

def is_okay(char):
    return char.isalnum()

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
print(list(filter(is_okay, lst)))
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Jeżeli jeszcze dziwnie wygląda, możemy to uczynić nawet bardziej czytelnym:

def is_okay(char):
    if char.isalnum():
        return True
    return False

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
print(list(filter(is_okay, lst)))
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

A nawet jeszcze bardziej czytelnym, nie zaszkodzi powtórek, zwłaszcza ucząc się nowych, nie takich łatwych wcale zagadnień:

def is_okay(char):
    if char.isalnum():
        return True
    else:
        return False

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
print(list(filter(is_okay, lst)))
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Okej, wiemy jak odfiltrować dane z listy, których nie chcemy. A jak zastosować na nich coś, co, bo ja wiem, zamieni wielką literę na małą, jeżeli zajdzie taka potrzeba? To aż tak nie jest nam potrzebne, bo możemy przecież na całym tekście dać .lower():

def is_okay(char):
    if char.isalnum():
        return True
    else:
        return False

msg = "Anna, anna!"
lst = list(msg.lower())
print(lst)
#['a', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
print(list(filter(is_okay, lst)))
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Ale jesteśmy ambitni. Chcemy poznać filter i map. Filter bierze funkcję, która zwraca prawda/fałsz i poddaje każdy element temu sprawdzeniu, zwracając listę tych, które przeszły. Zaś map robi coś innego: wykonuje pewną funkcję na każdym elemencie i zwraca taką listę. Przykład z brzegu:

lst1 = [1,2,3,4,5]
lst2 = list(map(lambda x: x * 2, lst1))
print(lst2)
#[2, 4, 6, 8, 10]

Tutaj map bierze każdą liczbę i ją mnoży razy 2 i taką listę zwraca. A teraz pamiętajmy: chcemy mieć napis, który jest pozbawiony znaków niealfanumerycznych (mamy od tego filter) zaś każdy jego element zapisany małą literą (weźmiemy do tego map):

def is_okay(char):
    if char.isalnum():
        return True
    else:
        return False


def to_lower(char):
    return char.lower()

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
lst = list(filter(is_okay, lst))
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = list(map(to_lower, lst))
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Tak to wygląda. Filter bierze is_okay, która sprawdza czy znak jest alfanumeryczny. Przechodzą tylko te, które są i zwracają True. Map zaś bierze funkcję to_lower, która zwraca znak zapisany małą literą i tak zamienioną listę zwraca. Jasne, są inne sposoby, ale chcemy poznać map i filter. A także działanie lambdy (dodam tylko, że zadanie, które robimy jest dobrym wstępem do ugryzienia czegoś takiego jak palindrom, czyli tekst, który pisany normalnie i wspak to to samo – np. „anna”, „Anna anna”, „kajak” to palindromy).

Okej, upraszczamy:

def is_okay(char):
    return char.isalnum()

def to_lower(char):
    return char.lower()

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
lst = list(filter(is_okay, lst))
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = list(map(to_lower, lst))
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Mało. Chcemy lambdy:

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
lst = list(filter(lambda char: char.isalnum(), lst))
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = list(map(lambda char: char.lower(), lst))
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Jeszcze raz – filter wykonuje naszą funkcję na każdym elemencie i jeśli nasza funkcja zwraca prawdę, element przechodzi. Funkcja przyjmuje znak i zwraca wynik porównania czy znak jest alfanumeryczny. Obiekt filter zamieniamy na list, działa.

Map bierze następną funkcję i wykonuje ją na każdym elemencie naszej listy. Nasza funkcja przyjmuje znak i zwraca znak małą literą zapisany. Obiekt map zamieniamy na list, działa.

Trzeba teraz znaleźć sposób, aby listę zamienić na tekst i będziemy mogli zbierać to wszystko do kupy, tworząc funkcję. Całe szczęście, listę na napis można zamienić. Trzeba mieć tylko „klej” (najlepiej pusty napis, „”) i funkcję join.

Przykład takiego czegoś:

lst = ["H", "e", "l", "l", "o"]
print("!".join(lst))
#H!e!l!l!o
print("".join(lst))
#Hello

W pierwszym przypadku klejem jest znak wykrzyknika i nie jest to super rozwiązanie. W drugim pusty string i działa, jak należy. Po co nam w ogóle twórcy Pythona dali możliwość wybierania własnego kleju?

Cóż, to nie zawsze musi być lista znaków.

lst = ["Hello", "world"]
print(" ".join(lst))
#Hello world

W przypadku wyrazów spacja może być dobrym klejem. Ale dość już o tym. Zróbmy naszą funkcję w końcu jak należy, czyli zamień na listę, odfiltruj niealfanumeryczne, zamień każdy znak na małą, z listy zrób napis:

msg = "Anna, anna!"
lst = list(msg)
print(lst)
#['A', 'n', 'n', 'a', ',', ' ', 'a', 'n', 'n', 'a', '!']
lst = list(filter(lambda char: char.isalnum(), lst))
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = list(map(lambda char: char.lower(), lst))
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
txt = "".join(lst)
print(txt)
#annaanna

Okej, teraz czas na zrobienie z tego porządnej funkcji:

def change_text(s):
    lst = list(s)
    lst = list(filter(lambda char: char.isalnum(), lst))
    lst = list(map(lambda char: char.lower(), lst))
    return "".join(lst)

print(change_text("Anna anna!"))
#annaanna

Myślę, że to już powinno być dla nas w miarę zrozumiałe. Jeżeli nie, to z pewnością następny sposób bardziej nam przypadnie do gustu

Sposób 3 – list comprehension

Zarówno zamienianie tekstu na listę, filtrowanie listy oraz wykonywanie na jej elementach jakichś operacji można uzyskać przy użyciu tzw. list comprehension.

Zamienianie tekstu na listę:

msg = "Anna"
lst = [letter for letter in msg]
print(lst)
#['A', 'n', 'n', 'a']

Filtrowanie listy, np. ze znaków, które nie są alfanumeryczne:

msg = "Anna anna!"
lst = [letter for letter in msg]
print(lst)
#['A', 'n', 'n', 'a', ' ', 'a', 'n', 'n', 'a', '!']
lst = [char for char in lst if char.isalnum()]
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Oraz wykonanie na każdym elemencie listy akcji lower:

msg = "Anna anna!"
lst = [letter for letter in msg]
print(lst)
#['A', 'n', 'n', 'a', ' ', 'a', 'n', 'n', 'a', '!']
lst = [char for char in lst if char.isalnum()]
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = [char.lower() for char in lst]
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']

Teraz jeszcze akcja join, aby mieć tekst:

msg = "Anna anna!"
lst = [letter for letter in msg]
print(lst)
#['A', 'n', 'n', 'a', ' ', 'a', 'n', 'n', 'a', '!']
lst = [char for char in lst if char.isalnum()]
print(lst)
#['A', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
lst = [char.lower() for char in lst]
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
txt = "".join(lst)
print(txt)
#annaanna

Można to wszystko zrobić za jednym zamachem:

msg = "Anna anna!"
lst = [char.lower() for char in msg if char.isalnum()]
txt = "".join(lst)
print(txt)
#annaanna

Czyli znak zapisz małą literą dla każdego znaku w msg, jeżeli ten znak jest alfanumeryczny. Zamienianie na listę, operacja na jej elementach i filtrowanie w jednym. Potem zamiana znowu na tekst. Prościej już się chyba nie da? Da.

msg = "Anna anna!"
txt = "".join([char.lower() for char in msg if char.isalnum()])
print(txt)
#annaanna

Listę tworzymy w locie, wewnątrz join, jako argument tej funkcji i w zasadzie robimy to samo. Nie wchodząc w zbyt skomplikowane szczegóły można to jeszcze tak zapisać:

msg = "Anna anna!"
txt = "".join(char.lower() for char in msg if char.isalnum())
print(txt)
#annaanna

Nic tylko stworzyć naszą funkcję:

def change_text(s):
    return "".join(char.lower() for char in s if char.isalnum())


msg = "Anna anna!"
print(change_text(msg))
#annaanna

Działa jak należy. Zamienia nasz tekst na zapisany bez spacji i innych znaków alfanumerycznych, małą literą. A po co nam coś takiego? Choćby po to, aby sprawdzić, czy dany wyraz bądź zdanie jest palindromem – zapisane wspak daje to samo, co zapisane normalnie:

def change_text(s):
    return "".join(char.lower() for char in s if char.isalnum())


def is_palindrome(text):
    text = change_text(text)
    return text == text[::-1]


print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
print(is_palindrome("kajak"))
#True
print(is_palindrome("A man, a plan, a canal, Panama"))
#True
print(is_palindrome("abc"))
#False

Tutaj zamieniamy tekst na jego wersje bez znaków alfanumerycznych i małą literą a następnie sprawdzamy, czy ten tekst od tyłu wygląda tak samo jak w normalnej kolejności.

Nie chcę się tu wgłębiać przesadnie, bo usuwanie spacji i znaków specjalnych jest głównie przydatne w metodzie z dwoma wskaźnikami, gdzie idziemy od lewej i prawej na raz i porównujemy czy tekst wygląda tak samo, poświęciłem temu zagadnieniu osobny wpis, natomiast chciałem też pokazać, że nie jest to jakiś głupi przykład, całkowicie przeze mnie wymyślony. Porównując tekst z jego odbiciem lustrzanym poprzez [::-1] na dobrą sprawę możemy porównywać go ze znakami specjalnymi i spacjami, tylko do małej litery wszystko zmniejszyć (aby „Anna” zapisana wspak wyglądała identycznie), możemy więc to zapisać tak:

def is_palindrome(text):
    text = text.lower()
    return text == text[::-1]


print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
print(is_palindrome("kajak"))
#True
print(is_palindrome("A man, a plan, a canal, Panama"))
#False
print(is_palindrome("abc"))
#False

Aczkolwiek tutaj widzimy, że już na przedostatnim przykładzie, ewidentnym palindromie, nasza funkcyjka poległa. Tym niemniej, gdyby tylko znaki specjalne były symetryczne, nie byłoby to potrzebne:

def is_palindrome(text):
    text = text.lower()
    return text == text[::-1]


print(is_palindrome("!anna!"))
#True

Tym niemniej, jeżeli chcemy sprawdzić bardziej zaawansowane struktury, czy są palindromami, to musimy pamiętać, że wielkość liter nie może nam robić znaczenia, podobnie jak znaki specjalne, które trzeba stamtąd po prostu usunąć, żeby móc sprawdzić, czy dany tekst i jego wersja wspak są takie same:

def change_text(s):
    return "".join(char.lower() for char in s if char.isalnum())


def is_palindrome(text):
    text = change_text(text)
    return text == text[::-1]

print(is_palindrome("A man, a plan, a canal, Panama"))
#True

Jeżeli zaciekawił nas temat palidromów, to podkreślam, najfajniejszym sposobem rozwiązania zadania jest tzw. metoda dwóch wskaźników, nie takie sztuczki z lustrzanym odbiciem jak [::-1]. Opisałem to w innym wpisie. Natomiast tak, to co robiliśmy na 3 sposoby (odfiltrowanie tekstu ze znaków niealfanumerycznych oraz zapisanie go małą literą) to nie taki całkiem wymyślony przykład zadania, ale coś, co jest częścią większej układanki, takiej jak napisanie skryptu, który sprawdza, czy dany tekst jest palindromem, czy nie jest.