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