Praca w oparciu o zasadę pojedynczej odpowiedzialności (SRP) w Pythonie, gdy połączenia są drogie

12

Niektóre punkty bazowe:

  • Wywołania metod w języku Python są „drogie” ze względu na ich interpretowaną naturę . Teoretycznie, jeśli twój kod jest wystarczająco prosty, rozbicie kodu w Pythonie ma negatywny wpływ oprócz czytelności i ponownego użycia ( co jest dużym zyskiem dla programistów, a nie dla użytkowników ).
  • Zasada pojedynczej odpowiedzialności (SRP) zapewnia czytelność kodu, jest łatwiejsza do testowania i utrzymania.
  • Projekt ma specjalny rodzaj tła, w którym chcemy czytelnego kodu, testów i wydajności czasowej.

Na przykład taki kod, który wywołuje kilka metod (x4), jest wolniejszy niż następujący, który jest tylko jedną.

from operator import add

class Vector:
    def __init__(self,list_of_3):
        self.coordinates = list_of_3

    def move(self,movement):
        self.coordinates = list( map(add, self.coordinates, movement))
        return self.coordinates

    def revert(self):
        self.coordinates = self.coordinates[::-1]
        return self.coordinates

    def get_coordinates(self):
        return self.coordinates

## Operation with one vector
vec3 = Vector([1,2,3])
vec3.move([1,1,1])
vec3.revert()
vec3.get_coordinates()

W porównaniu z tym:

from operator import add

def move_and_revert_and_return(vector,movement):
    return list( map(add, vector, movement) )[::-1]

move_and_revert_and_return([1,2,3],[1,1,1])

Jeśli mam zrównoważyć coś takiego, to dość obiektywnie tracę wydajność. Umysł to tylko przykład; mój projekt ma kilka mini rutyn z matematyką takich jak ta - Chociaż praca z nimi jest o wiele łatwiejsza, nasi profilerzy nie lubią tego.


Jak i gdzie przyjmujemy SRP bez uszczerbku dla wydajności w Pythonie, ponieważ jego implementacja bezpośrednio na niego wpływa?

Czy istnieją obejścia, takie jak jakiś procesor wstępny, który umieszcza rzeczy w linii do wydania?

A może Python po prostu słabo radzi sobie z rozkładem kodu?

lucasgcb
źródło
19
Pod względem wartości dwa przykłady kodu nie różnią się liczbą obowiązków. SRP nie jest ćwiczeniem liczącym metody.
Robert Harvey,
2
@RobertHarvey Masz rację, przepraszam za słaby przykład, a ja będę go edytować, gdy będę miał czas. W obu przypadkach ma to wpływ na czytelność i łatwość konserwacji i ostatecznie SRP psuje się w bazie kodu, gdy ograniczamy klasy i ich metody.
lucasgcb
4
Zwróć uwagę, że wywołania funkcji są drogie w każdym języku , chociaż kompilatory AOT mają luksus wbudowania
Eevee
6
Użyj implementacji JIT z Pythona, takiej jak PyPy. Powinien głównie rozwiązać ten problem.
Bakuriu,

Odpowiedzi:

17

czy Python po prostu słabo radzi sobie z rozkładem kodu?

Niestety tak, Python działa powoli i istnieje wiele anegdot na temat ludzi drastycznie zwiększających wydajność poprzez wstawianie funkcji i powodowanie, że ich kod jest brzydki.

Istnieje obejście, Cython, który jest skompilowaną wersją Pythona i jest znacznie szybszy.

- Edytuj Chciałem tylko odnieść się do niektórych komentarzy i innych odpowiedzi. Chociaż ich ciąg nie jest może specyficzny dla Pythona. ale bardziej ogólna optymalizacja.

  1. Nie optymalizuj, dopóki nie pojawi się problem, a następnie nie będziesz szukać wąskich gardeł

    Ogólnie dobra rada. Zakłada się jednak, że „normalny” kod jest zwykle wydajny. Nie zawsze tak jest. Poszczególne języki i frameworki mają swoje osobliwości. W tym przypadku wywołania funkcji.

  2. To tylko kilka milisekund, inne rzeczy będą wolniejsze

    Jeśli uruchamiasz swój kod na silnym komputerze stacjonarnym, prawdopodobnie nie obchodzi Cię to, dopóki kod pojedynczego użytkownika zostanie wykonany w ciągu kilku sekund.

    Ale kod biznesowy zwykle działa dla wielu użytkowników i wymaga więcej niż jednego komputera do obsługi obciążenia. Jeśli kod działa dwa razy szybciej, oznacza to, że możesz mieć dwa razy większą liczbę użytkowników lub połowę liczby komputerów.

    Jeśli posiadasz swoje maszyny i centrum danych, generalnie masz duży zapas mocy procesora. Jeśli twój kod działa trochę wolniej, możesz go wchłonąć, przynajmniej do momentu, gdy będziesz musiał kupić drugą maszynę.

    W dzisiejszych czasach przetwarzania w chmurze, gdzie używasz tylko dokładnie takiej mocy obliczeniowej, jakiej potrzebujesz, i więcej, nie ma bezpośredniego kosztu dla niewykonalnego kodu.

    Poprawa wydajności może radykalnie obniżyć główny koszt działalności w chmurze, a wydajność naprawdę powinna być na pierwszym miejscu.

Ewan
źródło
1
Chociaż Odpowiedź Roberta pomaga zasłonić niektóre podstawy potencjalnych nieporozumień związanych z tego rodzaju optymalizacją (która pasuje do tego pytania ), czuję, że odpowiada to na sytuację nieco bardziej bezpośrednio i zgodnie z kontekstem Pythona.
lucasgcb
2
przepraszam, jest trochę za krótki. Nie mam czasu pisać więcej. Ale myślę, że Robert się myli w tej sprawie. Najlepszą radą dotyczącą Pythona wydaje się być profilowanie podczas pisania kodu. Nie zakładaj, że będzie wydajny i zoptymalizuj go tylko, jeśli znajdziesz problem
Ewan
2
@Ewan: Nie musisz najpierw napisać całego programu, aby postępować zgodnie z moją radą. Jedna lub dwie metody są więcej niż wystarczające do uzyskania odpowiedniego profilowania.
Robert Harvey,
1
możesz także wypróbować pypy, czyli pyton JITted
Eevee,
2
@Ewan Jeśli naprawdę martwisz się narzutami związanymi z wydajnością wywołań funkcji, to, co robisz, prawdopodobnie nie jest odpowiednie dla Pythona. Ale tak naprawdę nie mogę wymyślić wielu przykładów. Zdecydowana większość kodu biznesowego jest ograniczona do operacji we / wy, a ciężkie procesory są zwykle obsługiwane przez wywołanie bibliotek rodzimych (numpy, tensorflow i tak dalej).
Voo,
50

Wiele potencjalnych problemów z wydajnością nie jest tak naprawdę problemem w praktyce. Podniesiony przez ciebie problem może być jednym z nich. W języku narodowym nazywamy martwienie się tymi problemami bez dowodu, że są to problemy przedwczesnej optymalizacji.

Jeśli piszesz interfejs dla usługi sieci Web, wywołania funkcji nie będą miały istotnego wpływu na wydajność, ponieważ koszt wysłania danych przez sieć znacznie przekracza czas potrzebny na wywołanie metody.

Jeśli piszesz wąską pętlę, która odświeża ekran wideo sześćdziesiąt razy na sekundę, może to mieć znaczenie. Ale w tym momencie twierdzę, że masz większe problemy, jeśli próbujesz użyć do tego Pythona, zadanie, do którego prawdopodobnie Python nie jest odpowiedni.

Jak zawsze dowiadujesz się, jak mierzyć. Uruchom profil wydajności lub niektóre liczniki czasu w kodzie. Sprawdź, czy to prawdziwy problem w praktyce.


Zasada jednolitej odpowiedzialności nie jest prawem ani mandatem; jest to wytyczna lub zasada. Projektowanie oprogramowania zawsze polega na kompromisach; nie ma absolutów. Często zdarza się, że zmniejsza się czytelność i / lub łatwość utrzymania prędkości, więc może być konieczne poświęcenie SRP na ołtarzu wydajności. Ale nie rób tego, chyba że wiesz, że masz problem z wydajnością.

Robert Harvey
źródło
3
Myślę, że to prawda, dopóki nie wymyśliliśmy przetwarzania w chmurze. Teraz jedna z dwóch funkcji kosztuje 4 razy więcej niż druga
Ewan
2
@Ewan 4 razy może nie mieć znaczenia, dopóki nie zmierzysz, aby był wystarczająco znaczący. Jeśli Foo zajmuje 1 ms, a Bar zajmuje 4 ms, to nie jest dobrze. Dopóki nie uświadomisz sobie, że przesyłanie danych przez sieć zajmuje 200 ms. W tym momencie spowolnienie Bar nie ma tak wielkiego znaczenia. (Tylko jeden możliwy przykład tego, że X razy wolniej nie robi zauważalnej lub znaczącej różnicy, niekoniecznie musi być super realistyczny.)
Becuzz
8
@Ewan Jeśli obniżenie rachunku pozwoli Ci zaoszczędzić 15 USD miesięcznie, ale naprawa i testowanie zajmie kontrahentowi 125 USD / godzinę 4 godziny, z łatwością uzasadnię, że nie warto poświęcić czasu firmie (a przynajmniej nie zrobić dobrze teraz, jeśli czas na wprowadzenie na rynek ma kluczowe znaczenie itp.). Zawsze są kompromisy. A to, co ma sens w jednej sytuacji, może nie w innej.
Becuzz,
3
twoje rachunki AWS są bardzo niskie
Ewan
6
@Ewan AWS zaokrągla do sufitu i tak partiami (standardowo jest to 100ms). Co oznacza, że ​​tego rodzaju optymalizacja pozwala Ci zaoszczędzić wszystko tylko wtedy, gdy konsekwentnie unika się pchania do następnej części.
Delioth,
2

Po pierwsze, kilka wyjaśnień: Python jest językiem. Istnieje kilka różnych interpretatorów, które mogą wykonywać kod napisany w języku Python. Implementacja referencyjna (CPython) jest zwykle tym, do której się odwołuje się, gdy ktoś mówi o „Python” tak, jakby to była implementacja, ale ważne jest, aby być precyzyjnym, gdy mówi się o charakterystyce wydajności, ponieważ mogą się one bardzo różnić między implementacjami.

Jak i gdzie przyjmujemy SRP bez uszczerbku dla wydajności w Pythonie, ponieważ jego implementacja bezpośrednio na niego wpływa?

Przypadek 1.) Jeśli masz czysty kod Python (<= język Python w wersji 3.5, 3.6 ma „obsługę wersji beta”), który opiera się tylko na czystych modułach Python, możesz objąć SRP wszędzie i użyć PyPy, aby go uruchomić. PyPy ( https://morepypy.blogspot.com/2019/03/pypy-v71-released-now-uses-utf-8.html ) to interpreter języka Python, który ma kompilator Just in Time (JIT) i może usunąć funkcję wywołać narzut, o ile ma wystarczająco dużo czasu na „rozgrzanie” przez śledzenie wykonanego kodu (kilka sekund IIRC). **

Jeśli jesteś ograniczony do używania interpretera CPython, możesz wyodrębnić powolne funkcje do rozszerzeń napisanych w C, które zostaną wstępnie skompilowane i nie będą obciążone żadnymi narzutami interpretera. Nadal możesz używać SRP wszędzie, ale twój kod zostanie podzielony między Python i C. To, czy jest to lepsze, czy gorsze pod względem konserwacji, niż selektywne porzucanie SRP, ale trzymanie się tylko kodu Python zależy od twojego zespołu, ale jeśli masz sekcje krytyczne pod względem wydajności kodu, będzie on niewątpliwie szybszy niż nawet najbardziej zoptymalizowany czysty kod Pythona interpretowany przez CPython. Wiele najszybszych bibliotek matematycznych Pythona korzysta z tej metody (numpy i scipy IIRC). Co jest miłym przejściem do przypadku 2 ...

Przypadek 2.) Jeśli masz kod Pythona, który używa rozszerzeń C (lub polega na bibliotekach, które używają rozszerzeń C), PyPy może, ale nie musi być przydatny, w zależności od tego, jak zostały napisane. Szczegółowe informacje można znaleźć na stronie http://doc.pypy.org/en/latest/extending.html , ale podsumowanie jest takie, że CFFI ma minimalny narzut, podczas gdy CTypes jest wolniejszy (użycie go z PyPy może być nawet wolniejsze niż CPython)

Cython ( https://cython.org/ ) to kolejna opcja, z którą nie mam tyle doświadczenia. Wspominam o tym w celu uzupełnienia, aby moja odpowiedź była „samodzielna”, ale nie wymagała żadnej wiedzy specjalistycznej. Po moim ograniczonym użyciu czułem, że musiałem ciężko pracować, aby uzyskać te same ulepszenia prędkości, które mogłem uzyskać „za darmo” z PyPy, a jeśli potrzebowałem czegoś lepszego niż PyPy, równie łatwo było napisać własne rozszerzenie C ( co ma tę zaletę, że jeśli ponownie użyję kodu w innym miejscu lub wyodrębnię jego część do biblioteki, cały mój kod może nadal działać pod dowolnym interpreterem Pythona i nie musi być uruchamiany przez Cython).

Boję się, że będę „zamknięty” w Cython, podczas gdy każdy kod napisany dla PyPy może również działać w CPython.

** Kilka dodatkowych uwag na temat PyPy w produkcji

Bądź bardzo ostrożny przy dokonywaniu wyborów, które mają praktyczny efekt „zablokowania cię” w PyPy w dużej bazie kodu. Ponieważ niektóre (bardzo popularne i przydatne) biblioteki stron trzecich nie grają dobrze z wyżej wymienionych powodów, może to powodować bardzo trudne decyzje później, jeśli zdasz sobie sprawę, że potrzebujesz jednej z tych bibliotek. Moje doświadczenie polega przede wszystkim na korzystaniu z PyPy w celu przyspieszenia niektórych (ale nie wszystkich) mikrousług, które są wrażliwe na wydajność w środowisku firmowym, gdzie dodaje ono znikomą złożoność do naszego środowiska produkcyjnego (mamy już wdrożonych wiele języków, niektóre z różnymi głównymi wersjami, takimi jak 2.7 vs 3.5 działa mimo to).

Odkryłem, że używanie zarówno PyPy, jak i CPython regularnie zmuszało mnie do pisania kodu, który opiera się tylko na gwarancjach wynikających z samej specyfikacji języka, a nie na szczegółach implementacji, które mogą ulec zmianie w dowolnym momencie. Może ci się wydawać, że myślenie o takich szczegółach stanowi dodatkowe obciążenie, ale uznałem je za cenne w moim rozwoju zawodowym i myślę, że jest „zdrowe” dla całego ekosystemu Python.

Steven Jackson
źródło
Tak! Rozważałem skupienie się na rozszerzeniach C w tym przypadku zamiast porzucić zasadę i napisać kod wieloznaczny, inne odpowiedzi sprawiły wrażenie, że będzie wolno, niezależnie od tego, czy zmieniłem interpreter referencyjny - aby to wyjaśnić, OOP nadal być według ciebie rozsądnym podejściem?
lucasgcb
1
w przypadku 1 (2. akapit) nie otrzymujesz tego samego, wywołując funkcje, nawet jeśli same funkcje są spełnione?
Ewan
CPython jest jedynym tłumaczem, który jest ogólnie traktowany poważnie. PyPy jest interesujący , ale z pewnością nie jest szeroko rozpowszechniony. Co więcej, jego zachowanie różni się od CPython i nie działa z niektórymi ważnymi pakietami, np. Scipy. Niewielu rozsądnych programistów poleciłoby PyPy do produkcji. W związku z tym rozróżnienie między językiem a implementacją nie ma w praktyce znaczenia.
jpmc26
Myślę jednak, że trafiłeś w sedno. Nie ma powodu, dla którego nie miałbyś lepszego tłumacza ani kompilatora. Python jako język nie jest nieodłączny. Po prostu utknąłeś z praktycznymi realiami
Ewan
@ jpmc26 Użyłem PyPy w produkcji i polecam rozważenie tego innym doświadczonym programistom. Jest świetny dla mikrousług korzystających z falconframework.org dla lekkich interfejsów API odpoczynku (jako jeden przykład). Zachowanie jest różne, ponieważ programiści opierają się na szczegółach implementacji, które NIE są gwarancją języka, nie jest powodem do nieużywania PyPy. Jest to powód do przepisania kodu. Ten sam kod może się i tak złamać, jeśli CPython dokona zmian w swojej implementacji (co jest wolne, o ile nadal jest zgodne ze specyfikacją języka).
Steven Jackson