piątek, 29 kwietnia 2011

Dlaczego ludzie formatują kod?

Przewrotnie pod tym tytułem ukryłem skłonność ludzi do „upiększania” tego, w jaki sposób kod źródłowy wygląda, zupełnie ignorując to, co akurat jest ważne.

Każdy język ma swoją ustaloną (bardziej lub mniej oficjalnie — ale ustaloną) konwencję, jak kod powinien wyglądać. Dla każdego programisty odpowiedź jest oczywista — ma być czytelny! Tylko, że nie dla każdego czytelność interpretowana przez drugiego programistę jest równie oczywista. Z tego też powodu powstały dokumenty w stylu Code Conventions for the Java Programming Language, jednak z jakiegoś powodu ludzie starają się omijać lub oszukiwać zasady z powodu własnej, bliżej nieokreślonej wygody (ok, nazwijmy to widzimisiem) lub tworzyć własne, poparte niekoniecznie rozsądnymi powodami.

W przypadku Pythona jest nie inaczej. Pomimo tego, że od dawna istnieje dokument PEP8 regulujący sprawę konwencji, wielu programistów ignoruje zalecenia w nim zawarte. Zadziwiające jest to, że zwykle są to programiści, którzy przychodzą ze świata Javy lub C++, a nie osoby dopiero uczące się programowania, lub mające nieduże doświadczenie. Najbardziej prawdopodobną przyczyną takiego stanu rzeczy, jaki mi się nasuwa na myśl, jest przenoszenie swoich przyzwyczajeń z poprzednich projektów na obecny (pomimo tego, że pisany jest w zupełnie innym języku). Najbardziej bolesne jest to, że owe przyzwyczajenia nie są zgodne z konwencją kodu dla danego języka!

Taby czy spacje?

No właśnie. Temat stary jak historia programowania. Zabawne, że większość dokumentów (w tym wyżej przytoczona konwencja dla Javy jak i wszystkie dotyczące C++, które znalazłem w sieci) zalecają stosowanie czterech spacji. Nie inaczej jest z plikami z kodem Pythonowym.

Oczywiście nikt nie broni stosowania znaków tabulacji, tylko że z tabami jest pewien drobny kłopot. Standardowy znak tabulacji od zarania dziejów ma długość ośmiu znaków. Ośmiu. Kropka. Niezbyt to oszczędne, ani wygodne, więc wiele edytorów i środowisk programistycznych radzi sobie z tym poprzez możliwość zdefiniowania długości znaku tabulacji. Tyle, że to ma miejsce wyłącznie po stronie danego edytora. Jeśli wyświetlić dany plik na ekran konsoli, długość znaku tabulacji w dalszym ciągu będzie… osiem.

Prosty przykład:

def bar(arg):
    if arg == 10:
        print "It's exactly 10!"
    else:
        print "It's not 10, it is %d" % arg

bar(2)
bar(10)

Wygląda w porządku ale…

gryf@mslug ~ $ python tt.py
   File "tt.py", line 4
    else:
       ^
SyntaxError: invalid syntax 

Oops. Przecież kod jest w porządku, WTF? Zobaczmy białe znaki w edytorze:

1 def bar(arg):
2     if arg == 10:
3         print "It's exactly 10!"
4 ▸―――else:
5         print "It's not 10, it is %d" % arg
6 
7 bar(2)
8 bar(10)

(Każdy ciąg znaków "▸―――" oznacza wcięcie przy pomocy znaków tabulacji)

Dlaczego tak się dzieje? Przecież wcięcia nadal są w porządku. Otóż nie. Tabulacja widoczna na zrzucie powyżej w edytorze reprezentowana jest przez 4 znaki, ale w rzeczywistości jest to 8 znaków!

gryf@mslug ~ $ cat tt.py
def bar(arg):
    if arg == 10:
        print "It's exactly 10!"
        else:
        print "It's not 10, it is %d" % arg

bar(2)
bar(10)

Nigdy nie należy mieszać znaków tabulacji i spacji, zwłaszcza w Pythonie, gdzie cała struktura kodu opiera się o wcięcia.

Uwaga końcowa. Większość doświadczonych programistów Pythona preferuje spacje. Są przewidywalne i zawsze dobrze wyglądają. Pozostali będą obstawać ideologicznie przy swoich standardach. Osobiście preferuję styl, jaki obowiązuje w obrębie danego języka (z drugiej strony niech ktoś spróbuje robić wcięcia spacjami w Makefile albo tabami w yaml…).

Formatowanie kodu

Najbardziej irytującą rzeczą, jaką można spotkać nie tylko w Pythonowym kodzie, zaraz po nieprawidłowym stosowaniu wcięć, jest zabawa w grafika ASCII. Na czym to polega? Już pokazuję.

 1 class Foo(object):
 2 ▸―――def __init__(self):
 3 ▸―――▸―――self.attribute = 10
 4 ▸―――▸―――self.result    = 12
 5 ▸―――▸―――self.myDict    = {'key1'▸―――▸―――: "val",
 6 ▸―――▸―――                  'another_key' : "val",
 7 ▸―――▸―――                  'another dict': {
 8 ▸―――▸―――                                    "key"   : "val",
 9 ▸―――▸―――                                    "key2"  : "val",
10 ▸―――▸―――▸―――▸―――▸―――▸―――  }
11 ▸―――▸―――▸―――▸―――▸―――▸――― }
12 
13 ▸―――▸―――self.doOneThing ( self.attribute , (self.result, self.doOther (self.myDict['another_key'])) )
14 
15 ▸―――# -------------------------------------------------------------------------------------------------------
16 
17 ▸―――def doOneThing(self, arg_1, arg_2):
18 ▸―――▸―――# ...
19 ▸―――▸―――return
20 
21 ▸―――# -------------------------------------------------------------------------------------------------------
22 
23 ▸―――def doOther(self, arg):
24 ▸―――▸―――# ...
25 ▸―――▸―――return

Konia z rzędem temu, kto wytłumaczy mi racjonalny powód robienia „szlaczków” w kodzie, które nie wnoszą niczego innego poza szumem. Gwoli informacji, żeby uzyskać efekt jak w liniach 6-9 (czyli dwa znaki tabulacji + spacje), trzeba zrobić to ręcznie. Tak, ponieważ przy konwencji używającej tabów, fragment kodu wyglądałby tak:

 5 ▸―――▸―――self.myDict = {'key1': "val",
 6 ▸―――▸―――▸―――▸――― 'another_key' : "val",
 7 ▸―――▸―――▸―――▸――― 'another dict': {
 8 ▸―――▸―――▸―――▸―――▸―――  "key": "val",
 9 ▸―――▸―――▸―――▸―――▸―――  "key2": "val",
10 ▸―――▸―――▸―――▸―――▸―――  }
11 ▸―――▸―――▸―――▸――― }

Tak automatycznie wyrównał kod VIm. Nie do końca jest to też dobre, bo ponownie, wcięcia reprezentują znaki tabulacji i spacje wyrównujące. Eclipse + PyDev radzi sobie nieco lepiej z tabami — po prostu nie dodaje spacji „wyrównujących”. Nie znam edytora, który by wspierał automatyczne formatowanie jak w pierwszym przykładzie. (EDIT: za wstawianie wyrównujących spacji w VImie odpowiedzialny jest alternatywny sposób wcięć dla pythona którego używam, a który jest zgodny z PEP8, gdzie z góry zakłada się użycie 4 spacji. Domyślna definicja wcięć dla Pythona, która rozprowadzana jest z VImem, ma zachowanie takie jak Eclpise z PyDev)

A przecież kod może wyglądać tak:

 1 class Foo(object):
 2     """
 3     Opis co ta klasa robi/do czego służy
 4     """
 5     def __init__(self):
 6         """
 7         Opis co metoda robi, jakie argumenty przyjmuje i co zwraca,
 8         czasem się przydaje, zwłaszcza gdy zagląda się do kodu po
 9         dłuższym czasie.
10         """
11         self.attribute = 10
12         self.result = 12
13         self.my_dict = {'key1': "val",
14                         'another_key' : "val",
15                         'another dict': {"key": "val",
16                                          "key2": "val"}}
17 
18         self.do_one_thing(self.attribute,
19                           (self.result,
20                            self.do_other(self.my_dict['another_key'])))
21 
22     def do_one_thing(self, arg_1, arg_2):
23         # ...
24         return
25 
26     def do_other(self, arg):
27         # ...
28         return

Czyż nie jest czytelniej? Czyż nie jest wkurzające przewijanie na boki? Dla mnie jest czytelne i wygodne, gdy nie muszę „jeździć kursorem” w te i wewte, lub odrywać się od pisania, by sięgnąć po mysz.

Zamiast epilogu

Programiści, którzy piszą kod w Pythonie są w tej fajnej sytuacji, że mają do dyspozycji narzędzia, które poinformują nie tylko o błędach związanych ze stylem (dzięki narzędziu linii komend pep8), ale również o błędach lub możliwych problematycznych miejscach w kodzie (dzięki pylintowi). Jasne — można ignorować zalecenia języka, nie używać lintów, ale prędzej czy później okaże się, że odbije się to czkawką na programistach (kolejnych i kolejnych), którzy będą utrzymywać naszą aplikację. Komendy pylint/pep8 można traktować jak ostrzeżenia kompilatora. Z czasem nie sposób je po prostu ignorować.

4 komentarze:

  1. Men, ja Struś, teoretycznie grafik z codzienności i kumpel Twój. Stwierdziłem dzisiaj, że nigdy nie zostałbym programistą. Ponieważ: przeczytałem to (prawym okiem) I stwierdzam że nic z tego nie kleje. Cholera, za mądre dla mnie. Jedź z koksem dalej i ciśnij pedał w podłogę.

    OdpowiedzUsuń na zawsze
  2. Ośmioznakowe tab stopy dla zwykłych plików tekstowych są cechą uniksową, którą Python zaadoptował, i na pewno nie są żadnym uniwersalnym standardem. (Przede wszystkim dlatego, że szerokość tabów zazwyczaj mierzy się w jednostkach fizycznych, a nie znakach). Fakt, że w różnych edytorach taby mogą mieć różną szerokość, jest potwierdzeniem tego faktu.

    Nie jest też prawdą, że nie da się mieszać znaków spacji i \t w takim sposób, żeby rezultat był wszędzie poprawny i czytelny. Problem w tym, że chyba nie ma edytora który by to robił dobrze, tj, biorać pod uwagę składnię języka. A jeśli już miałby to robić, to równie dobrze może obsługiwać elastic tabstops (http://en.wikipedia.org/wiki/Elastic_tabstop)

    OdpowiedzUsuń na zawsze
  3. Tabstopy wywodzą się z maszyn do pisania i koncept ten został zaadoptowany przez komputery, choć, jak twierdzi wikipedia, nie jest do końca jasne, dlaczego standardem długości taba stała się równowartość ośmiu znaków :) Python nie zaadoptował niczego, jedynie bazuje na ilości wcięć, które reprezentują znaki spacji lub tabulatora.

    W całym tym wpisie nie chodziło o to, czy jest możliwe mieszanie tabów ze spacjami, ale ogólnie o tendencje upiększania kodu nic nie wnoszącymi ornamentami złożonymi z grafiki ASCII czy wyrównywaniem kodu, zwłaszcza w sytuacji, gdy trzeba włożyć w to dodatkowy wysiłek w imię złudnej czytelności kodu, jednocześnie kompletnie ignorując to co jest faktycznie ważne.

    Osobiście uważam tego typu praktyki za totalną stratę czasu, a w przypadku Pythona za bluźnierstwo :) I mała dygresja a propos wcięć: kod Pythonowy bazuje na wcięciach, wobec czego właściwe i konsekwentne ich używanie jest rzeczą absolutnie fundamentalną. Wymieszanie spacji z tabami może się zemścić ślęczeniem nad absolutnie poprawnym kodem w poszukiwaniu błędu. Zalecenie więc jest takie, by używać tych czterech spacji i mieć święty spokój. Żałuję tylko, że nie zostało to z góry narzucone tak jak w Yaml, gdzie wcięcia nie mogą być tabami, albo w makefile'ach, gdzie wcięcia muszą być tabami.

    Zresztą, to nie tylko ja tak mam. Przeglądałem ostatnio interesującą pozycję o "pielęgnacji" kodu Clean Code: A Handbook of Agile Software Craftsmanship. Fragmenty można znaleźć na sieci, jeśli będziesz miał okazję, zachęcam do zapoznania się :)

    OdpowiedzUsuń na zawsze
  4. Czytałem tę książkę i w większości zgadzam się z nią, jak i z tym co napisałeś w poście. Wydaje mi się, że do takich poglądów trzeba dojrzeć: początkującym kod z dużą ilością takich ozdobników wydaje się bardziej czytelny.

    OdpowiedzUsuń na zawsze