Хороший инженер-программист понимает, насколько важно внимание к деталям; мельчайшие детали, если их упустить, могут создать огромную разницу между рабочим устройством и катастрофой. Вот почему написание чистого кода имеет большое значение—и чистый код-это не просто аккуратное отступление и форматирование; это внимание к тем деталям, которые могут повлиять на производство.
В этой статье вы увидите несколько коротких примеров проблемного кода в Python и то, как их можно улучшить. Обратите внимание, что это всего лишь примеры, и вы ни в коем случае не должны интерпретировать их как универсально применимые к реальным проблемам.
Изменяемые объекты и атрибуты
Одной из наиболее привлекательных особенностей функционального программирования является неизменность. Мутация просто не допускается в функциональном программном обеспечении. Однако в большинстве приложений, определяемых с помощью объектов, объекты подвержены мутациям, изменяя свое внутреннее состояние или представление. Согласен, что могут быть неизменяемые объекты, но таких случаев мало и они находятся далеко друг от друга. Взгляните на следующий фрагмент кода. (Отказ от ответственности: В этом есть что-то очень, очень неправильное!)
subtleties0.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 вы всегда должны учитывать компромисс между памятью и скоростью. Если объект слишком большой (возможно, большой словарь), запуск copy.deepcopy() на нем будет медленным и займет много памяти, поэтому, вероятно, быстрее просто изменить его на месте. • Исключение для правила [2] – при использовании дескрипторов, когда вы рассчитываете на этот побочный эффект. Кроме этого, не должно быть никаких причин идти по такому опасному пути. • Правило [3] не должно быть проблемой, если атрибуты доступны только для чтения. В этом случае установка диктантов и списков в качестве атрибутов класса может быть хорошей, но даже если вы сейчас уверены в их неизменности, вы не можете гарантировать, что никто никогда не нарушит это правило в будущем.
Итераторы
Итераторный протокол Python позволяет обрабатывать весь набор объектов по их поведению, независимо от их внутреннего представления.
Например, рассмотрим следующее:
for i in 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). Каждый символ повторяется, начиная с/, а затем рука скоро, что, по сути, замедляет работу программы. Использование лучшего интерфейса может помочь решить эту проблему. Например:
def process_files(*files_to_process, target_directory): for file_ in files_to_process: # ... shutil.copy2(file_, target_directory)
В приведенном выше примере сигнатура функции использует более чистый интерфейс, поскольку она допускает несколько файлов в качестве аргументов, тем самым устраняя проблему, описанную ранее. Более того, он также делает target_directory только ключевым словом, что даже более явно.
Надеюсь, вам понравилось читать эту статью. Если вы хотите узнать больше о том, как рефакторировать устаревший код, вы можете изучить Clean Code in Python by Mariano Anaya. Упакованный с многочисленными практическими примерами и проблемами, Чистый код на Python является обязательным чтением для руководителей команд, архитекторов программного обеспечения и старших инженеров, стремящихся улучшить устаревшие системы для повышения эффективности и экономии затрат.