Руководство по кодированию для всех языков.

Если вы программист Python, то вы уже знаете, что мы немного «хиппи», когда дело доходит до написания кода. Мы не просто заботимся о том, чтобы наши программы работали; мы также заботимся о том, чтобы сделать их красивыми. И вот тут-то и появляется «Дзен Python».

Это набор руководящих принципов программирования на Python, в основе которых лежит простота, удобочитаемость и элегантность.

Вам не нужно быть гуру Python, чтобы оценить эти принципы. На самом деле это общие понятия, которые можно применить к любому языку программирования. Тим Питерс, человек, стоящий за «Дзен Python», создал эти принципы как способ продвижения хороших методов программирования и облегчения нашей жизни как программистов. Эти принципы были очень полезны для меня.

1. Красивое лучше, чем некрасивое

Предположим, вам нужно написать программу для вычисления площади круга. Уродливое решение может выглядеть так:

r=int(input("Enter radius: "))
a=3.14*r*r
print(a)

Более красивое решение может выглядеть так:

import math

radius = float(input("Enter the radius of the circle: "))

area = math.pi * radius ** 2

print(f"The area of the circle is {area}.")

Последнее решение считается более красивым, потому что оно соответствует лучшим практикам, таким как использование осмысленных имен переменных, импорт модулей вместо жестко заданных значений и использование правильных типов данных. Его также легче читать и поддерживать, что в целом делает его лучшим выбором.

2. Явное лучше неявного

Допустим, вы пишете функцию, которая принимает список строк и возвращает список этих строк, в котором удалены все пробельные символы. Одним из способов сделать это может быть использование строкового метода replace():

def remove_whitespace(strings):
    cleaned_strings = []

    for string in strings:
        cleaned_strings.append(string.replace(' ', ''))

    return cleaned_strings

Этот код отлично работает, если входные строки содержат только пробелы в качестве пробельных символов. Однако что, если входные строки содержат другие типы пробельных символов, например символы табуляции или новой строки? Метод replace() не удалит эти символы, что может привести к неожиданному поведению.

Более явным подходом было бы использование константы whitespace модуля string, которая содержит все пробельные символы ASCII, и явное удаление каждого из этих символов:

import string

def remove_whitespace(strings):
    cleaned_strings = []

    for string in strings:
        cleaned_string = ''

        for char in string:
            if char not in string.whitespace:
                cleaned_string += char

        cleaned_strings.append(cleaned_string)
    return cleaned_strings

Этот код удаляет все пробельные символы из входной строки, проверяя каждый символ и добавляя его в новую строку, только если это не пробельный символ. Будучи явным и проверяя все возможные пробельные символы, мы можем гарантировать, что наша функция работает правильно для любого типа пробельных символов.

3. Простое лучше сложного

Предположим, вы хотите написать функцию, которая принимает строку и возвращает список всех уникальных слов в строке.

Комплексное решение может выглядеть так:

def unique_words(sentence):
    words = sentence.split()
    unique_words = []
    for word in words:
        if word not in unique_words:
            unique_words.append(word)
    return unique_words

Более простое решение может выглядеть так:

def unique_words(sentence):
    words = sentence.split()
    return list(set(words))

Последнее решение считается более простым, поскольку в нем используется встроенная функция set, оптимизированная для устранения дубликатов. Он также короче и проще для понимания, что в целом делает его лучшим выбором.

4. Сложное лучше, чем сложное

Вам нужно написать функцию, которая принимает список целых чисел и возвращает новый список, содержащий только четные числа из исходного списка.

Вот один из способов, которым вы можете написать эту функцию, используя цикл for и оператор if:

def get_even_numbers(numbers: list[int]):
    even_numbers = []
    for number in numbers:
        if number % 2 == 0:
            even_numbers.append(number)
    return even_numbers

Этот код работает нормально, но он немного сложнее, чем должен быть. Мы используем цикл for для перебора каждого числа в списке ввода, а затем используем оператор if для проверки четности каждого числа. Если это так, мы добавляем его в новый список четных чисел.

Более простым и элегантным решением было бы использование понимания списка:

def get_even_numbers(numbers: list[int]):
    return [number for number in numbers if number % 2 == 0]

Этот код выполняет то же самое, что и предыдущий пример, но он намного короче и легче читается. Мы используем генератор списка для создания нового списка чисел, соответствующих нашим критериям (то есть четных чисел), и делаем все это в одной строке кода.

5. Flat лучше, чем вложенный

Предположим, у вас есть список списков, где каждый внутренний список содержит имя человека и его возраст. Вы хотите создать новый словарь, в котором ключами являются имена, а значениями являются возрасты. Один из способов сделать это — использовать вложенные циклы:

people = [["Alice", 25], ["Bob", 30], ["Charlie", 35]]

ages = {}
for person in people:
    name = person[0]
    age = person[1]
    ages[name] = age

print(ages)  # {'Alice': 25, 'Bob': 30, 'Charlie': 35}

Хотя эта реализация работает правильно, она вложенная, с двумя уровнями отступов. Более питонический способ сделать то же самое — использовать понимание списка и функцию zip() для создания пар имен и возрастов:

people = [["Alice", 25], ["Bob", 30], ["Charlie", 35]]

ages = {name: age for name, age in zip(*people)}

print(ages)  # {'Alice': 25, 'Bob': 30, 'Charlie': 35}

Эта реализация более плоская и более удобочитаемая, чем предыдущая реализация, с одним уровнем отступа. Он также использует более лаконичный и выразительный синтаксис, что упрощает его понимание и поддержку.

6. Разреженный лучше, чем густой

Предположим, у вас есть список строк, и вы хотите создать новый список, содержащий только строки, начинающиеся с гласной. Один из способов сделать это — использовать понимание списка с условным выражением:

words = ["apple", "banana", "cat", "dog", "elephant"]
vowel_words = [word for word in words if word[0] in "aeiou"]
print(vowel_words)  # ["apple", "elephant"]

Хотя эта реализация работает правильно, она плотная, с одной строкой кода, которая включает в себя понимание списка и условное выражение. Это может затруднить чтение и понимание кода, особенно для начинающих программистов Python.

Более простой способ сделать то же самое — использовать цикл for с оператором if:

words = ["apple", "banana", "cat", "dog", "elephant"]
vowel_words = []
for word in words:
    if word[0] in "aeiou":
        vowel_words.append(word)
print(vowel_words)  # ["apple", "elephant"]

Эта реализация более явная и легкая для чтения, с большим количеством пробелов и отдельными строками для цикла и условного оператора. Он также использует более традиционный подход, который некоторым разработчикам может быть проще понять и поддерживать.

7. Удобочитаемость имеет значение

Предположим, вы работаете над групповым проектом, который включает обработку данных из CSV-файла, и вам нужно написать функцию, которая читает файл, извлекает определенные поля и возвращает данные в виде списка словарей. Вот начальная реализация:

def read_csv(filename):
    data = []
    with open(filename, "r") as f:
        headers = f.readline().strip().split(",")
        for line in f:
            fields = line.strip().split(",")
            record = {headers[i]: fields[i] for i in range(len(headers))}
            data.append(record)
    return data

Хотя эта реализация работает правильно, другим может быть сложно понять, что происходит, без тщательного изучения кода. Чтобы сделать код более читабельным, вы можете добавить некоторые комментарии, использовать осмысленные имена переменных и реорганизовать его следующим образом:

def read_csv(filename):
    """
    Reads a CSV file and returns the data as a list of dictionaries.

    :param filename: The name of the CSV file to read.
    :return: A list of dictionaries representing the data.
    """
    data = []
    with open(filename, "r") as file:
        # Read the header row and split the field names.
        header_line = file.readline().strip()
        headers = header_line.split(",")

        # Read each subsequent line and convert to a dictionary.
        for line in file:
            field_values = line.strip().split(",")
            record = {}
            for i in range(len(headers)):
                field_name = headers[i]
                field_value = field_values[i]
                record[field_name] = field_value
            data.append(record)

    return data

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

8. Особые случаи не настолько особенные, чтобы нарушать правила

Предположим, вы хотите написать функцию, которая возвращает длину списка или строки независимо от типа ввода.

Решение, нарушающее правила, может выглядеть так:

def length(input):
    if isinstance(input, list):
        return len(input)
    elif isinstance(input, str):
        return len(input)
    else:
        raise ValueError("Input must be a list or string")

Решение, которое следует правилам, может выглядеть так:

def length(input):
    return len(input)

Встроенная функция len() работает как со списками, так и со строками, поэтому нет необходимости писать пользовательскую функцию, которая обрабатывает их по-разному. Это делает код более простым, согласованным и легким в обслуживании.

9. Хотя практичность важнее чистоты

Предположим, вы работаете над скриптом Python, которому необходимо загрузить большой файл с удаленного сервера. Вы можете использовать модуль urllib для загрузки файла следующим образом:

import urllib.request

url = "https://example.com/large_file.zip"
filename = "large_file.zip"

try:
    urllib.request.urlretrieve(url, filename)
    print("Downloaded successfully!")

except urllib.error.URLError as e:
    print(f"Error downloading file: {e}")

Эта реализация проста и понятна, но она может оказаться непрактичной для больших файлов, поскольку она загружает весь файл в память перед его сохранением на диск. Для очень больших файлов это может привести к тому, что сценарию не хватит памяти или его выполнение займет очень много времени.

Чтобы решить эту проблему, вы можете вместо этого использовать модуль requests, который позволяет загружать файл фрагментами и записывать их на диск по мере их получения. Вот как вы можете изменить реализацию:

import requests

url = "https://example.com/large_file.zip"
filename = "large_file.zip"
chunk_size = 1024 * 1024

try:
    with requests.get(url, stream=True) as r:
        r.raise_for_status()

        with open(filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size):
                if chunk:
                    f.write(chunk)

    print("Downloaded successfully!")

except requests.exceptions.RequestException as e:
    print(f"Error downloading file: {e}")

Эта реализация использует функцию requests.get для загрузки файла фрагментами и записывает каждый фрагмент на диск по мере его получения. Это уменьшает использование памяти сценарием и позволяет более эффективно обрабатывать очень большие файлы.

Однако эта реализация уже не является «чистой», поскольку вместо встроенного модуля urllib в ней используется сторонняя библиотека. Тем не менее, он более практичен и полезен в реальных сценариях, поскольку решает проблему, которая может повлиять на производительность и масштабируемость скрипта.

10-11. Ошибки никогда не должны проходить бесшумно. Если явно не заглушить.

Предположим, вы читаете файл, содержащий данные в определенном формате, и хотите написать функцию, которая анализирует данные и возвращает список словарей.

Решение, которое молча передает ошибки, может выглядеть так:

def parse_data(filename):
    data = []
    with open(filename) as f:
        for line in f:
            fields = line.strip().split(",")
            data.append({
                "field1": fields[0],
                "field2": fields[1],
                "field3": fields[2],
            })
    return data

Решение, которое правильно обрабатывает ошибки, может выглядеть так:

def parse_data(filename):
    data = []
    with open(filename) as f:
        for i, line in enumerate(f, start=1):
            fields = line.strip().split(",")

            # 👇
            if len(fields) != 3:
                raise ValueError(f"Line {i} has incorrect number of fields")
            
            data.append({
                "field1": fields[0],
                "field2": fields[1],
                "field3": fields[2],
            })
    return data

Последнее решение считается лучшим, потому что оно вызывает ошибку, когда встречает строку с неправильным количеством полей. Это делает код более надежным и менее склонным к получению неожиданных результатов, поскольку он явно предупреждает вызывающую функцию, когда возникает проблема с данными. Выдавая ошибку, он также предоставляет четкую и полезную информацию о том, что пошло не так, что упрощает отладку и устранение любых возникающих проблем.

12. Перед лицом двусмысленности откажитесь от соблазна угадать.

Предположим, вы пишете функцию, которая принимает число в качестве входных данных и возвращает его квадратный корень.

Решение, делающее неоднозначное предположение, может выглядеть так:

def square_root(number):
    return number ** 0.5

Решение, которое отказывается угадывать, может выглядеть так:

import math

def square_root(number):
    if number < 0:
        raise ValueError("Input must be non-negative")
    return math.sqrt(number)

Последнее решение считается лучшим, потому что оно вызывает ошибку, когда ввод отрицательный, вместо того, чтобы сделать неоднозначное предположение о том, как с этим справиться. Это делает код более надежным и менее склонным к получению неожиданных результатов, поскольку он явно предупреждает вызывающую функцию, когда возникает проблема с входными данными. Выдавая ошибку, он также предоставляет четкую и полезную информацию о том, что пошло не так, что упрощает отладку и устранение любых возникающих проблем.

13. Должен быть один — и желательно только один — очевидный способ сделать это

Предположим, вы хотите преобразовать строку в верхний регистр. В Python есть несколько способов сделать это, но метод upper считается самым простым и очевидным. Вот пример:

word = "hello"
uppercase_word = word.upper()
print(uppercase_word)

Хотя существуют и другие способы преобразования строки в верхний регистр, такие как использование метода str.format или метода str.translate, метод upper является наиболее очевидным способом, поскольку он представляет собой простой вызов метода и не требует дополнительного кода.

14. Хотя поначалу это может быть неочевидно, если только вы не голландец.

Предположим, вы хотите подсчитать количество вхождений каждого символа в строке. Для этого в Python есть встроенный метод, но он может быть не очевиден для всех пользователей. Вот пример:

word = "hello"
char_count = {}
for char in word:
    if char in char_count:
        char_count[char] += 1
    else:
        char_count[char] = 1
print(char_count)

# Output:
# {'h': 1, 'e': 1, 'l': 2, 'o': 1}

Хотя этот код работает, он может быть не очевиден для всех. Более очевидным способом сделать это было бы использование модуля collections и класса Counter, например:

from collections import Counter
word = "hello"
char_count = Counter(word)
print(char_count)

# Output:
# Counter({'l': 2, 'h': 1, 'e': 1, 'o': 1})

Этот подход проще для понимания, так как использует встроенный класс. 14-й принцип — это напоминание о том, что, хотя «очевидный» способ делать что-то может быть не очевиден для всех, важно иметь ясный и простой способ делать что-то в коде.

15. Сейчас лучше, чем никогда

Предположим, вы работаете над проектом по созданию веб-сайта. У вас есть список функций, которые вы хотите внедрить, но вам трудно начать, потому что список длинный, и вы не знаете, с чего начать.

Один из подходов заключается в том, чтобы подождать, пока у вас не будет готов полный дизайн и не будут проработаны все детали, прежде чем приступать к кодированию. Но такой подход может привести к задержке и прокрастинации, так как легко добавлять все больше и больше к дизайну, и становится трудно начать кодирование.

Лучший подход — следовать принципу «сейчас лучше, чем никогда» и сразу приступать к кодированию, даже если у вас нет готового дизайна. Вы можете начать с небольшого фрагмента веб-сайта, например главной страницы, а затем перейти к следующему фрагменту. Такой подход поможет вам начать работу и добиться прогресса, а также даст вам возможность быстрее увидеть результаты своей работы.

16. Хотя никогда лучше, чем прямо сейчас

Предположим, вы работаете над проектом по созданию веб-сайта для клиента. Клиент попросил вас реализовать функцию, позволяющую пользователям загружать изображения. Однако вы знаете, что эта функция сложна и может потребовать дополнительного времени и ресурсов для реализации. Вы можете внедрить эту функцию сразу, но это может задержать реализацию проекта и вызвать разочарование у клиента.

Лучший подход — следовать принципу «хотя никогда лучше, чем прямо сейчас». Это означает, что лучше подождать и внедрить функцию позже, когда у вас будет время и ресурсы, чтобы сделать ее должным образом, чем спешить с ее реализацией сейчас, рискуя вызвать задержки или проблемы.

В этом случае вы можете объяснить клиенту, что функция загрузки изображений сложна и может потребовать дополнительного времени и ресурсов. Вы можете предложить альтернативное решение, например, использовать сторонний сервис для загрузки изображений, пока у вас не будет времени и ресурсов для правильной реализации этой функции.

16-й принцип дзен Python — это напоминание о том, что лучше потратить время на то, чтобы сделать все правильно, даже если это означает подождать, чем спешить сделать это сейчас и рискнуть создать проблемы.

17. Если реализацию сложно объяснить, это плохая идея

Допустим, вы пишете функцию, которая принимает строку текста и возвращает новую строку, в которой удалены все гласные. Одним из способов сделать это может быть использование регулярного выражения для сопоставления гласных и замены их пустой строкой:

import re

def remove_vowels(text):
    return re.sub('[aeiouAEIOU]', '', text)

Этот код работает нормально, но может быть сложно объяснить, что именно здесь происходит, если вы не знакомы с регулярными выражениями. Регулярные выражения — мощный инструмент, но они также могут быть запутанными и трудными для чтения.

Более простым и понятным решением было бы использовать цикл для перебора каждого символа в строке и проверки, является ли он гласным:

def remove_vowels(text):
    vowels = 'aeiouAEIOU'
    new_text = ''
    for char in text:
        if char not in vowels:
            new_text += char
    return new_text

Этот код выполняет то же самое, что и предыдущий пример, но его легче понять и объяснить. Мы используем цикл для перебора каждого символа в строке и проверки, является ли он гласным, а если нет, добавляем его в новую строку.

18. Если реализацию легко объяснить, это может быть хорошей идеей.

Предположим, вы хотите написать функцию, которая возвращает n-е число Фибоначчи. Вы можете написать реализацию, используя цикл for и список для отслеживания чисел Фибоначчи, например:

def fibonacci(n):
    fib = [0, 1]
    for i in range(2, n+1):
        fib.append(fib[i-1] + fib[i-2])
    return fib[n]

Эта реализация работает, но ее не так просто объяснить. Более простой реализацией, которую легче объяснить, является рекурсивный подход:

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

Эту реализацию легко объяснить, так как она использует рекурсию для вычисления числа Фибоначчи. Он также имеет четкую и лаконичную структуру, что упрощает его понимание и обслуживание.

18-й принцип дзен Python — это напоминание о том, что хотя реализация может быть технически правильной, важно учитывать, легко ли ее объяснить и понять. Код, который легко объяснить и понять, обычно легче поддерживать и отлаживать, и он с большей вероятностью будет использоваться другими.

19. Пространства имен — это отличная идея — давайте сделаем больше таких!

Предположим, в вашем коде есть несколько функций, которые используют переменную с именем counter, но каждая функция должна поддерживать свой собственный счет.

Без пространств имен вы могли бы написать что-то вроде этого:

def process_1():
    global counter
    counter = 0
    # some processing that increments counter

def process_2():
    global counter
    counter = 0
    # some processing that increments counter

Такой подход может быстро стать запутанным и подверженным ошибкам, особенно по мере роста количества функций.

С пространствами имен вы могли бы написать что-то вроде этого:

def process_1():
    counter = 0
    # some processing that increments counter
    return counter

def process_2():
    counter = 0
    # some processing that increments counter
    return counter

Каждая функция имеет свою отдельную переменную counter, содержащуюся в ее собственном пространстве имен, поэтому нет риска того, что одна функция будет мешать подсчету другой функции. Это делает код чище, организованнее и проще в обслуживании.

Заключение

Эти принципы следует рассматривать как рекомендации, а не жесткие правила. Вы должны взвесить преимущества применения того или иного принципа в сравнении с конкретными потребностями и ограничениями вашего проекта. Например, вы можете решить, что удобочитаемость важнее, чем производительность в определенном контексте, или наоборот.

Спасибо за чтение и счастливого кодирования 💙