Строковые дубликаты в исходниках python — вариант решения

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

def send_notification(respondents: list, message: dict) -> None:
    for resp in respondents:
        to_send = copy(message)
        if "subject" not in to_send:
            to_send["subject"] = "Hello, " + str(resp)
        if "from" not in to_send:
            to_send["from"] = None
        to_send['body'] = to_send['body'].replace('@respondent@', resp)
        to_send['crc'] = crc_code(to_send['body'])

Сразу оговорюсь - я не автор, данный код взят из доклада Григория Бакунова (и он не автор), на одной из конференций посвященных python (есть на youtube). Там рассматривались вопросы производительности, но я хотел бы поговорить не об этом. А о строках - строках "subject", "from", "body" - и подобных, которые дублируются в этом исходнике в виде строковых литералов. И наверняка в этом проекте еще в куче других мест они снова и снова всплывают в виде дублей... Специально взят чужой пример, чтобы обратить внимание, что данная проблема существует, не только в моем личном опыте.

С самого начала изучения языка python я постоянно встречаю подобное в самых разных исходниках разных разработчиков. И это для меня загадка - почему никто не борется с этим дублированием - вроде как DRY он и здесь должен же работать? Нет?

В общем-то чем это плохо:

1) Лишняя память при создании каждого экземпляра той же самой строки - это на самом деле маловероятно в python, т.к. в нем есть механизм автоматического интернирования строк похожих на идентификаторы (коротких с определенными символами). Создается словарь таких строк и они не дублируются в памяти и могут быстро сравниваться (к ним создается хеш), а не посимвольно. Так же в зависимости от реализации интерпретатора вообще все строки хранящиеся в исходнике могут подвергаться интернированию. Поэтому про память скорее мимо.

>>> "abc" is "abc"
True
>>> id("abc") == id("abc")
True

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

2) Многократно повышается вероятность ошибки опечаткой. Вместо одного места в коде, где строка присваивалась бы в константу, мы то и дело пишем эту строку и шанс получить скажем символ "c" не из латиницы, а кириллицы (а какой сейчас здесь?-глазами вообще не видно...) возрастает многократно. Да современные IDE позволяют проводить поиск и замену по проекту, но что вы будете искать, когда не знаете точно в каком слове опечатка?

3) Сложность поиска ошибок - Ни интерпретатор, ни линтеры нам не помогают такие ошибки найти. Если бы была константа subject_str содержащая "subject", то опечатавшись при упоминании константы мы получили бы ошибку от линтера, а если и нет(нет линтера или он сплоховал), то в рантайме, мы все равно получили бы четкое сообщение об ошибке, т.к. такого идентификатора ( например имя константы с опечаткой "subject_strr" - клавиша залипла, палец дрогнул и т.д. ) просто не существует. А вот неверный строковый литерал просто даст баги в рантайме, возможно вообще без падений, при сравнении с неверной строкой может не выполняться условие никогда и т.д.

4) Это неудобно поддерживать. Если в примере выше тег "subject" по какой-то причине придется заменить например на "topic" - то это как раз превращается в игру с теми самыми средствами поиска и замены в IDE, при этом надо внимательно смотреть каждое включение, ведь не обязательно, что именно все "subject" строки в проекте - это именно то, что надо будет заменить. Если бы строка была объявлена в одном месте константой, можно было бы просто изменить ее значение, при условии конечно, что константа использована согласно логике модулей.

Кстати, инструмент статического анализа кода SonarCube так же считает это проблемой https://rules.sonarsource.com/python/RSPEC-1192

Напрашивается простое решение в виде класса-справочника со строковыми константами.

class Colors:
    red   = "red"
    black = "black"
    white = "white"
C = Colors

...

print(C.red)

Конечно получше, но все таки остается дурацкое дублирование в описание класса, которое в 90% случаев будет именно таким и будет мозолить глаза, а также является местом для ошибки, хотя и вероятность ее сильно сокращается. Надо использовать метаклассы - так я сказал своему коллеге и он через 5 мин выдал простое и логичное решение, которым я лично пользуюсь до сих пор. А почему нет? Метаклассы создают классы, участвуют при их создании, атрибуты модифицировать могут, значит они нам подходят.

init_copy = None

class CStrProps_Meta(type):
    def __init__(cls, className, baseClasses, dictOfMethods):
        for k, v in dictOfMethods.items():
            if k.startswith("__"):
                continue
            if v is None:
                setattr(cls, k, k)

Логика работы простая - дандер методы пропускаем, те атрибуты, которые уже инициализированы - пропускаем, а тем, которые заполнены None присваиваем значение равное имени атрибута. Здесь init_copy просто для намека на то, что члены будущего класса словаря будут проинициализированы метаклассом, а вовсе не останутся None, как могло юы показаться при беглом взгляде на класс.

class Colors(metaclass=CStrProps_Meta):
    red   = init_copy
    black = init_copy
    white = init_copy
    msg   = "Color is "
C = Colors

print(C.msg + C.red)

Можно добавить дополнительный функционал - какой нужен в конкретном проекте, например заблокировать возможность редактирования уже созданных в классе справочнике констант:

class CStrProps_Meta(type):
    def __init__(cls, className, baseClasses, dictOfMethods):
        cls._items = {}
        for k, v in dictOfMethods.items():

            if k.startswith("__"):
                continue

            if v is None:
                setattr(cls, k, k)

            cls._items[k] = getattr(cls, k, k)

        cls.bInited = True
    
    def __setattr__(cls, *args):
        if hasattr(cls, "bInited"):
            raise AttributeError('Cannot reassign members.')
        else:
            super().__setattr__(*args)

Добавить вывод всех элементов в виде словаря:

    def dict(cls):
        return cls._items


Добавить итератор для возможности обхода циклом for:

    def __iter__(cls):
        return iter(cls._items)

Стоит конечно написать и простенький юнит тест:

import unittest

class Dummy_Str_Consts(metaclass = CStrProps_Meta):
    name  = init_copy
    Error = "[Error:]"
    Error_Message = f"{Error}ERROR!!!"
DSC = Dummy_Str_Consts

class Test_Str_Consts(unittest.TestCase):
    def test_str_consts(self):
        self.assertEqual( Dummy_Str_Consts.name,  "name" )

        self.assertEqual( Dummy_Str_Consts.Error, "[Error:]" )

        self.assertEqual( Dummy_Str_Consts.Error_Message, "[Error:]ERROR!!!" )

        l = ["name", "Error", "Error_Message"]
        l1 = []
        for i in DSC:
            l1.append(i)
        self.assertEqual( l, l1 )
        self.assertEqual( l, list(DSC.dict().keys()) )

        l = ["name", "[Error:]", "[Error:]ERROR!!!"]
        self.assertEqual( l, list(DSC.dict().values()) )

        with self.assertRaises(AttributeError):
            DSC.name = "new name"


        print(C.msg + C.black)

if __name__ == "__main__":
    unittest.main()

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

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

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

Источник: https://habr.com/ru/post/713474/


Интересные статьи

Интересные статьи

Всем привет! Меня зовут Евгений Симигин, я занимаюсь внедрением DevOps-практик в Центре компетенций по разработке облачных и интернет-решений МТС Digital. В этой статье – обзор Argo Rollouts, я покаж...
Мы стараемся держать руку на пульсе мира потребительской электроники, которая может быть полезна разработчикам. Довольно часто пишем о мини-ПК, и сегодня — не исключение. Правда, расскажем не о прив...
Существует несколько методов нахождения корней полиномиального уравнения 4-ой степени. Однако они не очень удобны при решении уравнений с коэффициентами, которые представляют собой в...
Эта книга — недостающая глава, отсутствующая в каждой всеобъемлющей книге Python. Frank Ruiz Principal Site Reliability Engineer, Box, Inc.
Привет, Хабр! Сегодня будем прорабатывать навык использования средств группирования и визуализации данных в Python. В предоставленном датасете на Github проанализируем несколько характери...