Тонкости Python |

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

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

Изменяемые объекты и атрибуты

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

sub¬tleties0.py (Источник)

"""Mutable objects as class attributes."""
import logging

logger = logging.getLogger(__name__)


class Query:
    PARAMETERS = {"limit": 100, "offset": 0}

    def run_query(self, query, limit=None, offset=None):
        if limit is not None:
            self.PARAMETERS.update(limit=limit)
        if offset is not None:
            self.PARAMETERS.update(offset=offset)
        return self._run(query, **self.PARAMETERS)

    @staticmethod
    def _run(query, limit, offset):
        logger.info("running %s [%s, %s]", query, limit, offset)

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

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

>>> q = Query()
>>> q.run_query("select 1")
running select 1 [100, 0]

>>> q.run_query("select 1", limit=50)
running select 1 [50, 0]

>>> q.run_query("select 1")
running select 1 [50, 0]

>>> q.PARAMETERS
{'limit': 50, 'offset': 0}

>>> new_query = Query()
>>> new_query.PARAMETERS
{'limit': 50, 'offset': 0}

Как видите, это крайне нестабильно и хрупко.

Вот несколько общих рекомендаций по решению этой проблемы:

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

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

• Для пункта 1 вы всегда должны учитывать компромисс между памятью и скоростью. Если объект слишком большой (возможно, большой словарь), запуск copy.deepcopy() для него будет медленным и займет много памяти, поэтому, вероятно, быстрее будет просто изменить его на месте.
• Исключение из правил [2] это при использовании дескрипторов, когда вы рассчитываете на этот побочный эффект. Кроме этого, не должно быть никаких причин идти по такому опасному пути.
• Правило [3] не должно быть проблемой, если атрибуты доступны только для чтения. В этом случае установка словарей и списков в качестве атрибутов класса может быть уместной, но даже если вы в настоящее время можете быть уверены в его неизменности, вы не можете гарантировать, что никто никогда не нарушит это правило в будущем.

Итераторы

Протокол итератора Python позволяет вам обрабатывать весь набор объектов по их поведению, независимо от их внутреннего представления.

Например, рассмотрим следующее:

for i in myiterable: ...

Что такое myiterable в приведенном выше коде? Это может быть список, кортеж, словарь или строка, и он все равно будет работать нормально. На самом деле, вы также можете полагаться на все методы, использующие этот протокол:

mylist.extend(myiterable)

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

def process_files(files_to_process, target_directory):
    for file_ in files_to_process:
        # ...
        shutil.copy2(file_, target_directory)

Ты видишь, что происходит? Здесь компилятор точно не знает, что такое files_to_process (кортеж, список или словарь).

Строки также могут быть итерируемыми. Предположим, вы передаете один файл (скажем, /home/ubuntu/foo). Каждый символ перебирается, начиная с /, затем h и так далее, что, собственно, и замедляет работу программы. Использование лучшего интерфейса может помочь решить эту проблему. Например:

def process_files(*files_to_process, target_directory):
    for file_ in files_to_process:
        # ...
        shutil.copy2(file_, target_directory)

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

Надеюсь, вам понравилось читать эту статью. Если вы хотите узнать больше о том, как рефакторить устаревший код, вы можете изучить Чистый код в Python Мариано Анайя. Упакованный многочисленными практическими примерами и задачами, Чистый код в Python обязательна к прочтению для руководителей групп, архитекторов программного обеспечения и старших инженеров, стремящихся улучшить устаревшие системы для повышения эффективности и сокращения затрат.

Похожие записи

Добавить комментарий

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