Upraszczamy kod w Pythonie - map(), filter(), reduce()

4 minut(y)

Jakiś czas temu na blogu Dawida Kozaka (swoję drogą bardzo polecam i równocześnie pozdrawiam Autora) natrafiłem na ciekawy wpis dotyczący upraszczania kodu Javascript przy wykorzystaniu funkcji .map(), .filter() i .reduce(). Zainspirowało mnie to do napisania niniejszego artykułu, który w zamierzeniu ma przedstawić zastosowanie tych funkcje oraz pokazać alternatywne metody w języka Python.

Funkcje (map, filter) dostępna jest w bibliotece standardowej Pythona zarówno w wersji 2 jak i wersji 3 języka Python. Funkcje reduce w wersji 3 Pythona została przeniesiona do modułu functools. Pomiędzy wersjami 2 i 3 różnica jest w typie zwracanych wartości. Dla wersji 2 jest to lista, a dla wersji 3 iterator.

Zacznijmy od początku czyli funkcji map().

map()

Funkcja map() daje nam możliwość wykonania zadanej funkcji dla każdego elementu kolekcji.

map(function, element_iterowalny)

Rozważmy taki problem: porzebujemy napisać funckję, która podwoi wartości elementów całkowitych tablicy przekazanych jako argument. dla danej tablicy liczb całkowitych potrzebujemy napisać funkcję, która zwróci nam nową tablicę z podwojoną wartością każdego elementu. Napiszmy najpierw funckję która zrobi to z wykorzystaniem pętli. Będzie przetwarzać element po elemencie w kolejnych iteracjach.

numbers = [7, 4, 9, 6, 8, 1]

def double(items):
    output = []
    for val in items:
      output.append(val * 2 )
    return output

result = double(numbers)
print(result)

W wyniku wykonania programu otrzymamy:

[14, 8, 18, 12, 16, 2]

Teraz zrefaktorujmy kod funkcji double() wykorzystując map()

...
def double(items):
    return list(map(lambda x: x*2, items))
...

W Pythonie w wersji 2 funkcja map() zwraca listę, w wersji 3 zwraca iterator, dlatego w tym przypadku w funkcji double() konwertujemy go do listy za pomocą list(). Prosciej? Zdecydowanie prościej.

W powyższym przykładzie do podwajania wartości użyliśmy funkcji anonimowej lambda. Nie jesteśmy w tym miejscu ograniczeni wyłącznie do funkcji anonimowych i możemy korzystać z wcześniej napisanych funkcji zewnętrznych. Zobaczmy jak wygląda użycie takiej zewnętrznej funkcji, którą przekażemy do map():

def double(value):
    return value * 2

numbers = [7, 4, 9, 6, 8, 1]
result = list(map(double, numbers))
print(result)

Jak się okazuje w języku Python mamy do dyspozycji wyrażenia listowe (ang. list comprehensions), które w tym przypadku także możemy zastosować (zamiast map()):

def double(items):
    return [v * 2 for v in items]

Równie prosto, a nawet można by powiedzieć, że czytelniej.

Zdarzają się sytuacje, w których będziemy potrzebowali aby funkcja modyfikjąca otrzymała dodatkowe argumenty. Oto jak powinniśmy napisać kod, aby działał jak należy:

def multiply(value, multiplier):
    return value * multiplier

...
# dla _map()_
result = list(map(lambda v: multiply(v, 3), numbers))

# dla wyrażeń listowych
result = [multiply(v, 3) for v in numbers]
...

Niektórzy z Was mogą zadać teraz pytanie: no dobrze mamy wyrażenia listowe i funkcję map(), to które z nich należy stosować? które rozwiązanie jest lepsze? Na to pytanie nie ma jednoznacznej odpowiedzi. W przeważającej większości przypadków nie będzie miało znaczenia, którą z tych metod będziemy stosować (poza przypadkiem, że na przykład w konkretnym projekcie czy zespole narzucona jest któraś z tych konwencji). Wyrażenia listowe uważa się za bardziej “pythonowe” (tzw. pythonic way), natomiast map() bliższa jest entuzjastom programowania funkcyjnego. Gdyby ktoś był niezdecydowany, którego z tych rozwiązań używać, to polecam zapoznanie się z bardziej technicznymi aspektami obu rozwiązań.

filter()

Funkcja filter() tworzy nową listę elementów na podstawie wejściowej listy elementów, wybierając tylko te wartości, dla których funkcja testując zwróci prawdę (True). Jak to miało miejsce w przypdaku omawiania map() zacznijmy od przedstawienia problemu i rozwiązania sposobem konwencjonalnym:

API serwisu pogodowego zwraca nam lsitę miast z określonych województw wraz z aktualną temperaturą. Należy utworzyć listę tylko tych miast, w których temperatura jest dodatnia.

data_from_api = [
    {'city': 'Kraków', 'province_id': 8, 'current_temp': 3.5},
    {'city': 'Warszawa', 'province_id': 1, 'current_temp': 2.8},
    {'city': 'Suwałki', 'province_id': 9, 'current_temp': -0.5},
    {'city': 'Gdańsk', 'province_id': 3, 'current_temp': -0.1},
    {'city': 'Rzeszów', 'province_id': 7, 'current_temp': 3.9},
    {'city': 'Wrocław', 'province_id': 2, 'current_temp': 5.0},
]

def filter_cities(cities):
  result = []
  for city in cities:
    if city['current_temp'] >= 0:
      result.append(city)
  return result

cities = filter_cities(data_from_api)

Za pomocą funkcji filter() realizujemy zadanie w sposób następujący:

cities = list(filter(lambda city: city['current_temp'] >= 0, data_from_api)

i za pomocą wyrażeń listowych:

cities = [city for city in data_from_api if city['current_temp'] >= 0]

Parafrazując słowa Panormixa - “prościej się nie da” ;)

reduce()

Przyszła teraz pora na funkcję reduce(). Funkcja ta jest bardzo użyteczne przy przeprowadzaniu rożnego rodzaju obliczeń na elementach listy (tablicy) i zwracaniu obliczonego wyniku. Stosuje ona obliczenia kroczące (ang. rolling computation) dla kolejnych par wartości elementów listy.

Pokażmy jej działanie na danych z poprzedniego przykładu, tylko zmieńmy sobie cel. Naszym celem niech będzie obliczenie średniej temperatury z temperatur w poszczególnych miastach. Do obliczenia średniej potrzebna jest suma wartości temperatur i liczba miast.

Tradycyjnie na początek metoda konwencjonalna:

sum = 0
for city in data_from_api:
  sum += city['current_temp']
avg_temp = sum / len(data_from_api)
# 2.433333333333333

Celowo pomijam tutaj sprawdzenie czy ilość elementów data_from_api nie jest przypadkiem równa 0!

Użyjmy teraz reduce():

from functools import reduce
avg_temp = reduce((lambda s, city: s + city['current_temp']), data_from_api, 0) / len(data_from_api)
# 2.433333333333333

i wyrażeń listowych

avg_temp = sum([city['current_temp'] for city in data_from_api]) / len(data_from_api)

Kończąc, zachęcam Was do eksperymentowania z kodem z różnymi rozwiązaniami związanymi zarówno z funkcjiami map(), filter(), reduce() jak i z wyrażeniami listowymi w Pythonie. Zapraszam też goroąco do komentowania i dzielenia się spostrzeżeniami.

Tagi:

Kategorie:

Ostatnia aktualizacja:

Zostaw komentarz