Насоки за кодиране за всички езици.

Ако сте програмист на 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. Плоското е по-добре от вложеното

Да предположим, че имате списък от списъци, където всеки вътрешен списък съдържа името на човек и неговата възраст. Искате да създадете нов речник, където ключовете са имената, а стойностите са възрастите. Един от начините да направите това е да използвате вложени цикли:

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}

Въпреки че тази реализация работи правилно, тя е вложена с две нива на отстъп. По-Pythonic начин за постигане на същото нещо е да се използва разбиране на списък и функцията 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

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

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, съдържаща се в нейното собствено пространство от имена, така че няма риск една функция да пречи на броя на друга функция. Това прави кода по-чист, по-организиран и по-лесен за поддръжка.

Заключение

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

Благодарим ви за четенето и приятно кодиране 💙