Принципи на качествения код в Python#

Съдържание:

  • SOLID

  • Clean Code

  • PEP8

  • Pylint

  • Пример

Писането на код е само една част от това да си програмист. В живота на едно парче код, то по-често ще бъде прочитано, отколкото променяно. В нашият програмистки живот, по-често ще ни се налага да четем код, отколкото да пишем. Затова едно от ключовите ни умения като програмист е това да пишем качествен и четим код.

Съществуват някои широко-разпространени добри практики за писане на качествен код (а и архитектура). Ще разгледаме някои тях

SOLID#

SOLID е колекция от принципи, имащи за цел да направят нашия (обектно-ориентиран) код по-четим, по-лесен за поддържане и по-гъвква. SOLID е акроним на 5 принципа, които ще разгледаме по-долу - Single-reponsibility principle, Open-closed principle, Liskov substitution principle, Interface segregation principle и Dependency inversion principle.

Single-reponsibility#

Както името му подсказва, single-reponsibility принципа казва, че “един клас не трябва да има повече от една причина да се променя”. На по-прост език, това означава, че един клас трябва да има една отговорност/дейност.

Тази идея може да бъде разширена и към всички други програмни единици, не само класовете. Трябва да се стараем да пишем функции или класове, които правят едно нещо.

Нека разгледаме следната задача (source: Advent of Code 2022, day 18, pt1):

Получаваме списък от координати на кубчета в тримерното пространство. На изхода на програмата трябва да върнем броя на “видимите” страни на кубчетата - например, ако имаме едно кубче с координати (1, 1, 1), и второ кубче с координати (2, 1, 1), то броя на видимите страни е 10 (две от страните няма да се виждат, защото са долепени една до друга).

Как бихме написали кода за тази задача ?

def solution():
    line = input()
    lines = []
    while line != '':
        lines.append(line)
        line = input()

    cubes = []
    for line in lines:
        x, y, z = line.split(',')
        cubes.append((int(x), int(y), int(z)))

    sides = 0

    for source_cube in cubes:
        hidden_sides = 0
        for target_cube in cubes:
            diff_x = abs(source_cube[0] - target_cube[0])
            diff_y = abs(source_cube[1] - target_cube[1])
            diff_z = abs(source_cube[2] - target_cube[2])

            if (diff_x == 1 and diff_y == 0 and diff_z == 0) or (diff_x == 0 and diff_y == 1 and diff_z == 0) or (diff_x == 0 and diff_y == 0 and diff_z == 1):
                hidden_sides += 1

        sides += 6 - hidden_sides
    
    print(sides)

solution()
10

Очевидно горната функция solution прави много неща - какво правим, ако имаме бъг в решението ? Трябва да прегледаме целия код, което тук е само 30 реда, но представете си, че говорим за 300 реда. Също така, ако направим промяна, не е много сигурно, че няма да счупим някое друго парче код. Затова нека започнем прилагането на принципа за единстветана отговорност.

Първото нещо, което ни хрумва да направим, е да отделим входа, сметките и изхода в отделни функции.

def handle_input() -> list[tuple[int, int, int]]:
    line = input()
    lines = []
    while line != '':
        lines.append(line)
        line = input()

    cubes = []
    for line in lines:
        x, y, z = line.split(',')
        cubes.append((int(x), int(y), int(z)))
    
    return cubes

def solution(cubes: list[tuple[int, int, int]]):
    sides = 0

    for source_cube in cubes:
        hidden_sides = 0
        for target_cube in cubes:
            diff_x = abs(source_cube[0] - target_cube[0])
            diff_y = abs(source_cube[1] - target_cube[1])
            diff_z = abs(source_cube[2] - target_cube[2])

            if (diff_x == 1 and diff_y == 0 and diff_z == 0) or (diff_x == 0 and diff_y == 1 and diff_z == 0) or (diff_x == 0 and diff_y == 0 and diff_z == 1):
                hidden_sides += 1

        sides += 6 - hidden_sides
    
    return sides

def handle_output(sides: int):
    print(f'The answer is {sides}')

cubes = handle_input()
result = solution(cubes)
handle_output(result)
The answer is 10

Една идея по-добре. Но все още нашите функции handle_input и solution правят повече от едно неща.

Нека разгледаме в детайли handle_input - макар и кратка, тя прави две основни неща - приема входа от клавиатурата, и го конвертира от низ до наредена тройка от цели числа. Какво би станало, ако искаме вместо от клавиатурата, нашия вход да се чете от файл ? Или пък от графичен интерфейс ? Тази промяна не би трябвало да има общо с това как превръщаме низ към наредена тройка. Затова ще разделим handle_input на две по-малки функции.

def read_input() -> list[str]:
    line = input()
    lines = []
    while line != '':
        lines.append(line)
        line = input()

    return lines

def transform_input(lines: list[str]) -> list[tuple[int, int, int]]:

    # [f(x) for x in X]
    cubes = []
    for line in lines:
        x, y, z = line.split(',')
        cubes.append((int(x), int(y), int(z)))
    
    return cubes

def solution(cubes: list[tuple[int, int, int]]):
    sides = 0

    for source_cube in cubes:
        hidden_sides = 0
        for target_cube in cubes:
            diff_x = abs(source_cube[0] - target_cube[0])
            diff_y = abs(source_cube[1] - target_cube[1])
            diff_z = abs(source_cube[2] - target_cube[2])

            if (diff_x == 1 and diff_y == 0 and diff_z == 0) or (diff_x == 0 and diff_y == 1 and diff_z == 0) or (diff_x == 0 and diff_y == 0 and diff_z == 1):
                hidden_sides += 1

        sides += 6 - hidden_sides
    
    return sides

def handle_output(sides: int):
    print(f'The answer is {sides}')


lines = read_input()
cubes = transform_input(lines)
result = solution(cubes)
handle_output(result)

Така вече, ако се наложи промяна в начина по който четем входа, няма да се налага да променяме функцията, която държи и логиката за трансформирането на входа. Или ако пък се промени формата на входа, трябва да променим само transform_input, без да пипаме read_input.

Друг плюс от това разделение е, че функцията read_input вече не е обвързана по никакъв начин с конкретната задача - спокойно тя може да бъде преизползвана за решаването на други задачи, които изискват четене от клавиатурата.

Можем обаче да отидем една стъпка по-напред - нека отделим логиката за трансформиране на само един ред, отделно.

def read_input() -> list[str]:
    line = input()
    lines = []
    while line != '':
        lines.append(line)
        line = input()

    return lines

def line_to_tuple(line: str) -> tuple[int, int, int]:
    x, y, z = line.split(',')
    return int(x), int(y), int(z)

def transform_input(lines: list[str]) -> list[tuple[int, int, int]]:
    return [line_to_tuple(line) for line in lines]

def solution(cubes: list[tuple[int, int, int]]):
    sides = 0

    for source_cube in cubes:
        hidden_sides = 0
        for target_cube in cubes:
            diff_x = abs(source_cube[0] - target_cube[0])
            diff_y = abs(source_cube[1] - target_cube[1])
            diff_z = abs(source_cube[2] - target_cube[2])

            if (diff_x == 1 and diff_y == 0 and diff_z == 0) or (diff_x == 0 and diff_y == 1 and diff_z == 0) or (diff_x == 0 and diff_y == 0 and diff_z == 1):
                hidden_sides += 1

        sides += 6 - hidden_sides
    
    return sides

def handle_output(sides: int):
    print(f'The answer is {sides}')


lines = read_input()
cubes = transform_input(lines)
result = solution(cubes)
handle_output(result)
The answer is 10

Тук задаваме въпросите - имаме ли нужда да раздробяваме кода чак толкова и имаме ли нужда от функция, която е само един ред ?

Отговора на двата въпроса е един и същ - зависи. Всичко опира до конкретната задача, и конкретния стил на човека - някои казват, че ако функцията е един ред, няма нужда от нея. Но пък за сметка на това, transform_input е по-лесно четимо от [line_to_tuple(line) for line in lines].

Нека сега приложим същите идеи и върху solution.

def read_input() -> list[str]:
    line = input()
    lines = []
    while line != '':
        lines.append(line)
        line = input()

    return lines

def line_to_tuple(line: str) -> tuple[int, int, int]:
    x, y, z = line.split(',')
    return int(x), int(y), int(z)

def transform_input(lines: list[str]) -> list[tuple[int, int, int]]:
    return [line_to_tuple(line) for line in lines]

def is_side_hidden(first_cube: tuple[int, int, int], second_cube: tuple[int, int, int]) -> bool:
    # diffs = [abs(first_cube[direction] - second_cube[direction]) for direction in range(3)]

    # return sum(diffs) == 1

    diff_x = abs(first_cube[0] - second_cube[0])
    diff_y = abs(first_cube[1] - second_cube[1])
    diff_z = abs(first_cube[2] - second_cube[2])

    is_side_x_hidden = (diff_x == 1 and diff_y == 0 and diff_z == 0)
    is_side_y_hidden = (diff_x == 0 and diff_y == 1 and diff_z == 0)
    is_side_z_hidden = (diff_x == 0 and diff_y == 0 and diff_z == 1)

    return is_side_x_hidden or is_side_y_hidden or is_side_z_hidden

def count_visible_sides(cube: tuple[int, int, int], others: list[tuple[int, int, int]]) -> int:
    return 6 - sum(1 for other in others if is_side_hidden(cube, other))

def solution(cubes: list[tuple[int, int, int]]):
    visible_sides = [count_visible_sides(cube, cubes) for cube in cubes]
    return sum(visible_sides)

def handle_output(sides: int):
    print(f'The answer is {sides}')


lines = read_input()
cubes = transform_input(lines)
result = solution(cubes)
handle_output(result)
Point(x=2, y=3, z=4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 8
      5 p1 = Point(2, 3, 4)
      6 print(p1)
----> 8 def read_input() -> list[str]:
      9     line = input()
     10     lines = []

TypeError: 'type' object is not subscriptable

Спрямо първоначалното ни решение (което бе 29 реда), това вече е 42 реда - но е доста по-четимо, доста по-лесно за промяна и доста по-тестваемо. is_side_hidden може да бъде написана по-кратко, но по-четимия код е по-добър от по-краткия.

Open-closed#

Open-closed принципа гласи, че софтуерните компоненти трябва да са отворени за разширение, но затворени за модификация.

Нека разгледаме примера, където имаме клас Rectangle, и функция която пресмята сумата от лицата на всички правоъгълници в списък.

from typing import List

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

def sum_areas(rectangles: List[Rectangle]):
    return sum(rect.width * rect.height for rect in rectangles)

rectangles = [Rectangle(1, 2), Rectangle(3, 4)]
print(sum_areas(rectangles))
14

Ако добавим нов клас Square, трябва да променим функцията, за да може да работи с новия клас.

from typing import List, Union

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

class Square:
    def __init__(self, width):
        self.width = width


def sum_areas(items: Union[List[Rectangle], List[Square]]):
    if isinstance(items[0], Square):
        return sum(square.width * square.width for square in items)
    elif isinstance(items[0], Rectangle):
        return sum(rect.width * rect.height for rect in items)
    else:
        return 0

Правилният подход тук е да създадем нов клас Shape, който да има метод area, и да променим функцията да работи с обекти от тип Shape.

from abc import ABC
from typing import List, Union

class Shape(ABC):
    def area(self) -> int:
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, width):
        self.width = width

    def area(self):
        return self.width * self.width

def sum_areas(items: List[Shape]):
    return sum(item.area() for item in items)

Така вече, sum_areas е отворена за разширение (чрез добавяне на нови класове, които наследяват Shape), но затворена за модификация.

Liskov-substitution#

Ако потърсите в Wikipedia за Liskov substitution principle, може да видите следното:

“Нека \(\phi(x)\) е стойност, която е доказуема за обектите \(x\) от тип \(T\). Тогава \(\phi(y)\) трябва да е вярно за обекти \(y\) от тип \(S\), където \(S\) е подтип на \(T\).”

Преведено на български, това означава, че ако имаме два класа Base и Child, навсякъде където използваме инстанции на Base, трябва да можем да ги заменим с инстанции на Child, и програмата ни да работи.

Нека разгледаме пример, в който този принцип не е спазен.

class Rectangle:
    def __init__(self, x: int = 0, y: int = 0) -> None:
        self.__x = x
        self.__y = y
    
    @property
    def x(self) -> int:
        return self.__x
    
    @x.setter
    def x(self, x: int) -> None:
        self.__x = x

    @property
    def y(self) -> int:
        return self.__y
    
    @y.setter
    def y(self, y: int) -> None:
        self.__y = y

    def area(self) -> int:
        return self.__x * self.__y

class Square(Rectangle):
    def __init__(self, x: int) -> None:
        super().__init__(x, x)

rectangle = Square()
rectangle.x = 2
rectangle.y = 3
print(f'Rectangle area: {rectangle.area()}')
Rectangle area: 6

Ако в този код заместим Rectangle с Square, програмата няма да работи по същия начин. Това е пример за нарушаване на LSP. Методите на Rectangle са по-общи от методите на Square, и не всички обекти от тип Square са обекти от тип Rectangle.

class Base:
    def foo(self, a):
        pass

class Child(Base):
    def foo(self, a, b):
        pass

Interface segregation#

Interface segregration принципа гласи “много специфични интерфейси са по-добри от един общ интерфейс”. Това означава, че ако имаме два класа, които имат общ интерфейс, но само един от класовете го използва, то е по-добре да има два интерфейса, по един за всеки клас.

Това означава, че ако имаме даден интерфейс, който има 10 метода, но само 2 от тях се използват от класовете, които го имплементират, то е по-добре да има два интерфейса, по един за всеки клас, и да имплементирате само тези 2 метода.

Въпреки, че в Python няма интерфейси, този принцип е валиден. Нека разгледаме пример, в който този принцип не е спазен.

from abc import ABC

class Camera(ABC):
    def __init__(self, name: str, resolution: str) -> None:
        self.__name = name
        self.__resolution = resolution
    
    @property
    def name(self) -> str:
        return self.__name
    
    @name.setter
    def name(self, name: str) -> None:
        self.__name = name

    @property
    def resolution(self) -> str:
        return self.__resolution
    
    @resolution.setter
    def resolution(self, resolution: str) -> None:
        self.__resolution = resolution

    def take_photo(self) -> None:
        pass
    
    def take_video(self) -> None:
        pass

Тук имаме два метода - take_photo и take_video - които биха се използвали от бъдещи Camera и VideoCamera класове. Това е пример за нарушаване на ISP, защото Camera не използва take_video, и VideoCamera не използва take_photo. Това означава, че Camera и VideoCamera не трябва да имплементират CameraInterface, а трябва да имат по един интерфейс, който да има само един метод - take_photo или take_video. Допълнително, можем да направим трети интерфейс - Device, който да държи общите данни и методи за тези устройства.

from abc import ABC

class Device(ABC):
    def __init__(self, name: str, resolution: str) -> None:
        self.__name = name
        self.__resolution = resolution
    
    @property
    def name(self) -> str:
        return self.__name
    
    @name.setter
    def name(self, name: str) -> None:
        self.__name = name

    @property
    def resolution(self) -> str:
        return self.__resolution
    
    @resolution.setter
    def resolution(self, resolution: str) -> None:
        self.__resolution = resolution

class Photoable(ABC):
    def take_photo(self) -> None:
        pass

class Videoable(ABC):
    def take_video(self) -> None:
        pass

Dependency inversion#

Принципът за инверсия на зависимостите казва, че модулите от високо ниво не трябва да зависят от модули от ниско ниво, а обратното. Интерфейсите трябва да зависят от конкретните имплементации, а не обратното.

Dependency inversion

Нека разгледаме следния пример: Имаме три класа - Developer, Tester и ProjectcManager. Developer и Tester имплементират work метод, а ProjectManager управлява тези инстанции.

class Developer():
    def work():
        pass

class Tester():
    def work():
        pass

class ProjectManager():
    def __init__(self) -> None:
        self.employees = []

    def add_developer(self, developer: Developer):
        self.employees.append(developer)
    
    def add_tester(self, tester: Tester):
        self.employees.append(tester)

Тук ProjectManager зависи от Developer и Tester, което е нарушение на DIP. Това означава, че ако искаме да добавим нов клас, който да имплементира work метода, но не е Developer или Tester, то ProjectManager няма да може да го управлява. Това е проблем, защото ProjectManager не трябва да знае какви класове има в системата, а само да управлява тези, които имплементират work метода.

Решението е да създадем интерфейс Worker, който да има само work метода. Developer и Tester ще имплементират Worker, а ProjectManager ще зависи от Worker.

from abc import ABC

class Worker(ABC):
    def work(self) -> None:
        pass

class Developer(Worker):
    def work(self) -> None:
        print('Developer is working')

class Tester(Worker):
    def work(self) -> None:
        print('Tester is working')

class EmployeeService(abc.ABC):
    @abstractmethod
    def work(self, worker: Worker):
        ...

class EmployeeServiceImpl(EmployeeService):\
    ...

class MockEmployeeService(EmployeeService):
    ...

class ProjectManager():
    def __init__(self, service: EmployeeService) -> None:
        self.employees = []
        self.employee_service = service

    def add_worker(self, worker: Worker):
        self.employees.append(worker)

    def manage(self) -> None:
        for employee in self.employees:
            employee_service.work(employee)

Clean code#

Освен SOLID принципите, съществуват и някои други правила, които спомагат за това нашия код да е четим и лесен за поддържане.

Meaningful Names#

Както споменахме в началото, в работна среда по-често ще ни се налага да четем код, отколкото да пишем. Затова доброто именуване на всички единици от кода е от изключителна важност.

Ще разгледаме няколко примера за добро и лошо именуване на променливи, функции и класове.

class Point:
    def __init__(self, x: int = 0, y: int = 0) -> None:
        self.__x = x
        self.__y = y
    
    @property
    def x(self) -> int:
        return self.__x
    
    @x.setter
    def x(self, x: int) -> None:
        self.__x = x
    
    @property
    def y(self) -> int:
        return self.__y
    
    @y.setter
    def y(self, y: int) -> None:
        self.__y = y
    
    def dist(self, p2: 'Point') -> float:
        return ((self.__x - p2.x) ** 2 + (self.__y - p2.y) ** 2) ** 0.5

Тук основния заподозрян е метода dist. Първото нещо, което можем да направим, е да променим името на distance_to_point. Второто нещо, което можем да променим, е името на аргумент - вместо p2, може да се казва other или target.

Името на променлива/функция/клас трябва да е максимално информативно и минимално кратко. В него не трябва да се съдържат някакви съкращения, освен ако не са много популярни. Например, id е много популярно съкращение, което се използва във всички езици, но id не е много информативно. Вместо това, може да се използва identifier.

При булевите променливи и функциите които връщат булева стойност, трябва да започват с is, has, can, should, will или did. Другите функции трябва да бъдат именувани като глаголи, а класовете се именуват като съществителни.

Имената трябва да могат да бъдат лесно четими, но и лесно произнасяеми - например string_compare е по-добро име от strcmp.

Functions#

Някои допълнителни насоки за писането на функции:

  • Не трябва да има повече от 3 аргумента на функция. Ако има повече, трябва да се използва обект, който да съдържа всички аргументи.

  • Функцията трябва да прави само едно нещо. Ако прави повече, трябва да се раздели на няколко функции.

  • Функцията трябва да има единственно предназначение. Ако може да се използва за различни неща, трябва да се раздели на няколко функции.

  • Избягвайте страничните ефекти във функциите

  • Функциите трябват да имат висок cohesion, но нисък coupling

    • Cohesion - колко силно свързани са нещата, които правят функцията

    • Coupling - колко силно свързани са функциите между си

Comments#

Коментарите трябва да са информативни и да се използват за обясняване на нещата, които не са ясни от кода. Те не трябва да повтарят кода, а да го допълват. Използвайте коментари, за да обясните неясните неща от кода - някой regex, някакъв алгоритъм, някакъв неочакван резултат и т.н. Може да се добави примерен вход и изход към функцията.

Силно препоръчително е използването на docstrings. В тях описваме на високо ниво какво прави функцията, кавки аргументи приема и какво връща.

def bfs(graph: List[List[int]], start=Point(0, 0), target=Point(0, 0)) -> int:
    """
    Performs a breadth-first search on the given graph, starting at the given start position.
    
    :param graph: The graph to search.
    :param start: The start position.
    :param target: The target position.

    :return: The shortest path length from the start position to the target position.
    """
    queue = [(start, 0)]
    map_bounds = (len(graph), len(graph[0]))
    distances = {}

    visited = set()
    shortest_path = 1000000

    while len(queue) > 0:
        position, path = queue.pop(0)

        if not is_position_inside(position, map_bounds) or position in visited:
            continue

        if position == target:
            shortest_path = min(shortest_path, path)

        visited.add(position)

        for offset_x, offset_y in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            next_position = Point(position.x + offset_x, position.y + offset_y)

            if not is_position_inside(next_position, map_bounds):
                continue

            if heights[next_position.x][next_position.y] - heights[position.x][position.y] <= 1:
                queue.append((next_position, path + 1))

    return shortest_path

При спазването на дадени стандарти, можем лесно да генерираме документация за нашия код. Например, в Python можем да използваме Sphinx, който генерира HTML документация от docstrings.

PEP8#

PEP8 е стандарт за писане на Python код. Той съдържа правила за именуване на променливи, функции, класове, както и правила за писане на код. Той е създаден от Python Software Foundation и е съвместим с Python 2 и 3.

Стандарта сам по себе си е доста голям, затова ще се спрем само на части от него.

Подредба на кода#

Индентацията трябва да е 4 интервала. Според PEP8, табулациите трябва да се използват само ако се пише код във файлове, които са съвместими със табулации. Във всички останали случаи трябва да се използват интервали. Максималният размера на реда трябва да е до 79 символа, докато коментарите и docstring-овете трябва да са ограничени до 72 знака.

При пренасяне на код на нов ред, пренесеният ред трябва да е подравнен спрямо отварящи delimiter, или с допълнието на 4 интервала спрямо предишния ред.

# Correct:

# Aligned with opening delimiter.
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# Add 4 spaces (an extra level of indentation) to distinguish arguments from the rest.
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# Hanging indents should add a level.
foo = long_function_name(
    var_one, var_two,
    var_three, var_four)

# Wrong:

# Arguments on first line forbidden when not using vertical alignment.
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Further indentation required as indentation is not distinguishable.
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

В PEP8 също пише, че новите редове трябва да са преди бинарните оператори:

# Wrong:
# operators sit far away from their operands
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

# Correct:
# easy to match operators with operands
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

Относно празните редове - функциите на високо ниво и класовете трябва да бъдат разделени с два празни реда, а функциите на ниско ниво и методите на класовете - с един празен ред.

Import-ите трябва да бъдат на отделни редове и да са подредени в следния ред:

  1. Стандартните модули от Python

  2. Външни модули

  3. Модули от текущия проект

Между отделните групи може (а и е хубаво) да има празен ред.

# Correct:
import os
import sys

# Wrong:
import sys, os

Кавички#

В Python няма разлика между единичните и двойните кавички - PEP8 не препоръчва използването на едните пред другите. Но важно е да се използват еднакви кавички в рамките на един файл/проект.

Празни места#

Избягвайте използванто на множество празни места в кода.

# Correct:
spam(ham[1], {eggs: 2})

# Wrong:
spam( ham[ 1 ], { eggs: 2 } )
# Correct:
foo = (0,)

# Wrong:
bar = (0, )
# Correct:
if x == 4: print(x, y); x, y = y, x

# Wrong:
if x == 4 : print(x , y) ; x , y = y , x
# Correct:
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]

# Wrong:
ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : upper]
ham[ : upper]
# Correct:
spam(1)

# Wrong:
spam (1)
# Correct:
dct['key'] = lst[index]

# Wrong:
dct ['key'] = lst [index]
# Correct:
x = 1
y = 2
long_variable = 3

# Wrong:
x             = 1
y             = 2
long_variable = 3

Именуване, част 2#

  • Класовете в Python следва да се именуват спрямо CapWords (или PascalCase) конвенцията.

  • Имената на променливите и функциите следва да се именуват спрямо snake_case конвенцията.

Pylint#

Pylint е инструмент, който проверява дали написания от нас код съотвества на PEP8 стандарта. Можем да го инсталираме чрез pip install pylint. Стартираме го чрез pylint.

Пример#

Live coding demo, idea is WIP.