Cześć,
dziś wezmę na tapet trzy elementy języka Python:
- pętlę for,
- list comprehension,
- funkcję map.
Pomierzymy sobie ich efektywność w dwóch sytuacjach i na koniec wyciągniemy wnioski, odpowiadając na pytanie: co jest szybsze: pętla for, list comprehension czy funkcja map? Zapraszam!
Sytuacja pierwsza: wynik jest nam potrzebny
Wyobraźmy sobie, że działamy pętlą po jakiejś kolekcji, na jej elementach wykonujemy funkcję i wynik tej funkcji potrzebujemy zachować. Może to być np. wygenerowanie potęg liczb od 0 do 99. Możemy to zrobić na trzy sposoby:
Za pomocą standardowej pętli for:
1 2 3 | numbers = [] for x in range(100): numbers.append(x * x) |
Następnie z użyciem starej, dobrej listy składanej:
1 | numbers = [x * x for x in range(100)] |
I na koniec – funkcji map:
1 | numbers = list(map(lambda x: x * x, range(100))) |
Aby była pewność, że każde z tych podejść da ten sam efekt, dorzucam kawałek kodu, który to potwierdza:
1 2 3 4 5 6 7 8 9 | numbers = [] for x in range(100): numbers.append(x * x) numbers2 = [x * x for x in range(100)] numbers3 = list(map(lambda x: x * x, range(100))) print(numbers == numbers2 == numbers3) |
Jego rezultatem jest oczywiście „True”.
Jeżeli ten sam rezultat można uzyskać na trzy różne sposoby, to który wybrać? Zmierzmy to!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from timeit import timeit code1 = """ numbers = [] for x in range(100): numbers.append(x * x) """ code2 = """ numbers = [x * x for x in range(100)] """ code3 = """ numbers = list(map(lambda x: x * x, range(100))) """ print("For loop:", timeit(code1, number=100000)) print("List comp:", timeit(code2, number=100000)) print("Funkcja map:", timeit(code3, number=100000)) |
Wynik pomiaru jest dość ciekawy:
For loop: 1.0205242090160027 List comp: 0.6673786389874294 Funkcja map: 1.0319141389336437
Tę kategorię wygrywa, przeciwników zostawiając w tyle, podejście z użyciem list comprehensions!
Czekaj, stop!
Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
Sytuacja druga: wynik nie jest nam potrzebny
Nasze podejście trochę się zmienia, gdy zmienia się sytuacja w której używamy tych elementów. W poprzednim przykładzie wynik przetwarzania miał być nam potrzebny w dalszej części programu. Teraz zakładamy, że nie jest on mam potrzebny. Mamy znów trzy podejścia. Kolejno z pętlą for, listą składaną oraz funkcją map:
1 2 | for x in numbers: mock(x) |
1 | [mock(x) for x in numbers] |
1 | list(map(mock, numbers)) |
Tutaj słowo wyjaśnienia – funkcja map tworzy generator i nie uruchomi naszej funkcji, dopóki nie będzie ku temu okazji. Okazję wymuszamy konwersją na listę.
Skoro mamy zebrane rzeczy do analizy, to tak jak wcześniej, zmierzmy to!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from timeit import timeit setup = """ def mock(x): return x * x numbers = range(100) """ code1 = """ for x in numbers: mock(x) """ code2 = """ [mock(x) for x in numbers] """ code3 = """ list(map(mock, numbers)) """ print("For loop:", timeit(code1, number=100000, setup=setup)) print("List comp:", timeit(code2, number=100000, setup=setup)) print("Funkcja map:",timeit(code3, number=100000, setup=setup)) |
Rezultaty pomiaru:
For loop: 0.9080528659978881 List comp: 1.028726649004966 Funkcja map: 0.8416485550114885
Tutaj nie ma już jednogłośnego zwycięzcy. Niby funkcja map wygrywa, ale jest tylko troszkę szybsza niż pętla for.
Podsumownie
Wnioski nasuwają się same, prawda? Jeśli potrzebujesz pracować na danych w dalszej części programu, to używaj listy składanej. Jeśli nie ma potrzeby pracy na wyniku przetwarzania, a samo uruchomienie funkcji jest wystarczające to… To tu nie jest tak jednoznacznie. To, co bym polecał, to jednak używać pętli for. Różnica jest niewielka, a czytelność znacznie lepsza.
Oczywiście nie wszystkie pętle jesteśmy w stanie zapisać z użyciem list składanych czy zapakować do funkcji map. Przykładowo, jeśli Twoja pętla posiada słówka break lub continue, to może być ciężko.
Mateusz Mazurek
super wytłumaczone, robisz świetną robotę, dziękuję
To ja dziękuję za taki komentarz! Od razu się chce bardziej:)
Ja najczęściej korzystam z .map :)
Każdy ma jakieś podejście które sobie wyrobił na przestrzeni czasu:) i myślę że to jest spoko:)
Bardzo fajny artykuł.
Dla mnie map było mało zrozumiałe do czasu aż z JS przerzuciłem się na Pythona. W JS klepałem najczęściej pętle for do wszystkiego. W starszej wersji JS map chyba nie było, natomiast w nowszej wersji JS funkcja map już doszła oraz pojawiło się też bardzo dużo nowych elementów.
Ten artykuł trochę mi przypomina artykuł na moim blogu o optymalizacji kodu w Pythonie.
Pozdrawiam :-).
Cóż za zwięzłość ekspresji, czuję że będę tu zaglądał.
Wiadomo czemu tak zachowują się polecenia? Czemu w przypadku pierwszym jest az taka różnica np.
Nie podglądałem bytecode’u, a pewnie tam znajduje się odpowiedź :D możliwe że kiedyś podejrzę i zrobię osobny wpis. Jeśli mnie uprzedzisz, to będę super wdzięczny, jeśli podzieliłbyś się wnioskami w komentarzy, znacząco rozszerzyło by to zakres artykułu:)