6 практик Python, которые отличают Сениоров от Джуниоров

В январе 2023 года я опубликовал статью о 5 хитростях Python, которые отличают Сениоров от Джуниоров. В этой статье, вместо того чтобы рассматривать хитрости, мы рассмотрим 6 лучших практик в Python, которые могут отличить опытных разработчиков от новичков. На различных примерах мы рассмотрим различия между кодом, написанным Senior разработчиком, и кодом, написанным Джуниор-разработчиком.

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

1. Используйте правильный итеративный тип данных

Iterable – это любой объект Python, способный возвращать свои элементы по одному за раз, что позволяет выполнять итерации по нему в цикле for. (источник)

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

  • Списки предназначены для повторяющихся объектов, которые должны быть упорядочены и изменяемы.
  • Множества предназначены для итераций, которые должны содержать только уникальные значения и являются изменяемыми и неупорядоченными. Им следует отдавать предпочтение при проверке на наличие товара, в которой они выполняются чрезвычайно быстро. Однако они работают медленнее, чем список, когда используются для перебора.
  • Кортежи предназначены для итераций, которые должны быть упорядоченными и неизменяемыми. Кортежи работают быстрее и с большей экономией памяти, чем списки.

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

requested_usernames = ["John123", "Delilah42"]
taken_usernames = []
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.append(username)
  else:
    print(f"Username '{username}' is already taken!")

taken_usernames – это список в приведённом выше коде. Однако все значения в taken_usernames должны встречаться только один раз, нет необходимости в повторяющихся значениях, так как повторяющиеся имена пользователей не допускаются. Кроме того, вариант использования здесь для taken_usernames заключается в проверке наличия в нём нового имени пользователя. Следовательно, здесь нет причин использовать список. Вместо этого лучше использовать множество (это связано с тем, что проверка наличия выполняется быстрее при использовании множества и потому что нет необходимости сохранять одно и то же значение более одного раза).

requested_usernams = ["John123", "Delilah42"] 
taken_usernames = set()
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.add(username)
  else:
    print(f"Username '{username}' is already taken!")

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

Для итеративных объектов, которые не будут изменяться во время выполнения и нуждаются в упорядочении, лучшим вариантом является кортеж; например:

# more junior example
weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]

for day in weekdays:
  ...

--------------------------------------------------------------------------
# more senior example
WEEKDAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")

for day in WEEKDAYS:
  ...

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

В следующем разделе мы рассмотрим соглашения об именовании в Python, после чего станет ясно, почему, например, WEEKDAYS было написано именно заглавными буквами (в примере выше).

2. Используйте соглашения об именовании в Python

В Python существует два вида правил для имён переменных:

  • Принудительные правила
  • Соглашения об именовании

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

2nd_name = "John"

# output 
SyntaxError: invalid syntax

---------------------------

second-name = "John"

# output 
SyntaxError: invalid syntax

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

Это некоторые из наиболее важных соглашений об именовании в Python:

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

  • first_name items names_list

функции и методы: то же правило, что и для переменных, например:

  • get_avg load_data print_each_item

классы: используйте camelCasing; начинайте с заглавной буквы, и каждое новое слово начинается с другой заглавной буквы, без подчеркиваний между ними:

  • Person TrainStation MarineAnimal

константы: только в верхнем регистре, с подчеркиванием для разделения слов, например:

  • WEEKDAYS FORBIDDEN_WORDS

модули: для имён файлов Python используйте то же соглашение, что и для переменных, функций и методов (нижний регистр с подчёркиванием для разделения слов):

  • calculations.py data_preprocessing.py

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

Код на Python, соответствующий соглашениям об именовании PEP-8:

# circle.py

PI = 3.14 # Value won't change, so it's a constant

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    @property
    def area(self):
        return (self.radius **2) * PI
    
    @property
    def perimeter(self):
        return 2 * self.radius * PI

small_circle = Circle(1)
big_circle = Circle(5)

print(f"Area of small circle = {small_circle.area}")
print(f"Perimeter of small circle = {small_circle.perimeter}")

print(f"Area of big circle = {big_circle.area}")
print(f"Perimeter of big circle = {big_circle.perimeter}")

Код на Python, который не использует соглашения об именовании:

# CIRCLE.py

Pi = 3.14

class CIRCLE:
    def __init__(Self, RADIUS: float):
        Self.Radius = RADIUS

    @property
    def AREA(Self):
        return (Self.Radius **2) * Pi
    
    @property
    def PERIMETER(Self):
        return 2 * Self.Radius * Pi

SmallCIRCLE = CIRCLE(1)
BigCIRCLE = CIRCLE(5)

print(f"Area of small circle = {SmallCIRCLE.AREA}")
print(f"Perimeter of small circle = {SmallCIRCLE.PERIMETER}")

print(f"Area of big circle = {BigCIRCLE.AREA}")
print(f"Perimeter of big circle = {BigCIRCLE.PERIMETER}")

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

Для получения более подробной информации о соглашениях об именовании в Python см. PEP-8.

3. Используйте правильные инструкции для сравнения

Операторы сравнения (…) сравнивают значения по обе стороны от них и возвращают логическое значение. Они определяют, является ли утверждение истинным или ложным в зависимости от условия. (источник)

В Python существует много способов написания (почти) идентичных операторов сравнения, но они не обязательно одинаково подходят. Давайте взглянем на небольшой пример:

def is_even(x):
  return True if x % 2 == 0 else False

x = 10

# different comparison statements which result in the same result:
if is_even(x) == True:
  print(f"{x} is an even number!")

if is_even(x) is True:
  print(f"{x} is an even number!")

if is_even(x):
  print(f"{x} is an even number!")

Важно отметить, что почти любая переменная Python будет иметь значение True, за исключением:

  • Пустых последовательностей, таких как списки, кортежи, строки и т.д.
  • Числа 0 (как в целочисленном, так и в плавающем типе)
  • None
  • False (очевидно)

Это означает, например, что каждый оператор if <number>: будет иметь значение True, за исключением случаев, когда это число равно 0. Следовательно, оператор if без какого-либо конкретного примера может показаться слишком глобальным для использования, поскольку высока вероятность того, что он невольно может быть оценён как True. Тем не менее, мы можем очень хорошо использовать тот факт, что пустые последовательности всегда оцениваются как False, в то время как последовательность, имеющая хотя бы одно значение, всегда оценивается как True. Часто в более неосознанном Python вы столкнётесь со следующим оператором сравнения: if <переменная> != [], например, в приведенном ниже фрагменте.

def print_each_item(items):
  if items != []:
    for item in items:
      print(item)
  else:
    raise ValueError("'items' is an empty list, nothing to print!")

Однако, что произойдет, когда кто-то вставит другой итерируемый тип? Например, множество. Если вы хотите вызвать ValueError для пустого списка, вы, вероятно, также захотите вызвать ValueError для пустого множества. В приведённом выше коде пустое множество всё равно будет иметь значение True, потому что оно не равно пустому списку. Один из способов предотвратить такое нежелательное поведение – использовать if items вместо if items != [], потому что if items теперь будет вызывать ValueError для каждой пустой итерации, включая список, множество и кортеж из section1.

def print_each_item(items):
  if items:
    for item in items:
      print(item)
  else:
    raise ValueError("No items to print inside 'items'")

Если вы хотите сравнить значение с True или False , вам следует использовать is True или is False вместо == True и == False . Это также относится к None , потому что все они являются одиночными. Смотрите PEP285. Хотя различия в производительности незначительны, is True работает немного быстрее, чем == True. Прежде всего, это показывает, что вы знакомы с PEPs (предложениями по улучшению Python), что свидетельствует о зрелости навыков разработчика.

# more junior example
if is_even(number) == True:
  
# more senior example
if is_even is True:
  
-------------------

# more junior example
if value == None:
  
# more senior example
if value is None:

Бонусный совет: PEP8 предупреждает об использовании значения if, чтобы убедиться, что значение не равно None. Чтобы проверить, не равно ли значение None , используйте if value is not None.

Выбор наиболее подходящего оператора сравнения иногда может избавить вас или других пользователей от необходимости отлаживать сложную ошибку. Но, прежде всего, Senior разработчики оценят, что вы более равны им, если вы используете, например, if value is True, а не if value == True.

Конечно, вместо того, чтобы просто писать оператор сравнения для значения переменной, лучше сначала проверить тип данных, но как мы создаем хорошие исключения для этого? Давайте взглянем на создание информативных исключений в следующем разделе!

4. Создавайте информативные исключения

Что-то, что Джуниоры редко делают – это “вручную” создавать исключения с помощью пользовательских сообщений. Давайте рассмотрим следующую ситуацию: мы хотим создать функцию с именем print_each_item(), которая выводит каждый элемент итеративного типа.

Самым простым решением было бы:

def print_each_item(items):
  for item in items:
    print(item)

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

Функция print_each_item() работает только с итеративными объектами Python, поэтому нашей первой проверкой должно быть, может ли Python выполнять итерации по предоставленному аргументу, используя встроенную в Python функцию iter():

def print_each_item(items):

  # check whether items is iterable
  try:
    iter(items)
  except TypeError as error:
    raise (
      TypeError(f"items should be iterable but is of type: {type(items)}")
      .with_traceback(error.__traceback__)  
    )

Пробуя функцию iter() для элементов, мы проверяем, возможно ли выполнять итерации по элементам. Конечно, также возможно проверить тип элементов через isinstance(items, Iterable), однако некоторые пользовательские типы Python могут не считаться итерируемыми. Мы добавляем .with_traceback здесь к исключению, чтобы предоставить больше контекста для отладки при возникновении ошибки.

Далее, когда мы подтвердим, что items является итерируемым, мы должны убедиться, что items не является пустым, чтобы предотвратить отсутствие результатов вывода:

def print_each_item(items):

  # check whether items is an Iterable
  try:
    iter(items)
  except TypeError as e:
    raise (
      TypeError(f"'items' should be iterable but is of type: {type(items)}")
      .with_traceback(e.__traceback__)  
    )

  # if items is iterable, check whether it contains items
  else:
    if items:
      for item in items:
        print(item)

    # if items doesn't contain any items, raise a ValueError
    else:
      raise ValueError("'items' should not be empty")

Ваша функция, скорее всего, всё равно будет отклонена при проверке другими пользователями. Это потому, что она не содержит docstring или каких-либо намёков на типы, которые необходимы для высококачественного кода на Python.

5. Аннотации типов и docstrings

Аннотации типов были введены в Python 3.5. С помощью них вы можете подсказать, какой тип ожидается. Очень упрощённым примером может быть:

def add_exclamation_mark(sentence: str) -> str:
  return f"{sentence}!"

Указывая str в качестве аннотации типа, мы знаем, что предложение должно быть строкой, а не, например, списком со словами. Через -> str мы указываем, что функция возвращает объект типа string. Python не будет применять правильные типы (if не вызовет исключения, если вставлен объект другого типа), но часто IDE, такие как Visual Studio Code и PyCharm, помогают вам писать код, используя аннотации типа (см. скриншот далее в этом разделе).

Мы также можем применить это в нашем print_each_item() через:

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  ...

Строки документации помогают объяснить фрагменты кода, такие как функции или классы. Мы можем добавить следующую строку документа в print_each_item(), чтобы другим пользователям и нам самим в будущем было абсолютно ясно, что делает эта функция:

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  """
  Prints each item of an iterable.
  
  Parameters:
  -----------
  items : Iterable
      An iterable containing items to be printed.

  Raises:
  -------
  TypeError: If the input is not an iterable.
  ValueError: If the input iterable is empty.

  Returns:
  --------
  None
  
  Examples:
  ---------
  >>> print_each_item([1,2,3])
  1
  2
  3
  
  >>> print_each_item(items=[])
  ValueError: 'items' should not be empty
  """
  ...

Теперь, если мы пишем код, который использует print_each_item, мы видим, что появляется следующая информация:

6 практик Python, которые отличают Сениоров от Джуниоров

Добавив подсказки по типам и docstrings, мы сделали нашу функцию намного более удобной для пользователя!

Для получения дополнительной информации об аннотациях типа нажмите здесь. Подробнее о документах смотрите в PEP-257.

Примечание: может показаться, что писать длинную строку документа для такой простой функции – это немного излишне, и вы могли бы возразить, что иногда это так и есть. К счастью, когда функция не является секретной, вы можете легко попросить ChatGPT написать для вас очень точную и продуманную строку документа!

6. Используйте логирование

Есть несколько вещей, которые делают использование вашего кода намного приятнее для других, такие как аннотации типа и docstrings. Однако одной из наиболее важных, малоиспользуемых и недооценённых функций является logging. Хотя многие (Junior) разработчики считают логирование сложным или ненужным, запуск правильно протоколированной программы может иметь огромное значение для любого, кто использует ваш код.

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

import logging

logger = logging.getLogger(__name__)

Теперь вы можете легко добавить логирование, чтобы помочь, например, с отладкой:

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """
  logger.debug(
    f"Printing each item of an object that contains {len(items)} items."
  )
  ...

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

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """
  
  logger.debug(
      f"Printing each item of an object that contains {len(items)} items."
    )
    
  # check whether items is iterable
  try:
    iter(items)
  except TypeError as e:
    error_msg = f"'items' should be iterable but is of type: {type(items)}"
    logger.error(error_msg)
    raise TypeError(error_msg).with_traceback(e.__traceback__)
  
  # if items is iterable, check whether it contains items
  else:
    if items:
      for item in items:
        print(item)
  
    # if items doesn't contain any items, raise a ValueError
    else:
      error_msg = "'items' should not be empty"
      logger.error(error_msg)
      raise ValueError(error_msg)

Поскольку логирование – такая редкая функция, которую можно увидеть в коде более молодых разработчиков, добавление её в свой собственный делает вас уже (кажущимся) гораздо более опытным в Python!

Заключение

В этой статье мы рассмотрели 6 лучших практик Python, которые могут изменить представление о джуниор-разработчике. Конечно, есть ещё много факторов, которые отличают оптыных разработчиков от джунов, однако, применяя эти 6 практик, вы определённо отличите себя (будь то на работе, на собеседовании по программированию или при создании пакетов с открытым исходным кодом) от других джунов разработчиков, которые не применяют их!

Ресурсы

Итерируемые типы

 https://www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Iterables.html#
https://stackoverflow.com/questions/2831212/python-sets-vs-lists
https://towardsdatascience.com/15-examples-to-master-python-lists-vs-sets-vs-tuples-d4ffb291cf07

Соглашение об именовании
https://peps.python.org/pep-0008
https://www.techtarget.com/whatis/definition/CamelCase#:~:text=CamelCase%20is%20a%20way%20to,humps%20on%20a%20camel’s%20back.

Правильные инструкции для сравнения
https://peps.python.org/pep-0008
https://peps.python.org/pep-0285/
https://flexiple.com/python/comparison-operators-in-python/

Аннотации типа и docstrings
https://docs.python.org/3/library/typing.html
https://peps.python.org/pep-0257/

Логирование
https://docs.python.org/3/library/logging.html

Мемы
https://www.reddit.com/r/ProgrammerHumor/comments/l9lbm2/code_review_be_like/

+1
0
+1
1
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *