W tym dokumencie prostuję najczęściej spotykane uchybienia wobec dobrego stylu programowania w Pythonie 3.x
Jestem wdzięczny zarówno programistom, od których nauczyłem się poniższych zasad, jak i studentom, których uczyłem: poniżej streszczam te zasady, których nieznajomość najczęściej dostrzegałem w programach zaliczeniowych.
Przekazuję niniejszy dokument do domeny publicznej. Wolno go zwielokrotniać, zmieniać i rozpowszechniać, nawet w celach komercyjnych, bez pytania o zgodę.
Do czytelników, którym przydadzą się poniższe rady, mam osobistą prośbę: pomyślcie o jakiejś osobie, która okazała wam dobroć, i podziękujcie jej osobiście.
-
Repozytorium nie powinno zawierać zbędnych katalogów
__pycache__
,.idea
,venv
itp. ani zbędnych plików, np.desktop.ini
-
Wskazane jest za to dodanie pliku
.gitignore
o treści odpowiedniej dla projektów pisanych w Pythonie. Osoby, które nie dodały tego pliku przy zakładaniu repozytorium, tutaj znajdą odpowiednią treść. -
Nazwy bibliotek zewnętrznych niezbędnych do działania programu umieszczamy w pliku
requirements.txt
(zewnętrzne znaczy „nie te, które zainstalowaliśmy razem z Pythonem”; nie zakładamy plikurequirements.txt
, jeśli byłby pusty). Dzięki temu przez poleceniepip install -r requirements.txt
można je wszystkie naraz zainstalować. Tutaj jest przykładowa treść plikurequirements.txt
. Przypuszczam, że w większości Państwa projektów wystarczy wymienić nazwy bibliotek bez uściślania ich wersji, np.numpy pygame
Zachęcam do zapoznania się z treścią poradnika dla programistów Pythona w firmie Google i stosowania się do jego zasad. Większa część tego poradnika powtarza i objaśnia treść dokumentu PEP 8, który uznają wszystcy programiści Pythona.
Nie wymagam tylko podawania w docstringach funkcji
i metod sekcji Args:
, Returns:
i Raises:
ani w docstringach klas sekcji Attributes:
(punkty 3.8.3 i 3.8.4).
Nie obowiązują również zasady związane z Pythonem 2,
czyli nie należy pisać from __future__ import ...
,
dziedziczyć z object
ani korzystać z biblioteki six
.
Najczęściej ignorowane zasady powtarzam poniżej.
-
Pylint jest Państwa przyjacielem. Proszę go zainstalować, przepuszczać przez niego swój kod i poprawiać wskazane przez niego miejsca (punkt 2.1). Gdyby wszyscy z Państwa słuchali rad Pylinta, większość poniższych punktów byłaby niepotrzebna.
-
Proszę używać instrukcji
from ... import ...
tylko do importowania modułów z pakietów, a nie poszczególnych klas, funkcji czy stałych z modułów (punkt 2.2), bo długi wykaz potrzebnych identyfikatorówfrom grocery import spam, ham, eggs, cheese...
jest niewygodny, a krótki nie ma przewagi nadimport grocery
. A już zdecydowanie proszę nigdy nie pisaćfrom grocery import *
, bo zaśmiecanie przestrzeni nazw nieokreśloną liczbą nieokreślonych identyfikatorów dezorientuje czytelnika programu. Rozumiem, że ta zasada może budzić Państwa opór, ale my tu symulujemy pracę w firmie. W projektach tworzonych przez zespół lepsze są zbyt sztywne reguły od braku reguł. Poza tym standardy firm mniejszego kalibru niż Google bywają bardziej niedorzeczne. Proszę się przyzwyczajać. -
Można rozdzielać pustymi wierszami sąsiednie sekcje złożone z instrukcji
import
. Sekcje powinnny dotyczyć kolejno: biblioteki standardowej, bibliotek zewnętrznych i naszych własnych modułów. Nazwy plików i modułów w każdej sekcji porządkujemy leksykograficznie (punkt 3.13). -
Nie ma przymusu pisania czegokolwiek zaraz po nawiasie okrągłym otwierającym listę parametrów (punkt 3.4). Zamiast
bardzo_długa_nazwa = bardzo_długi_tasiemiec.zjedz(mielonka,
szynka,
jaja)
korzystniej wygląda
bardzo_długa_nazwa = bardzo_długi_tasiemiec.zjedz(
mielonka, szynka, jaja)
-
Wystrzegamy się zmiennych globalnych (punkt 2.5). Często da się ich pozbyć przez zmianę architektury kodu z proceduralnej na obiektową, dzięki czemu stają się atrybutami instancji klasy.
-
Globalne stałe są natomiast mile widziane, a nawet wymagane zamiast magicznych liczb, napisów i większych obiektów (zobacz artykuł Magic number w Wikipedii). Na przykład zamiast
7
definiujemy stałąBOARD_WIDTH = 7
, zamiast odróżniania kierunku przy użyciu zmiennej o wartości'left'
albo'right'
— definiujemy stałeLEFT, RIGHT = range(2)
(miłośnikom nowości polecamenum.Enum
), zamiast krotki(38, 139, 210)
— definiujemy stałąBACKGROUND_COLOR = (38, 139, 210)
. Nazwa stałej powinna odpowiadać jej zastosowaniu, a nie zawartości: gdyby ostatnia stała nosiła nazwęBLUE
, doszłoby do absurdu, gdybyśmy kiedyś chcieli zmienić tło na pomarańczowe. Stałe zwyczajowo zapisujemyWIELKIMI_LITERAMI_Z_PODKREŚLNIKAMI
(punkt 3.16.4). Nie od rzeczy będzie przypomnieć, że XXI wiek trwa już dość długo i w identyfikatorach można swobodnie korzystać z polskich liter, jeśli ktoś ma taką ochotę. -
Nazwy funkcji i zmiennych w
styluWielbłądzim
(camelCase
) są niepytoniczne. Powinny być wstylu_wężowym
(snake_case
) (punkt 3.16.4). -
Ponieważ z nazw plików
*.py
i katalogów z nimi powstają nazwy modułów i pakietów, do nich też lepiej pasujestyl_wężowy
(punkt 3.16). -
Warto pamiętać, że czytelnik programu nie musi znać tych samych skrótów, co my. Wniosek pierwszy: nie stosujemy skrótów, ani w identyfikatorach, ani w docstringach i komentarzach, chyba że są one zrozumiałe dla każdego pięciolatka (punkt 3.16). Przykład zrozumiałego i powszechnie używanego skrótu to
num_
zamiastnumber_of_
. Wniosek drugi: tym bardziej nie stosujemy jednoliterowych identyfikatorów (punkt 3.16.1). Pierwszy wyjątek toi
,j
,k
jako liczniki pętli, czyli zmienne całkowite generowane przez funkcjęrange()
. Drugi wyjątek to litery pasujące do typu danych w funkcjach anonimowych i wyrażeniach listowych, słownikowych, zbiorowych i generatorowych, czyli po naszemu lambda expressions, list/dict/set comprehensions i generator expressions. Na przykład literac
pasuje do pojedynczych znaków (character
), literas
do napisów (string
) itp. Jeśli trudno wyczuć, jaka litera pasuje do typu danych, można użyć literyx
, np.squares = [x**2 for x in collection]
-
Poniżej podaję wzorce i antywzorce instrukcji warunkowych (punkt 2.14.4 i 2.8.4).
# LEPIEJ # GORZEJ
#### Istnieje jeden egzemplarz stałej |None|. ####
if spam is None: if spam == None:
frobnicate() frobnicate()
#### ####
if spam is not None: if spam != None:
frobnicate() frobnicate()
#### ####
if spam: if spam is True:
frobnicate() frobnicate()
#### ####
if not spam: if spam is False:
frobnicate() frobnicate()
#### Listy i krotki. ####
if spam_sequence: if len(spam_sequence) > 0:
frobnicate() frobnicate()
#### ####
if spam_string: if spam_string != '':
frobnicate() frobnicate()
#### ####
if needle in haystack_dict: if needle in haystack_dict.keys():
frobnicate() frobnicate()
#### ####
if spam == ham == eggs: if spam == eggs and ham == eggs:
frobnicate() frobnicate()
#### ####
if 0 <= spam < 10: if spam >= 0 and spam < 10:
frobnicate() frobnicate()
#### Poniższy warunek działa też bez nawiasów. ####
if not (0 <= spam < 10): if spam < 0 or spam >= 10:
frobnicate() frobnicate()
-
Sklejanie napisów przez
+
jest nieładne. Ładne są za to f-stringi (punkt 3.10). -
Dodajemy docstringi do modułów, klas, metod i funkcji, chyba że są to metody lub funkcje jednowierszowe lub w inny sposób oczywiste. Proszę się zapoznać z zasadami pisania docstringów (punkt 3.8) i ich przestrzegać. W skrócie: w pierwszym wierszu docstringa wpisujemy zwięzłe zdanie opisujące działanie metody lub funkcji, np.
"""Zwraca indeks naciśniętego przycisku."""
(na końcu zdania stawiamy kropkę). Jeśli potrzebne jest dłuższe objaśnienie, dodajemy je wewnątrz tego samego napisu, oddzielone pustym wierszem. -
Wiersze programu nie powinny być zbyt długie. Jeśli trzeba je połamać na krótsze kawałki, dobrze wiedzieć, że kawałki otoczone dowolnym rodzajem nawiasów (
()
,[]
,{}
) same się ze sobą łączą i wyglądają lepiej niż ze znakiem obciachu\
na końcach (punkt 3.2).
-
Dobrym zwyczajem są przyrostki z jednostkami w nazwach wszystkiego, co można mierzyć na różne sposoby:
FRAME_INTERVAL_SECONDS
,document_age_days
,page_width_mm
,calculate_vat_pln()
. Ku przestrodze: artykuł Mars Climate Orbiter w Wikipedii. -
Zamiast
r'spam\ham.png'
lub'spam\\ham.png'
, co działa tylko pod Windows, lepiej pisać'spam/ham.png'
, co działa pod każdym systemem operacyjnym, w tym pod Windows. -
Zamiast sklejać dłuższe ścieżki do plików przez
+
, lepiej używaćos.path.join()
, bo wtedy nie trzeba pamiętać, które kawałki ścieżki kończą się na'/'
, a które nie. -
Wewnątrz
sqlite3.Cursor.execute()
itp. zmienne parametry wstawiamy do SQL-a tylko przez?
,?42
,:spam
,$spam
lub@spam
, a nigdy przez+
, f-stringi ani.format()
. O tej zasadzie w komiksie XKCD nr 327. Ku pamięci: w sposobach z pytajnikiem parametry po stronie Pythona muszą być w krotce lub liście, więc kiedy parametr jest jeden, piszemy(spam,)
lub[spam]
. -
Z drugiej strony w zapytaniach z dynamicznymi nazwami kolumn lub z
IN (?, ?,...)
o zmiennej liczbie pytajników nie da się obejść bez f-stringów lub.format()
. -
Polecam poniższe szablony czytania z bazy danych. O metodzie
.fetchall()
lepiej zapomnieć.
# O ile kolumn prosi SELECT, tyle elementów mają
# krotki generowane przez |cursor.execute()|.
# 1-elementowe krotki trzeba rozpakowywać swoiście.
spam_list = []
for (spam,) in cursor.execute( # Działa też spam, lub [spam].
"""
SELECT spam
FROM SpamTable
WHERE ham = ?
AND eggs = ?
""",
(ham, eggs)
):
spam_list.append(spam)
# Przy rozpakowywaniu dłuższych krotek nie ma niespodzianek.
for spam, ham in cursor.execute(
"""
SELECT spam, ham
FROM SomeTable
"""
):
frobnicate(spam, ham)
-
Żeby się dowiedzieć, jakie kolumny zwraca
SELECT *
, trzeba znaleźć kod tworzący tabelę. Dlatego w programach należy jawnie wymieniać kolumny poSELECT
. -
Python to nie C. Poniżej wzorce i antywzorce pętli.
# PYTONICZNIE # NIEPYTONICZNIE
#### ####
for spam in spam_list: for i in range(len(spam_list)):
frobnicate(spam) frobnicate(spam_list[i])
#### ####
for i, spam in enumerate(spam_list): i = 0
frobnicate(i, spam) for spam in spam_list:
frobnicate(i, spam)
i += 1
#### ####
for spam, ham in zip(spam_list, ham_list): for i in range(len(spam_list)):
frobnicate(spam, ham) frobnicate(spam_list[i], ham_list[i])
#### ####
COINS = [ COINS = [
('1 grosz', 0.01), ('1 grosz', 0.01),
('2 grosze', 0.02), ('2 grosze', 0.02),
..., ...,
] ]
for name, value_pln in COINS: for coin in COINS:
frobnicate(name, value_pln) frobnicate(coin[0], coin[1])
#### ####
ham = '\n'.join(spam_list) ham = ''
for i, spam in enumerate(spam_list):
if i:
ham += '\n'
ham += spam
#### ####
ham = '\n'.join( ham = ''
frobnicate(x) for x in spam_list) for i, spam in enumerate(spam_list):
if i:
ham += '\n'
ham += frobnicate(spam)
-
Python to nie Java, odsłona pierwsza. Nie tworzymy osobnych plików na małe klasy w stylu
PrzechowywaczMonet
tylko po to, żeby później pisaćimport PrzechowywaczMonet
iprzechowywacz_monet = PrzechowywaczMonet.PrzechowywaczMonet()
-
Python to nie Java, odsłona druga. Zamiast pobieraczy (getters) i ustawiaczy (setters) robiących tylko
return self._spam
iself._spam = spam
wystarczy nazwać ten atrybutself.spam
i bezpośrednio go odczytywać i zapisywać. Uwaga: wewnątrz metod danego obiektu możemy robić z jego atrybutami, co się nam żywnie podoba; jest też w porządku, gdy kod poza obiektem bezpośrednio odczytuje jego atrybuty; natomiast bezpośrednie gmeranie z zewnątrz przy wartościach atrybutów jest w złym guście — tylko w tym wypadku warto stosować ustawiacze. -
Jeśli potrzebny jest pobieracz, który robi coś więcej, pomoże nam dekorator
@property
:
@property
def total_spam(self):
"""Używać bez nawiasów: ham = self.total_spam"""
return sum(self._spam_list)
- Proszę odróżniać atrybuty klasy, które inicjalizujemy tak:
class Spam:
ham = 42
od atrybutów instancji, które inicjalizujemy w konstruktorze:
class Spam:
def __init__(self):
self.ham = 42
Atrybuty klas mają wartość początkową nadawaną tylko raz
i są wspólne dla wszystkich instancji klasy
(możemy się do nich odwoływać przez Spam.ham
lub
self.ham
). Atrybuty instancji są osobne w każdej instancji
(self.ham
).
Jeśli atrybut klasy jest stałą,
to wszystko jest w porządku. Natomiast jeśli atrybut
klasy zmienia wartość w trakcie działania programu,
to prosimy się o kłopoty. Nawet gdy przewidujemy
istnienie podczas działania programu tylko jednej
instancji klasy (np. Game
), to przy testowaniu
zupełnie normalne jest tworzenie jedna po drugiej
coraz nowszych instancji. Jeśli te instancje
będą zmieniać wartość współdzielonego atrybutu,
to może dojść do niepożądanych skutków.
Osoby ciekawe nowości zachęcam do zapoznania się
z dekoratorem
@dataclasses.dataclass
,
który jest powiązany z tym zagadnieniem.
- Statyczne nazwy dobre, dynamiczne nazwy złe. Dlatego zmienne w stylu
point = {'x': 42, 'y': 56}
wyglądają lepiej jako
collections.namedtuple
.
- Podobnie stałe w rodzaju
COLORS = {
'yellow': (181, 137, 0),
'orange': (203, 75, 22),
...,
}
zyskują po zmianie na
class Colors:
"""Paleta barw."""
# pylint: disable=too-few-public-methods
YELLOW = (181, 137, 0)
ORANGE = (203, 75, 22)
...
-
Importowanie modułu nigdy nie powinno mieć skutków ubocznych — takich jak wypisywanie tekstu, otwieranie okien, wczytywanie (zapisywanie?!) plików, wystrzeliwanie pocisków balistycznych itp. Są to zawsze skutki kodu lewitującego poza funkcjami. Taki kod wkładamy do funkcji, a te wywołujemy z funkcji
main()
-
Na wczytywane z dysku obrazki, fonty, dźwięki itp. proponuję założyć plik
assets.py
o poniższej treści. W funkcjimain()
należy wywołać funkcjępygame.init()
, a następnie metodęassets.Assets.load()
"""Zasoby potrzebne do gry.""" import pygame class Assets: """Przechowuje zasoby.""" # pylint: disable=too-few-public-methods @staticmethod def load(): """Wczytuje zasoby z dysku.""" Assets.SPAM_IMAGE = pygame.image.load('assets/spam.png') ... Assets.LARGE_FONT = pygame.font.Font('assets/Delicious-Roman.otf', 48) ...
Podobnie można zgrupować wczytywanie obrazków przez
tkinter.PhotoImage
-
Koniec głównego modułu programu powinien wyglądać jak poniżej. Oczywiście nie wszystkie sekcje funkcji
main()
muszą wystąpić w każdym programie.def main(): [inicjalizacja bibliotek zewnętrznych] [tworzenie obiektów odpowiednich klas] [wywołanie funkcji lub metody wprawiającej te obiekty w ruch] [sprzątanie] if __name__ == '__main__': main()
-
W plikach z testami wystarczy jeden docstring na początku (pozostałe docstringi podpadają pod zasadę „lub w inny sposób oczywistych” powyżej) i nie trzeba zamieniać magicznych liczb itp. na zdefiniowane stałe. Szablon pliku
numbers_test.py
do testowania zawartości fikcyjnego plikunumbers.py
zamieszczono poniżej. Pełną dokumentację modułuunittest
można znaleźć tutaj."""Testy modułu numbers.""" import unittest import numbers class ReaderTest(unittest.TestCase): def setUp(self): self.reader = numbers.Reader() def test_read_0(self): self.assertEqual(self.reader.read(0), 'zero') def test_read_7(self): self.assertEqual(self.reader.read(7), 'siedem') def test_read_20(self): self.assertEqual(self.reader.read(20), 'dwadzieścia') def test_read_42(self): self.assertEqual(self.reader.read(42), 'czterdzieści dwa') ... def test_read_negative(self): self.assertEqual(self.reader.read(-14), 'minus czternaście') def test_read_raises_on_invalid_input(self): with self.assertRaises(numbers.InvalidNumberError): self.reader.read('spam') class WriterTest(unittest.TestCase): ... if __name__ == '__main__': unittest.main()
-
Widok kolorów w stylu
(255, 0, 0)
przypomina mi czasy magnetofonów kasetowych i 16-kolorowych trybów graficznych. Wśród 16.777.216 barw można znaleźć ciekawsze. Ja lubię paletę Solarized; jest też mnogość innych palet.
-
Kopiuj-wklejoza (w najostrzejszych przypadkach mająca miejsce między różnymi plikami) nie przystoi prawdziwym programistom. Po to są moduły, dziedziczenie, metody, funkcje, pętle itp., żeby z nich korzystać. W łagodniejszych przypadkach, kiedy po wklejeniu trzeba coś pozmieniać, można stosować zasadę „do trzech razy sztuka”.
-
Lepszy jest program, który ma za dużo klas, niż program, który ma ich za mało. Oto przykład:
# LEPIEJ, mimo że kod jest dłuższy: # GORZEJ, chociaż krócej: # osobne klasy robią osobne rzeczy # pomieszanie z poplątaniem class DictList: class Frobnicator: def __init__(self): def __init__(self): self.n2x = [] self.n2x = [] self.x2n = {} self.x2n = {} [inicjalizacja innych pól] def add(self, x): if x not in self.x2n: def frobnicate(self, x): self.x2n[x] = len(self.n2x) if x not in self.x2n: self.n2x.append(x) self.x2n[x] = len(self.n2x) return self.x2n[x] self.n2x.append(x) n = self.x2n[x] [właściwa treść tej metody] class Frobnicator: def __init__(self): self.dictlist = DictList() [inicjalizacja innych pól] def frobnicate(self, x): n = self.dictlist.add(x) [dalsza treść tej metody]
-
Łatwiej zrozumieć sprawdzanie wyrażeń logicznych, gdy unikamy negacji: zarówno operatora
not
, jak wartości o zanegowanym sensie. Na przykład zamiast pisaćif not is_invalid_email(field):
można przerobić program i napisaćif is_valid_email(field):
-
Dziedziczenie niepotrzebnie komplikuje programy. Łatwiejsze i ogólniejsze jest składanie obiektów.
-
Dobre konstruktory poznajemy po tym, że tylko łączą w całość przekazane im inne, wcześniej skonstruowane obiekty. Dzięki takiemu „wstrzykiwaniu zależności” (dependency injection) znacznie łatwiej jest testować klasy.
-
Złe konstruktory poznajemy po:
- wywołaniach innych konstruktorów
(
super().__init__()
jest OK); - wywołaniach funkcji, które zmieniają globalny stan programu;
- instrukcjach warunkowych lub pętlach;
- inicjalizacji atrybutów bardziej skomplikowanej niż zwykłe przypisania;
- tworzeniu obiektu, który potem trzeba jeszcze doinicjować inną metodą;
- dziwnych obejściach, nie korzystających z metody
__init__()
po to, żeby móc zwracać kod błędu.
- wywołaniach innych konstruktorów
(
-
Złe metody poznajemy po:
- naruszaniu
reguły Demeter,
czyli przechodzeniu przez więcej niż jeden obiekt,
np.
self.pies.ogon.merdaj()
zamiast poprawnegoself.pies.merdaj()
, przy czym zmyłka polega na tym, że błąd daje o sobie znać tutaj, a leży w klasiePies
(gwoli jasności: w regule Demeter nie chodzi o liczenie kropek, tylko o nierozmawianie z obiektami oddalonymi odself
; takie odwołania jakconstants.SpamEnum.HAM
są koszerne).
- naruszaniu
reguły Demeter,
czyli przechodzeniu przez więcej niż jeden obiekt,
np.
-
Złe klasy poznajemy po:
- opisie zawierającym spójnik „i”;
- rozłącznych zbiorach metod, które działają na rozłącznych zbiorach atrybutów;
- atrybutach zmienianych z zewnątrz obiektu przez bezpośrednie przypisania do obiektu zamiast naturalnego korzystania z jego metod.
Ekspert to osoba, która popełniła wszystkie błędy, które można popełnić wewnątrz ograniczonej dziedziny — Niels Bohr