Witam w trzecim artykule z serii dotyczącej tego, czego nie powinniśmy robić w Pythonie. Załączam dwa poprzednie wpisy:
i bez przydługiego wstępu, przejdźmy do treści.
Z rozwagą używajmy metody insert z obiektu listy
Metoda insert(index, element) pozwala dodać do listy element na konkretnym miejscu (indeksie). Zerknijmy na kod:
1 2 3 4 5 6 | numbers = [] numbers.insert(0, 'jeden') numbers.insert(len(numbers), 'dwa') print(numbers) |
Ponieważ najpierw dodaliśmy do listy na indeksie zerowym słowo „jeden”, a potem na indeksie równym długości listy (a więc tu na pierwszym) słowo „dwa”, wobec tego uzyskany wynik to:
['jeden', 'dwa']
I to wszystko jest pewnie jasne.
To przed czym chciałbym przestrzec, to sytuacja w której dodajemy metodą insert jakiś element na zerowy indeks, ponieważ Python będzie zmuszony do przesunięcia reszty zawartości tej listy.
1 2 3 4 5 6 | numbers = [] numbers.insert(0, 'jeden') numbers.insert(0, 'dwa') print(numbers) |
wynik wykonania się tego kodu to
['dwa', 'jeden']
Czemu w takim razie nie powinno się używać tej metody do dodawania elementów na początek listy? Bo to jest bardzo nieefektywne rozwiązanie.
Jeśli piszesz swój kod i już wiesz, że będziesz potrzebował dodawać elementy na początek listy, radzę skorzystać z obiektu deque. Wykorzystajmy timeit’a i zmierzmy czas wykonania się bliźniaczych operacji dla listy i dla deque (double-ended queue czyli lista dwukierunkowa):
1 2 3 4 | from timeit import timeit print(timeit('s.appendleft(37)', 'import collections; s = collections.deque()', number=100000)) print(timeit('s.insert(0,37)', 's = []', number=100000)) |
wykonanie takiego kodu da rezultat:
0.007789244002196938 # dla deque
2.211549069004832 # dla listy
Różnica wynika ze złożoności obliczeniowej obu rozwiązań, a złożoność obliczeniowa ze sposobu implementacji ich w Pythonie. Złożoność obliczeniowa dla list.insert() jest liniowa, a dla deque.appendleft() jest stała, czyli niezależna od ilości elementów.
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 :)
Brak rozpakowywania list
Czasem potrzebujemy przypisać elementy z listy do zmiennych, by móc ich potem używać. Możemy to zrobić tak:
1 2 3 4 5 6 7 | some_values = ['jeden', 2, 'three'] first = some_values[0] second = some_values[1] third = some_values[2] print(first, second, third) |
co oczywiście – zadziała, ale jest to bardzo mało Pythonowe rozwiązanie. Możesz zrobić to lepiej w taki sposób:
1 2 3 4 5 | some_values = ['jeden', 2, 'three'] first, second, third = some_values print(first, second, third) |
Czemu tak? Bo to ładne i przede wszystkim Python’owe rozwiązanie. Wynik obu rozwiązań jest taki sam!
Używanie listy do kolekcji elementów w których będziemy tylko szukać
Najlepiej sytuację o której myślę pokaże kawałek kodu:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Shop: STATES = ['open', 'closed', 'temporary_closed', 'nearly_closed'] def __init__(self, state): self.state = state def has_valid_state(self): return self.state in self.STATES s = Shop('open') print(s.has_valid_state()) |
Klasa sklepu ma swoje możliwe stany i w oparciu o wartość pola zwracamy informację, czy aktualny stan jest poprawny. Na 100% widziałeś podobny kod wiele razy.
To podejście oczywiście spełni swoją funkcję. Jest jednak nieoptymalne.
Jeśli chcemy tworzyć kolekcję, której jedynym zastosowaniem jest sprawdzanie, czy jakiś element w niej istnieje, np jak w klasie wyżej – do zawężenia możliwych stanów elementu, powinniśmy zrezygnować z listy. I użyć zbioru.
I znów, udowodnijmy to korzystając z timeit’a!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from timeit import timeit setup1 = """ STATES = {"OPEN", "NEARLY_CLOSED", "NEARLY_OPEN", "TEMPORARY_CLOSED", "CLOSED"} """ setup2 = """ STATES = ["OPEN", "NEARLY_CLOSED", "NEARLY_OPEN", "TEMPORARY_CLOSED", "CLOSED"] """ print("Checking in set with not exists element: ", timeit("'NO_EXISTS' in STATES", setup=setup1, number=100000)) print("Checking in list with not exists element: ", timeit("'NO_EXISTS' in STATES", setup=setup2, number=100000)) print("Checking in set with exists element: ", timeit("'NEARLY_OPEN' in STATES", setup=setup1, number=100000)) print("Checking in list with exists element: ", timeit("'NEARLY_OPEN' in STATES", setup=setup2, number=100000)) |
i wynik kodu:
Checking in set with not exists element: 0.0025714500006870367
Checking in list with not exists element: 0.006017967999468965
Checking in set with exists element: 0.0023319259998970665
Checking in list with exists element: 0.0035892980004064157
Sprawdzamy tu 4 przypadki:
- szukanie w zbiorze nie istniejącego elementu
- szukanie w liście nie istniejącego elementu
- szukanie w zbiorze istniejącego elementu
- szukanie w liście istniejącego elementu
I, co widać po czasach, wygrywa zbiór. Zatem polecam używać go w sytuacjach kiedy rozważamy posiadanie kolekcji w której będziemy tylko szukać.
Tym wnioskiem kończymy ten wpis. Będą kolejne :)
Mateusz Mazurek
Trochę już w Pythonie programuję, ale dzięki tej serii widzę co mogę robić lepiej :)
Dzięki za docenienie! To wiele znaczy :)
Czy korzystanie z seta zamiast listy wygrywa tylko czas sprawdzania i to jeszcze nie rzędem a jakimś mniejszym przelicznikiem?
Jak dla mnie nie jest to argument za używaniem set’a w tym przypadku. Jeśli używamy pythona i myślimy tylko o czasie wykonywania kodu w ms… to używamy złego języka w mojej skromnej opinii :) Bardziej bym się zastanawiał wtedy czy nie używać enuma i sprawdzania typów na wejściu aniżeli bawił się w takie optymalizację kodu
Heloł! Myślę że tak proste optymalizacje jak ta to raczej kwestia bardziej czystego kodu i świadomości niż myślenia tylko o czasie wykonywania kodu:) ja bym chyba nawet nie używał słowa „optymalizacja” a bardziej „dobra praktyka”. To trochę jak wbijanie gwoździa – możesz to zrobić śrubokrętem ale wygodniej będzie młotkiem:)
To jest elementarne dobieranie właściwej struktury danych. Optymalizacja prędkości wykonania jest jedynie produktem ubocznym i zakładam, że autor użył jej jako przykładu jednej z wielu korzyści płynących z dobrych praktyk programistycznych.
Rozpakowywanie listy można też wykonać w następujący sposób (przydatne np. do przechowywania kompaktowo wartości stałych):
Tak!:)