Jak pisać czytelny kod w Pythonie?

in #pl-artykuly6 years ago (edited)

Intro

Słowem wstępu - nie jestem ekspertem. W Pythonie grzebię od dwóch lat (w tym rok komercyjnie), w wolnym czasie kaleczę jeszcze (póki co nieudolnie) Rusta. Python ze względu na swoją specyfikę jest popularny do robienia różnego rodzaju skryptów, testów i tym podobnych - więc bardzo wielu ludzi korzysta z niego nie mając żadnej wiedzy jak kod w tym języku powinien wyglądać - i kończą z nieczytelnym bajzlem. Nie jest to jednak tylko dziedzina ludzi którzy Pythonem zajmują się przy okazji - widziałem kod komercyjny od którego włosy stawały na głowie. Kod komercyjny pisany przez ludzi którzy mają po parę lat doświadczenia.

Nie przedłużając - zaczynajmy.

Funkcje - giganty

Chyba najczęściej spotykana przyprawa do spaghetti.

Funkcje na parę tysięcy linii, z wcięciami tak wielkimi, że człowiek musi odsuwać się na parę metrów w tył żeby ogarnąć kod w całości.

Nie róbcie tak - rozdzielajcie kod na małe kawałki.

ale co z tego mam? Czemu powinienem to robić?

Raz, że kod jest czytelniejszy - dzielisz go na wiele kawałków, opisanych, przewidywalnych, łatwych do przetestowania, GIGANTYCZNIE upraszcza debugging, dwa - staje się bardziej uniwersalny. Każda funkcja powinna zawierać jakiś docstring, więc czytający od razu wie co się dzieje, sama nazwa często jest dużo mówiąca - tzw. self documenting code. Ostatnio robiłem refactor jednej gigantycznej funkcji na ponad 1,5 tysiąca linijek w Pythonie - po refactorze łącznie miała około 400... i to wliczając, oczywiście, wszystkie stworzone funkcje.

W niebezpiecznym_wonszu jest to o tyle ważne, że odpowiednio wywołując kolejne funkcje zamiast wszystko robić w jednym molochu unikamy nadmiaru wcięć.

okej, to kiedy powinienem wyciąć kod i wrzucić go gdzie indziej?

Ja na ogół robię tak, że mam jedną funkcję z "logiką biznesową" (nazwa bardzo nieformalna) z której po kolei wywołuję wszystkie kolejne instrukcje zajmujące się już brudną robotą. Dobrym podejściem jest też to, żeby każda funkcja robiła jedną rzecz, ale robiła ją bardzo dobrze i uniwersalnie.

Imaginujcie sobie taki kod:

item1 = self.env['model.model'].get([ 
    <miliard tupl  w postaci (1, '=', 2)>
    ])
if not item1:
    item1 = self.env['model.model'].create([ 
    <miliard tupl  w postaci (1, '=', 2)>
    ])

# tutaj dzieją się różne rzeczy z itemem 1

item2 = self.env['model.model'].get([ 
    <miliard tupl  w postaci (1, '=', 2)>
    ])
if not item2:
    item1 = self.env['model.model'].create([ 
    <miliard tulp w postaci (1, '=', 2)>
    ])

# tutaj dzieją się różne rzeczy z itemem 2

# ...i tak 10 razy

Bardzo łatwo go uprościć do postaci:

def get_or_create_item(attributes_list):
    attributes = [ (i[0], '=', i[1]) for i in attributes_list ]
    item1 = self.env['model.model'].get(attributes)

    if not item:
        item1 = self.env['model.model'].create(attributes)
    return item


def foo():
    item1 = get_or_create_item([
        (1, 2), 
        (3, 4), 
        (5, 6)
            ])
    # tu dzieją się rzeczy z itemem1

    item2 = get_or_create_item([
        ('koparka', False), 
        ('suwmiarka', True),
        ('name', 'Wodzisław Paradygmat')
            ])

Jest to rzecz jasna bardzo uproszczony przykład, ale daje mniej-więcej pojęcie o co chodzi. Jeśli poszczególne wykonania nieco się od siebie różnią, to można dodać jakąś flagę, albo dodatkowe parametry.

W ten sposób pozbywamy się z kodu zbędnego tłuszczu, skutecznie zmniejszamy jego objętość, sprawiamy że jest przyjaźniejszy i czytelniejszy i NIESAMOWICIE ułatwiamy sobie unittesty - czytając funkcję widzimy co robi, zamiast zanudzać czytelnika kolejnymi identycznymi liniami kodu.

Brak komentarzy albo ich nadmiar

(albo komentarze w języku którego nie rozpoznaje nawet translator)

Tutaj będzie krótka piłka - czytając kod osoba trzecia powinna wiedzieć co wasze dzieło właściwie rzecz biorąc robi.

Jak wstawicie za dużo komentarzy, to będzie to wyglądało jak czytanka dla debila, jak nie wstawicie w ogóle, to człowiek będzie się czuł jakby czytał matrixa po niemiecku cyrylicą.

Ja to robię tak - zanim w ogóle napiszę kod, robię parę kluczowych komentarzy mówiących co będzie się dziać, rozpisuję logikę aplikacji, rozpisuję funkcje tam gdzie uznam to za stosowne - a później do tego piszę kod. W ten sposób widzimy co dany program robi.

Korzystajcie z docstringów w funkcji. Serio. Piszcie co przychodzi i co zwraca. Type hinting nic was nie kosztuje i do niczego nie zobowiązuje - a potrafi ułatwić życie osobie trzeciej.

PEP8

Tu też będzie krótko - przestrzegajcie zapisów świętego PEP8. Python to język który potrafi być piękny i prosty, ale jeśli nie stosujecie się do dość rygorystycznych zasad zapisu kodu to wygląda beznadziejnie PEP8 jest w sieci, wszystkie porządne edytory tekstu mają go wbudowanego - po prostu z niego korzystajcie, IDE\vim\notatnik SAM podkreśli ci błędy.

List comprehension

List comprehension w Pythonie jest darem od Bogów i coś, z czego powinniście korzystać jak najwięcej i jak najczęściej - praktycznie wszędzie gdzie się da. Upraszcza życie NIESAMOWICIE.

Switche

W pythonie nie ma switchy. Zamiast takowe robić, albo zagnieżdżać miliard ifów elifów wykorzystujemy dicty (mają bodajże log(n) w najbardziej pesymistycznym wypadku). Polecam także - kiedy się da! Łączyć to ze wspomnianym list comprehension.

Przykład z pracy (skrócony - miał ponad 60 warunków):

dict_list = []

for i in list_of_i:
    x = {'i': i }
    if i == 3:
        x['itemA'] = 25
    elif i == 4:
        x['itemB'] = 25
    elif i == 45:
        x['itemC'] = 56
    elif i == 42:
        x['itemY'] = 79
    else:
        x['foo'] = 21

    dict_list.append(x)

Zamiast tego lepiej i skuteczniej jest napisać:

switch_dict = {
    3: {'itemA': 25}, 
    4: {'itemB': 25}, 
    45: {'itemC': 56}, 
    42: {'itemY': 79}
}

dict_list = []

for i in list_of_i:
    dict_list.append({
        'i': i, 
        **switch_dict.get(i, ('foo', 21))
        })

To rozwiązanie, według mnie, jest o wiele prostsze, szybciej można dodawać kolejne argumenty, łatwiej zrobić ewentualny refactor.

Wyobraźcie sobie, że musicie przy danym switchu, w tym przypadku, dodać do dicta dwa albo trzy extra argumenty - switch_dict można szybko rozszerzyć o dodatkowe argumenty list comprehension i podkleić co trzeba. W przypadku ifów elifów jest to żmudna, ciężka robota - i mówię to z doświadczenia.

Functools & getattr

Korzystajcie z nich. Partial to błogosławieństwo i nie wiem czemu tyle ludzi się przed nim wzbrania.

W przypadku getattr krótka piłka - zamiast pisać 30 razy "weź foo.bar, foo.x, foo.y" piszemy

for i in ['bar', 'x', 'y']:
    i = getattr(foo, i)
    # do whatever you want with i

Dekoratory i context managery

Dekoratory są fajne. Poczytajcie o nich i je stosujcie. Context managery są niesamowicie przydatne i bardzo często upraszczają życie.

Pamiętajcie, że za pomocą dekoratorów można zrobić ładny i skuteczny error handler który da się podpiąć gdzie tylko checie - co jest dodatkowym argumentem za dzieleniem kodu na wiele małych funkcji.

Ten problem poruszę głębiej w artykule, który właśnie piszę - jak go skończę, to pojawi się tutaj stosowny link.

Copy and paste from Stack Overflow

Nie róbcie tego. Po prostu tego nie róbcie. Jeśli korzystacie z gotowców z neta, to miejscie przynajmniej tyle mózgu, żeby jakoś te nieszczęsne odpowiedzi zredagować. Do tego starajcie się weryfikować dane które zbieracie - często kopiowanie czegoś co po prostu "działa" kończy się w dłuższej perspektywie katastrofą.

Polecam też zaglądać do dokumentacji zanim wyszukacie pytanie - dokumentacja pythona jest całkiem niezła, dokumentacja takiego Django to najlepsza rzecz jaką w życiu widziałem - wyczerpująca, z świetnymi przykładami, do tego w necie mamy setki dodatkowych materiałów które dodatkowo ją uzupełniają.

Testy

Piszcie testy, jak najwięcej testów.

Testy to przykład wywołania waszej funkcji. Testy sprawdzają czy dany kawałek kodu działa według założeń. Po testach ktoś może zbadać jak działa nasz kod i co ma robić, jak się zachowywać. Testy pozwolą wam zweryfikować czy funkcja nagle nie wywala się po zmianie czegoś po drugiej stronie kodu. Nie będę tutaj się rozwodził jak testy powinny wyglądać - internet jest pełen materiałów na ten temat.

Outro

Czytelny kod to świetna sprawa. Czy warto pisać czytelny kod?

Pracownik który pisze czytelny kod, z testami automatycznymi, idealny żeby go rozwijać, dodawać nowe funkcjonalności bez rozwalenia wszystkiego, piękny do debuggowania to prawdziwy skarb. Skarb dla działu HR, który do niego podejdzie, poklepie go po plecach, uśmiechnie się, po czym go wyrzuci, bo jego kod ogarnie byle hindus zarabiający 1\4 tego, a nie ma już sensu go utrzymywać.

Jeśli piszecie nieczytelny kod, z komentarzami które wprowadzają więcej zamętu niż pomagają, to nie da się was wyrzucić. Ba! Stajecie się ważni - ludzie przychodzą do was, pytając co dana funkcja robi, nagle okazuje się, że bez was wszystko runie - bo wasze poplątane spaghetti robi za fundament zespołu.

Pisanie czytelnego kodu w niczym wam nie pomoże. bo właśnie w większości przypadków, skill nie jest ważny - liczy się tylko umiejętność uśmiechania się i gadania o bzdurach w kuchni.

W freelancerce nie jest lepiej - żaden klient nie spojrzy wam na ręce, bo oni po prostu nie potrafią kodzić i nie rozpoznają javy od javascripta (co w epoce webassembly może mieć tragiczne skutki). Liczy się efekt , byleby był i byleby szybko - a jakim kosztem i jak będzie wyglądał to już wasza sprawa.

I to właściwie tyle - trochę mi wstyd, że piszę o podstawach podstaw podstaw, ale bardzo wiele ludzi ich nie ogarnia. Może i podwyżka jest lepsza od chłosty, ale po co komuś dawać podwyżkę, skoro można zastąpić go botem kopiującym z Stack Overflow?

Sort:  

Thanks for your sharing

Nawiedził cię kumpel i dał ci mały prezent na zachętę! Oddaj głos na ten komentarz, a będę mógł głosować z większą mocą :)

Nawiedził cię Yavin i dał ci artykuł. Napisz coś wartościowego, to rzucę w ciebie Eurogąbkami i będziesz mógł głosować z większą mocą.

Xd Testuję bota. Ale jak nie zmienię komentarza, to chyba mnie zlinczują :D

Nazwy zmiennych/funkcji czy czegokolwiek po polsku wyglądają zawsze komicznie ;D

O matko, zapomniałem o tym wspomnieć. Najgorzej jak rozwijasz jakiś moduł i nie możesz zmienić nazw, bo musiałbyś zrobić gruby refactor w bardzo wielu miejscach - więc też piszesz nazwy zmiennych po polsku żeby zachować jednolitą konwencję...

Zastanawiam się co kieruje devami, którzy piszą kod po polsku na co dzień. Dostają takie wymaganie? Jakoś nie mogę sobie tego wyobrazić.

Pracowałem z takimi i mam wrażenie, że idą po prostu po linii najmniejszego oporu - tym bardziej że stosowali dziwną mieszankę jednego z drugim. Byleby odjebać jak najszybciej i się poopierdalać, a błędy krytyczne? Panie, kto by się tym przejmował!

Czasem wygląda to tak, że w firmie masz okresloną praktykę i niezbyt możesz od niej odejść, bo "spójność w zakresie całej spółki" itp. rzeczy. Wiem, bo tego doświadczyłem.

Zawsze chcialem zaczac uczyc sie Python'a.

Nazwałbym ten artykuł bardziej w stylu "kilka porad, by Twój python stał się lepszy, ale mniejsza już o to. Widzę parę spoko rad, które zapewne zastosuję w wolnej chwili.
Nie przepadam za pythonem, nie leży mi ta składnia, ale piszę skrypty do automatyzacji testów w tym języku, więc niestety warto by było popracować nad tym, by te moje skrypty były jak najczytelniejsze, jak najlepsze i jak najprostsze w obsłudze (i ewentualnym refactorze/rozbudowie), przyda się więc ta garść porad ;)

z2519231QJanPawelII28stycznia2005.jpgDzięki przyda mi się gdy na dobre wejdę w pythona daje okejke

Coin Marketplace

STEEM 0.26
TRX 0.11
JST 0.033
BTC 64359.90
ETH 3105.50
USDT 1.00
SBD 3.87