Jak zoptymalizować skrypty w języku Python, aby uzyskać lepszą wydajność

Jak Zoptymalizowac Skrypty W Jezyku Python Aby Uzyskac Lepsza Wydajnosc



Optymalizacja skryptów Pythona w celu uzyskania lepszej wydajności polega na identyfikowaniu i usuwaniu wąskich gardeł w naszym kodzie, dzięki czemu działa on szybciej i wydajniej. Python to popularny i wydajny język programowania, który jest obecnie używany w wielu aplikacjach, w tym w analizie danych, projektach ML (uczenie maszynowe), tworzeniu stron internetowych i wielu innych. Optymalizacja kodu w języku Python to strategia mająca na celu poprawę szybkości i wydajności programu deweloperskiego podczas wykonywania dowolnej czynności przy użyciu mniejszej liczby linii kodu, mniejszej pamięci lub dodatkowych zasobów. Duży i nieefektywny kod może spowolnić działanie programu, co może skutkować niskim zadowoleniem klienta i potencjalną stratą finansową lub koniecznością włożenia większej ilości pracy w naprawę i rozwiązywanie problemów.

Jest to konieczne podczas wykonywania zadania wymagającego przetwarzania kilku działań lub danych. Dlatego też wymiana i ulepszenie niektórych nieefektywnych bloków kodu i funkcjonalności może dać niesamowite rezultaty, takie jak poniższe:

  1. Zwiększ wydajność aplikacji
  2. Twórz czytelny i zorganizowany kod
  3. Uprość monitorowanie błędów i debugowanie
  4. Oszczędzaj znaczną moc obliczeniową i tak dalej

Profiluj swój kod

Zanim zaczniemy optymalizować, konieczne jest zidentyfikowanie części kodu projektu, które ją spowalniają. Techniki profilowania w Pythonie obejmują pakiety cProfile i profile. Korzystaj z takich narzędzi, aby ocenić szybkość wykonywania określonych funkcji i wierszy kodu. Moduł cProfile generuje raport zawierający szczegółowe informacje o czasie działania każdej funkcji skryptu. Ten raport może nam pomóc znaleźć funkcje, które działają wolno, co umożliwi nam ich ulepszenie.







Fragment kodu:



import cProfil Jak cP
def obliczSuma ( numer wejściowy ) :
suma_liczb_wejściowych = 0
chwila numer wejściowy > 0 :
suma_liczb_wejściowych + = numer wejściowy% 10
numer wejściowy // = 10
wydrukować ( „Suma wszystkich cyfr w numerze wejściowym to: 'suma_of_input_numbers'' )
powrót suma_liczb_wejściowych
def główna_funkcja ( ) :
cP. uruchomić ( „oblicz sumę (9876543789)” )
Jeśli __nazwa__ == '__główny__' :
główna_funkcja ( )

Program wykonuje w sumie pięć wywołań funkcji, jak widać w pierwszym wierszu wyniku. Szczegóły każdego wywołania funkcji są pokazane w kilku kolejnych wierszach, łącznie z liczbą wywołań funkcji, całkowitym czasem trwania funkcji, czasem trwania wywołania oraz całkowitą ilością czasu w funkcji (w tym wszystkie funkcje, które on wywołuje).



Dodatkowo program drukuje raport na ekranie zachęty, który pokazuje, że program kończy czas wykonania wszystkich swoich zadań w ciągu 0,000 sekundy. To pokazuje, jak szybki jest program.





Wybierz odpowiednią strukturę danych

Charakterystyka wydajności zależy od struktury danych. W szczególności słowniki umożliwiają szybsze wyszukiwanie niż listy dotyczące przechowywania ogólnego. Wybierz strukturę danych, która jest najbardziej odpowiednia dla operacji, które przeprowadzimy na Twoich danych, jeśli je znasz. Poniższy przykład bada skuteczność różnych struktur danych dla identycznego procesu, aby określić, czy element w strukturze danych jest obecny.



Oceniamy czas potrzebny na sprawdzenie, czy element występuje w każdej strukturze danych — liście, zestawie i słowniku — i porównujemy je.

OptymalizujDataType.py:

import Czas Jak tt
import losowy Jak rndobj
# Wygeneruj listę liczb całkowitych
lista_danych_losowych = [ rndobj. randek ( 1 , 10000 ) Do _ W zakres ( 10000 ) ]
# Utwórz zestaw z tych samych danych
losowy_zestaw_danych = ustawić ( lista_danych_losowych )

# Utwórz słownik z tymi samymi danymi co klucze
obj_DataDictionary = { na jednego: Nic Do na jednego W lista_danych_losowych }

# Element do wyszukania (istnieje w danych)
losowy_numer_do_znalezienia = rndobj. wybór ( lista_danych_losowych )

# Zmierz czas, aby sprawdzić członkostwo na liście
czas_listy = tt. Czas ( lambda : losowy_numer_do_znalezienia W lista_danych_losowych , numer = 1000 )

# Zmierz czas, aby sprawdzić przynależność do zbioru
set_time = tt. Czas ( lambda : losowy_numer_do_znalezienia W losowy_zestaw_danych , numer = 1000 )

# Zmierz czas, aby sprawdzić członkostwo w słowniku
dykt_czas = tt. Czas ( lambda : losowy_numer_do_znalezienia W obj_DataDictionary , numer = 1000 )

wydrukować ( F „Czas sprawdzania członkostwa na liście: {list_time:.6f} sekund” )
wydrukować ( F „Ustaw czas sprawdzania członkostwa: {set_time:.6f} sekund” )
wydrukować ( F „Czas sprawdzania członkostwa w słowniku: {dict_time:.6f} sekund” )

Ten kod porównuje wydajność list, zestawów i słowników podczas sprawdzania członkostwa. Ogólnie rzecz biorąc, zbiory i słowniki są znacznie szybsze niż listy do testów członkostwa, ponieważ korzystają z wyszukiwań opartych na skrótach, więc ich średnia złożoność czasowa wynosi O(1). Z drugiej strony listy muszą przeprowadzać wyszukiwania liniowe, co skutkuje testami członkostwa ze złożonością czasową O(n).

  Zrzut ekranu komputera. Opis wygenerowany automatycznie

Używaj funkcji wbudowanych zamiast pętli

Liczne wbudowane funkcje lub metody w Pythonie mogą służyć do wykonywania typowych zadań, takich jak filtrowanie, sortowanie i mapowanie. Używanie tych procedur zamiast tworzenia własnych pętli pomaga przyspieszyć kod, ponieważ często są one zoptymalizowane pod kątem wydajności.

Zbudujmy przykładowy kod, aby porównać wydajność tworzenia pętli niestandardowych przy użyciu wbudowanych funkcji dla typowych zadań (takich jak map(), filter() i sorted()). Ocenimy skuteczność różnych metod mapowania, filtrowania i sortowania.

WbudowanyInFunctions.py:

import Czas Jak tt
# Przykładowa lista liczb_list
lista_numerów = lista ( zakres ( 1 , 10000 ) )

# Funkcja do kwadratu liczb_listy za pomocą pętli
def Square_using_loop ( lista_numerów ) :
wynik_kwadratowy = [ ]
Do na jednego W lista_numerów:
wynik_kwadratowy. dodać ( na jednego ** 2 )
powrót wynik_kwadratowy
# Funkcja filtrowania listy parzystej za pomocą pętli
def filter_even_using_loop ( lista_numerów ) :
wynik_filtra = [ ]
Do na jednego W lista_numerów:
Jeśli na jednego % 2 == 0 :
wynik_filtra. dodać ( na jednego )
powrót wynik_filtra
# Funkcja sortowania listy_numerów za pomocą pętli
def sort_using_loop ( lista_numerów ) :
powrót posortowane ( lista_numerów )
# Zmierz czas kwadratury liczb_listy za pomocą map()
czas_mapy = tt. Czas ( lambda : lista ( mapa ( lambda x: x ** 2 , lista_numerów ) ) , numer = 1000 )
# Zmierz czas filtrowania listy parzystej za pomocą filter()
czas_filtrowania = tt. Czas ( lambda : lista ( filtr ( lambda x: x% 2 == 0 , lista_numerów ) ) , numer = 1000 )
# Zmierz czas sortowania listy_numerów za pomocą sorted()
sortowany_czas = tt. Czas ( lambda : posortowane ( lista_numerów ) , numer = 1000 )
# Zmierz czas kwadratury liczb_listy za pomocą pętli
pętla_mapa_czas = tt. Czas ( lambda : kwadrat_używanie_pętli ( lista_numerów ) , numer = 1000 )
# Zmierz czas filtrowania listy parzystej za pomocą pętli
czas_filtrowania_pętli = tt. Czas ( lambda : filter_even_using_loop ( lista_numerów ) , numer = 1000 )
# Zmierz czas sortowania listy_numerów za pomocą pętli
pętla_sortowany_czas = tt. Czas ( lambda : sort_using_loop ( lista_numerów ) , numer = 1000 )
wydrukować ( „Lista numerów zawiera 10000 elementów” )
wydrukować ( F „Czas mapy(): {map_time:.6f} sekund” )
wydrukować ( F „Czas filtrowania(): {filter_time:.6f} sekund” )
wydrukować ( F „Czas sortowania(): {sorted_time:.6f} sekund” )
wydrukować ( F „Czas pętli (mapy): {loop_map_time:.6f} sekund” )
wydrukować ( F „Czas pętli (filtrowania): {loop_filter_time:.6f} sekund” )
wydrukować ( F „Czas pętli (posortowany): {loop_sorted_time:.6f} sekund” )

Prawdopodobnie zaobserwujemy, że wbudowane funkcje (map(), filter() i sorted()) są szybsze niż niestandardowe pętle dla tych typowych zadań. Wbudowane funkcje w Pythonie oferują bardziej zwięzłe i zrozumiałe podejście do wykonywania tych zadań i są wysoce zoptymalizowane pod kątem wydajności.

Optymalizuj pętle

Jeśli konieczne jest pisanie pętli, istnieje kilka technik, które możemy zastosować, aby je przyspieszyć. Ogólnie rzecz biorąc, pętla range() jest szybsza niż iteracja wstecz. Dzieje się tak, ponieważ funkcja range() generuje iterator bez odwracania listy, co w przypadku długich list może być kosztowną operacją. Dodatkowo, ponieważ funkcja range() nie tworzy nowej listy w pamięci, zużywa mniej pamięci.

ZoptymalizujLoop.py:

import Czas Jak tt
# Przykładowa lista liczb_list
lista_numerów = lista ( zakres ( 1 , 100 000 ) )
# Funkcja umożliwiająca iterację po liście w odwrotnej kolejności
def pętla_odwrotna_iteracja ( ) :
wynik_odwrotny = [ ]
Do J W zakres ( tylko ( lista_numerów ) - 1 , - 1 , - 1 ) :
wynik_odwrotny. dodać ( lista_numerów [ J ] )
powrót wynik_odwrotny
# Funkcja do iteracji po liście za pomocą funkcji range()
def iteracja_zakresu pętli ( ) :
wynik_zakres = [ ]
Do k W zakres ( tylko ( lista_numerów ) ) :
wynik_zakres. dodać ( lista_numerów [ k ] )
powrót wynik_zakres
# Zmierz czas potrzebny na wykonanie iteracji odwrotnej
czas_odwrotny = tt. Czas ( pętla_odwrotna_iteracja , numer = 1000 )
# Zmierz czas potrzebny na wykonanie iteracji zakresu
zakres_czas = tt. Czas ( iteracja_zakresu pętli , numer = 1000 )
wydrukować ( „Lista numerów zawiera 100 000 rekordów” )
wydrukować ( F „Czas odwrotnej iteracji: {reverse_time:.6f} sekund” )
wydrukować ( F „Czas iteracji zakresu: {range_time:.6f} sekund” )

Unikaj niepotrzebnych wywołań funkcji

Każde wywołanie funkcji wiąże się z pewnym obciążeniem. Kod działa szybciej, jeśli unika się niepotrzebnych wywołań funkcji. Na przykład zamiast wielokrotnie wykonywać funkcję obliczającą wartość, spróbuj zapisać wynik obliczenia w zmiennej i użyć go.

Narzędzia do profilowania

Aby dowiedzieć się więcej o wydajności Twojego kodu, oprócz wbudowanego profilowania, możemy skorzystać z zewnętrznych pakietów profilowania, takich jak cProfile, Pyflame lub SnakeViz.

Wyniki w pamięci podręcznej

Jeśli nasz kod musi wykonywać kosztowne obliczenia, możemy rozważyć buforowanie wyników, aby zaoszczędzić czas.

Refaktoryzacja kodu

Refaktoryzacja kodu, aby był łatwiejszy do odczytania i utrzymania, jest czasami niezbędną częścią jego optymalizacji. Szybszy program może być również czystszy.

Skorzystaj z kompilacji just-in-time (JIT)

Biblioteki takie jak PyPy lub Numba mogą zapewnić kompilację JIT, która może znacznie przyspieszyć niektóre typy kodu Pythona.

Uaktualnij Pythona

Upewnij się, że używasz najnowszej wersji Pythona, ponieważ nowsze wersje często zawierają ulepszenia wydajności.

Równoległość i współbieżność

W przypadku procesów, które można zrównoleglić, przeanalizuj techniki równoległe i synchronizacyjne, takie jak przetwarzanie wieloprocesowe, wątki lub asyncio.

Pamiętaj, że benchmarking i profilowanie powinny być głównymi motorami optymalizacji. Skoncentruj się na ulepszaniu obszarów naszego kodu, które mają największy wpływ na wydajność i stale testuj wprowadzone ulepszenia, aby mieć pewność, że przyniosą oczekiwane efekty bez wprowadzania kolejnych defektów.

Wniosek

Podsumowując, optymalizacja kodu w Pythonie ma kluczowe znaczenie dla poprawy wydajności i efektywności wykorzystania zasobów. Programiści mogą znacznie zwiększyć szybkość wykonywania i responsywność swoich aplikacji w języku Python, korzystając z różnych technik, takich jak wybór odpowiednich struktur danych, wykorzystanie wbudowanych funkcji, ograniczenie dodatkowych pętli i efektywne zarządzanie pamięcią. Ciągłe testy porównawcze i profilowanie powinny kierować wysiłkami optymalizacyjnymi, zapewniając, że ulepszenia kodu odpowiadają rzeczywistym wymaganiom wydajnościowym. Aby zagwarantować długoterminowy sukces projektu i zmniejszyć ryzyko wprowadzenia nowych problemów, optymalizacja kodu powinna być stale równoważona celami czytelności i łatwości konserwacji kodu.