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ć.


, Etykiety: python, vim