Прежде чем предложить свой взгляд на это явление, хотелось бы описать само явление. Я совершенно не понимаю почему, но в питон исходниках разработчики очень часто дублируют строки. Например есть такой кусок кода:
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/
Добавить комментарий