Прежде чем предложить свой взгляд на это явление, хотелось бы описать само явление. Я совершенно не понимаю почему, но в питон исходниках разработчики очень часто дублируют строки. Например есть такой кусок кода:
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), а можно просто присвоить в переменную лишь в одном месте то, что нужно нам там сейчас.
В общем такой вариант борьбы с этими повторяющимися строковыми литералами использую я, возможно есть решение и еще лучше, предлагаю поделиться в комментариях.
Комментарии (99)
druidvav
29.01.2023 07:25+15Сделали мутную неочевидную штуку, код который не делает вообще ничего полезного, но который нужно будет в будущем поддерживать. Это очень плохо.
break1 Автор
29.01.2023 07:40+1В чем муть и что надо поддерживать? Неужели код метакласса на 3 строчки кода это так сложно?
druidvav
29.01.2023 07:43+6через много лет кому-то это мигрировать на новый питон и пытаться понять зачем это сделано. сложное решение отсутствующей проблемы.
break1 Автор
29.01.2023 07:53Много лет это сколько? 20? Через сколько версий питона вы собираетесь проскочить, чтобы потом вдруг именно этот простенький код не заработал, там уже от текущей актуальной версии вообще ничего наверное не останется? Ни метаклассов, ни классов? Или как?
Про отсутствующую проблему - точно мимо. Проблема - это дублирование строк, в чем именно она заключается подробно описано в первой части статьи.
В 3.11 есть стандартное решение на основе энамов. Предложено в комментарие выше. Синтаксис описания класса справочника будет схож на 90%from enum import StrEnum, auto
class Color(StrEnum):
....red = auto()
....blue = auto()
....white = auto()
....msg = "color is "
C = Colorprint( C.msg + C.red )
Или вы можете использовать только то, что написано в стандартных или сторонних либах, оправдывая это тем, что поддерживать будет кто-то другой?
Основная мысль в статье - борьба со строковыми дубликатами, возможно энамы будут лучшим решением, но называть это отсутствующей проблемой точно не стоит.druidvav
29.01.2023 07:55+6Если что-то можно реализовать стандартными средствами — нужно реализовать стандартными средствами.
Проблемы в дублировании строк даже и нет как таковой, проблема надумана и вы сами это понимаете судя по оговоркам в статье.
break1 Автор
29.01.2023 07:57-6Какими оговорками? Вы ее хоть читали? Или просто поспорить ни о чем есть большое желание? Подробно описал в чем проблемы с дублированием строк. Спорить с этим просто не имеет смысла.
Стандартное решение есть в 3.11, для меня на большинстве проектов доступен пока только 3.9. Можно поиграться с модификацией стандартного энама (что и сделано в 3.11 с наследованием от str и переопределением функции генерации следующего значения), но очевидно лично вам это покажется еще сложнее чем метакласс на 3 строчки кода. Это ведь тоже надо будет поддерживать.
druidvav
29.01.2023 08:02+9Зачем решать проблему? у вас 1 пункт вообще не проблема, 2, 3, 4 решаются иде, а если не нравится решение иде — есть решение стандартным функционалом которое вы сами же и привели. Потом, когда перейдете на 3.11 — переделаете на новое стандартное решение.
Поменьше агрессии, а то так можно и инфаркт на код-ревью получить.Предложенное вами "решение" это яркий пример overengineering, почитайте на досуге что это такое
break1 Автор
29.01.2023 08:06-2Агрессии? С моей стороны ее вообще нет. 2,3,4 - не решаются IDE, что я подробно и описал.
До 3.11 какое есть стандартное решение?druidvav
29.01.2023 08:09+3То самое которое вы описали "Конечно получше, но все таки остается дурацкое дублирование в описание класса".
Никому оно в реальности мозолить глаза не будет, потому что модифицируется оно редко, не порождает нового кода, который нужно поддерживать.
break1 Автор
29.01.2023 08:15Я привел пример с 3 цветами для простоты, а в реальных проектах у меня сотни строковых констант. И они отлично мозолят глаза тупым копированием имени переменной в ее значение. И когда эта простыня занимает весь экран (точнее несколько экранов) это очень неудобно поддерживать, гораздо сложнее, чем когда справа будет одно и то же значение "init_copy" или "auto()". Если бы этой проблемы не было, никто бы и не думал вводить StrEnum - который по сути делает примерно то же, да и обычные энамы зачем с автогенерацией значений, можно было бы везде заменить старым добрым присвоением в каждый элемент.
druidvav
29.01.2023 08:21+2StrEnum это просто синтаксический сахар, чтобы сделать немного удобнее. а вы придумали велосипед. если у вас там сотни строковых констант которые вынесены вот так, есть у меня подозрения что что-то вы делаете неправильно. но я могу обсуждать только случай из примера. В примере, если уж хочется поинжинирить, надо сделать класс message, а не прописывать строковые константы.
break1 Автор
29.01.2023 08:31-4Свои подозрения вы можете оставить при себе, как и оценки, что есть велосипед, а что синтаксический сахар, ведь как я понял конечного определения озвучено не будет.
Внутри StrEnum делается ровно то же, что и внутри StrProps_Meta - а именно в атрибут присваивается значение равное имени атрибута. Но в одном случае это почему то велосипед, а в другом сахар ))))
invasy
29.01.2023 11:12+1Давайте попробуем разобраться.
StrEnum
:- входит в стандартную библиотеку, т. е. будет поддерживаться и меняться вместе с языком — стабильный интерфейс;
- является наследником Enum, добавляет только удобство при работе со строковыми перечислениями, не внося существенной новой функциональности, — синтаксический сахар.
Ваше решение:
- не имеет стабильного интерфейса, будет жить и поддерживаться только при вашем желании, зависит от знаний возможностей языка конкретного разработчика;
- дублирует функциональность из стандартной библиотеки — велосипед;
- появилось для неочевидной проблемы: она выглядит надуманной и действительно решается другими инструментами, перечисленными в статье.
Также могу посоветовать спокойнее относиться к ревью и советам. Часто бывает, что они действительно полезные.
break1 Автор
29.01.2023 11:40StrEnum нет в питон до 3.11, кроме того, наличие класса в стандартной библиотеке еще не означает, что его поведение не будет меняться с версиями интерпретатора. Собсвтенно из документации на тот самый метод автогенерации значений:
NoteThe goal of the default
_generate_next_value_()
method is to provide the nextint
in sequence with the lastint
provided, but the way it does this is an implementation detail and may change.
ой... Может быть изменен...
По поводу синтаксический сахар или нет - я уже высказался. По поводу стабильности интерфейса - у вас что проект без классов, которые есть только в ваших проектах и поддерживаются только вами?
Лично мне вообще без разницы что будет использовано в проекте справочник с метаклассом или энам. Энам вносит оверхед. Класс с метаклассом проще. Использовать можно и то и то. Главное избавляться от дублирования строк.
Я нормально отношусь к полезным советам, гораздо забавнее читать как люди в принципе отрицают проблему дублирования.
invasy
29.01.2023 12:01+1отрицают проблему дублирования
Может, её и нет?
Документацию читайте внимательнее: детали реализации могут поменяться, интерфейс и функциональность не изменятся. В версии 3.11 этого примечания уже нет. Да и этот метод предназначен в том числе для самостоятельного переопределения при необходимости.
Заменять литералы объектами в принципе оверхед и оверинжениринг в этой ситуации (как замечено выше в комментах).
break1 Автор
29.01.2023 12:10-1Ну вообще в питоне все объект, если что. И просто строка тоже.
Если проблемы дублирования строк нет - почему она есть в SonarCube?
Нет ни оверхеда ни оверинжиниринга, есть только люди, которые видимо сами злоупотребляют дублированием строк, и видимо поэтому им не хочется это признавать проблемой, т.к. приняли для себя на это забить.
shpaker
29.01.2023 09:51+1Енамы с 3.8 (об этом уже писали выше) нормально будут работать. Велкам ту class SomeEnum(str, Enum).
break1 Автор
29.01.2023 09:58Будут, как и метакласс работает нормально. Можно использовать и то и то. Манипуляции примерно одни и те же. В энаме так же надо переопределить метод автогенерации значений, который в атрибут присвоит в качестве значения его имя. В 3.11 это оформили в StrEnum.
shpaker
29.01.2023 10:07+1Да понял. Ступил. Но в любом енаме питона всегда можно читать ключи как строки.
druidvav
29.01.2023 07:47+3Вы кроме принципа SOLID забыли очень важный принцип KISS.
break1 Автор
29.01.2023 07:56-1Я про SOLID вообще ничего не говорил. Оправдывать любое дублирование в коде одним лишь KISS слабая аргументация.
druidvav
29.01.2023 07:57+2вы в коде очень много раз продублировали if, сделаете макрос, чтобы не дублировать if?
break1 Автор
29.01.2023 08:07Я вижу только 2 ифа в коде, каждый нужен на своем месте и это не дублирование.
druidvav
29.01.2023 08:10Ровно столько же упоминаний subject и from :)
break1 Автор
29.01.2023 08:23+2Ну если вам не видна разница между операторами языка и строковыми константами, которые вводит программист пишущий на этом языке, то вам я ничего не докажу.
pfffffffffffff
29.01.2023 09:28Вообще ваш словарь выполняет функцию объекта у которого эти свойства и должны быть описаны. Использовать константы для имен полей это глупость. Это должно быть инкапсулировано в адаптере.
druidvav
29.01.2023 10:38+1Это конечно совершенно не в тему замечание, но обсессивная борьба с «повторами» она иррациональна.
Lazytech
29.01.2023 08:28+2Сам в основном программирую на JavaScript, реже на TypeScript. Наверное, бывалым разработчикам мой подход покажется примитивным и даже наивным, но все-таки излагаю его ниже.
Если у меня в коде встречаются используемые более чем в одном файле строковые константы, обычно выношу их в отдельный файл (constants.js или constants.ts) и экспортирую. Названия даю в верхнем регистре.
export const ENTITY_ONE = 'entity_one';
export const ENTITY_TWO = 'entity_two';
Затем, соответственно, импортирую эти константы там, где они используются.
import { ENTITY_ONE, ENTITY_TWO } from 'constants.js';
По-моему, такой подход требует минимум усилий. Если что не так, IDE укажет на ошибку.
break1 Автор
29.01.2023 08:34+1Вполне нормальное решение для большинства языков, например на С++ делаю так же, и не знаю способа, как там можно было бы это еще улучшить.
shpaker
29.01.2023 09:54Почему вынос литералов в константы вам не угоден в питоне?
break1 Автор
29.01.2023 10:04В питоне это можно улучшить. Это везде не удобно до конца, но там улучшить это нельзя.
shpaker
29.01.2023 10:10+2Улучшение это когда что-то становится лучше, а у вас просто какая-то странная штука которая лучше то ничего и не делает.
break1 Автор
29.01.2023 10:21Вообще то она убирает дублирование, где в константу присваивается значение равное имени этой константы. Когда констант у которых имена со значениями совпадает много, а таких случав вполне хватает, этот код убирает это дублирование.
MentalBlood
29.01.2023 09:53+1Многократно повышается вероятность ошибки опечаткой
Можно использовать
Pylance
вstrict
режиме и типизировать (type-hint
ить) ключи словаря объединениемLiteral
ов. Илиtyped dict
использовать. Илиdataclass
Dair_Targ
29.01.2023 10:00+1Рекомендую посмотреть в сторону имеющихся стандартных и околостандартных средств питона:
TypedDict позволяет описывать, что у такого-то словаря должен быть такой-то ключ с такими-то значениями
mypy (статическая проверка типов) позволяет п.1 проверять автоматом. Можно даже в IDE проверку на лету организовать.
Вменяемые IDE и language servers поддерживают как проверку, так и автодополнение кода с использованием п.1
Наконец для меня основной недостаток решения в том, что вместе с метаклассами и сокрытием строк получается код, которые себя ведёт неочевидным способом. При работе с кодовой базой с таким решением всегда придётся держать в голове, как оно работает. И всегда кто-то может "улучшить" метакласс - тогда нужно будет переучиваться :)
p.s. При написании кода лучше придерживаться pep8
break1 Автор
29.01.2023 10:06Каким же образом вам typedDict или mypy поможет избежать описанных мною кейсов с ошибками? Очевидно что никаким. Линтеру то какое дело, что вы там в строковых литералах храните.
Dair_Targ
29.01.2023 10:18Вы заявляли проблемы:
2) Многократно повышается вероятность ошибки опечаткой.<...>
3) Сложность поиска ошибок - Ни интерпретатор, ни линтеры нам не помогают такие ошибки найти. Если бы была константа subject_str содержащая "subject", то опечатавшись при упоминании константы мы получили бы ошибку от линтера <...>
4) Это неудобно поддерживать. Если в примере выше тег "subject" по какой-то причине придется заменить например на "topic" - <...>
Ровно эти проблемы связка TypedDict и mypy в случае работы со словарями покрывает, а это очень частый случай - Вы с него начали. Утверждение "Линтеру то какое дело, что вы там в строковых литералах храните" говорит о том, что Вы ни разу не пробовали использовать эту возможность.
break1 Автор
29.01.2023 10:23Ды Вы кажется сами не пользуетесь тем, что описали. У меня то линтер постоянно включен. Я еще раз повторю вопрос - каким образом типизированный словарь и линтер может покрыть вопрос опечатки внутри строкового литерала?
MentalBlood
29.01.2023 10:19+1При замене, например,
if "subject"
наif "subjec"
в следующем коде,pylance
подчеркнет строку красным и напишетExpression will always evaluate to True since the types "Literal['subjec']" and "Literal['subject', 'from', 'body', 'crc']" have no overlap
import copy import typing MessageKey = typing.Literal['subject'] | typing.Literal['from'] | typing.Literal['body'] | typing.Literal['crc'] Message = dict[MessageKey, typing.Any] def crc_code(body: typing.Any): pass def send_notification(respondents: list[typing.Any], message: Message) -> None: for resp in respondents: to_send = copy.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'])
break1 Автор
29.01.2023 10:33А если в разных файлах эти строковые литералы будут - что он скажет? Да и решение с описанием литералов, чтобы потом опять их везде писать как литералы и надеяться лишь на линтер - явно уступает строковым константам, даже в самом простом варианте.
Вы будете все исходники просматривать на предмет подчеркнул ли линтер красным строчку с очередным литералом или нет? И это вместо нормальной проверки интерпретатором в рантайме?MentalBlood
29.01.2023 10:42А если в разных файлах эти строковые литералы будут — что он скажет?
Он умеет работать сразу со всеме файлами в проекте (workspace). Главное указать соответствующие type-hintы
Вы будете все исходники просматривать
Нет, я напишу в конфиге
pylance
"python.analysis.diagnosticMode": "workspace"
и для перехода к следующей (в том числе первой) ошибке нажмуf8
. Еще кол-во "проблем" вvscode
пишется внизу слеваbreak1 Автор
29.01.2023 10:52-1То есть вы эти самые тайп хинты будете указывать заново в каждом файле, где такие же литералы используются, чтобы линтер их смог протестить?
А если другой разработчик без линтера исправит что-то в коде? Или с другими настройками линтера? Пусть ломается в рантайме? С каких пор линтер, который лишь вспомогательное средство разработчика имеет приоритет над контролем в рантайме.То есть зачем использовать линтер скармливая ему в каждом файле строковые тайп хинты, вместо использования строковых констант, которые в принципе не допускают вероятности ошибки, и никакие проверки строковых литералов линтером будут просто не нужны.
MentalBlood
29.01.2023 10:59То есть вы эти самые тайп хинты будете указывать заново в каждом файле, где такие же литералы используются, чтобы линтер их смог протестить
В случае приведенного кода я буду указывать
Message
как тип словаря. А то, что такоеMessage
, там написано, это можно вынести в отдельный файл и импортировать туда где нужно типизировать что-то какMessage
А если другой разработчик без линтера исправит что-то в коде?
Для решения таких проблем обычно настраивают и используют CI
P.S. Прямо в рантайме некоторые вещи можно валидировать с помощью
pydantic
, хотя он конечно не такой умный как линтерbreak1 Автор
29.01.2023 11:11-1Решение явно более проигрышное, можете это не признавать. Вы вместо отказа от дублирования строк - натравливаете систему проверки этого дублирования, которая попросту не нужна, если строки не дублируются, а берутся из констант. Кроме того, оно требует еще больших дополнительных манипуляций, чем вынесение строк в константы, особенно при поддержке, и если один из литералов будет переименован.
Survtur
29.01.2023 10:11Что-то я не понял, как будет выглядеть вот этот участок кода, если в нём применить ваше решение?
if "subject" not in to_send: to_send["subject"] = "Hello, " + str(resp)
Строки
"subject"
заменятся наC.subject
? Если да, то как это поможет поиску ошибок случайного различного написания? Или смысл только в том, чтобы при необходимости автопереименование поля сработало?break1 Автор
29.01.2023 10:26+2В смысле как поможет - эти ошибки сможет найти интерпретатор если вы опечатаетесь. И линтер вам подсветит такую опечатку. Ведь переменной с именем c.subjectt - не будет существовать, в то время как в строковом литерале вы можете написать что угодно ("subjectt"), но при этом ни интерпретатор, ни линтер вам никак не помогут.
Survtur
29.01.2023 10:34Виноват-с. Глядя на кусочки кода приведённые в статье я подумал, что значение С.subject иницилизируется при первом обращении. Соотетственно я подумал, что и С.subjectt тоже просто инициализировалось бы.
А если смысл не в этом, то, почему было не оставить обычный enum... Ну ОК.break1 Автор
29.01.2023 10:45C.subject инициализируется при создании класса в котором оно будет описано. В дальнейшем при любом обращении оно уже содержит свое значение равное имени атрибута.
Обычный энам - в качестве значений имеет инты, можно использовать готовый строковый энам из 3.11 или дописать функцию автогенерации значений для более ранних версий - тогда функциональность будет аналогичной. Пример был приведен одним из первых комментаторов.
dakuan
29.01.2023 14:45Зачем вы перескочили с одного примера на другой?
В примере с цветами целесообразность использования чего-то типа enum очевидна. Например, потому что цвет можно использовать в качестве параметра функции. В то время как в примере с subject - не совсем. Скорее всего "subject", "from" - это внутренние константы, которые кроме send_notification нигде больше не используются. Т.е. вполне возможно, что вам придется часто менять цвет с белого на черный, с черного на красный и т.д. Но какова вероятность, что придется менять "subject" на "from" или "from" на "crc"?
Логично было бы показать как будет выглядеть проблемный кусок кода с использованием вашего подходом.
Z55
29.01.2023 10:17+3Вы сходу привели пример "не оптимального" кода. Было бы логично, в конце статьи (ну или где-то ещё), показать переработанный вариант этого кода. Добавите?
break1 Автор
29.01.2023 10:29Можно сделать, но я привел другие примеры, думал будет и так понятно. Описываем строковые константы "subject", "body", "from" и т.д. предложенным способом и далее используем их вместо строковых литералов. c.subject, c.body, c.from и т.д.
Просто в моем примере это цвета. Читаемость куска кода приведенного в начале статьи никак не ухудшится с применением констант.
shpaker
29.01.2023 10:20+1Конечно получше, но все таки остается дурацкое дублирование в описание класса, которое в 90% случаев будет именно таким и будет мозолить глаза
Вкусовщина. Пока 3.11 не используется на проекте - можно и потерпеть.
а также является местом для ошибки, хотя и вероятность ее сильно сокращается.
Прям сильно вытянуто из пальца если честно.
Надо использовать метаклассы - так я сказал своему коллеге и он через 5 мин выдал простое и логичное решение
У меня бы оба бы сразу по ушам за такое получили и таску на выпиливание этого простого и логичного чуда. Ну и для статьи это мелковато - возможно вы там оба открываете для себя питон, и столкнулись с чудным миром языка идеально подходящего для написания велосипедов всех сортов, но кажется и тут мимо кассы. Я понимаю еще, если бы вы довели свое решение до какой-то завершенности. Например, написали бы пакетик, который бы работал как StrEnum, только для питонов от 3.8 до 3.11... Но нужно ли реально оно кому-то? Судя по комментам выше - нет. Но возможно, что само решение под таким углом заиграло бы другими красками.
break1 Автор
29.01.2023 10:39+1Людям свойственно отвергать очевидные вещи. Я не считаю своей целью донести очевидное до каждого. По комментариям так же видно, что люди не отличают линтер, что является лишь вспомогательным средством проверки для разработчика от рантайма.
Мой простенький велосипед выполняет лишь одну функцию - заполняет атрибут значением равным его имени, ровно то же делает StrEnum в 3.11, но с присущим ему оверхедом т.к. в нем есть все от Enum. Но тот вариант вы врят ли столь настырно будете критиковать и называть велосипедом - хоть там метаклассы будут, хоть что-то еще, ведь это стандартная библиотека и ее велосипеды - это норма.Survtur
29.01.2023 10:44+1Людям свойственно отвергать очевидные вещи.
Эта фраза очевидно некорректна.
break1 Автор
29.01.2023 10:55Почему? А фраза, что кто-то кому-то там даст по ушам за какое-то там чудо - корректна полностью? )))
Survtur
29.01.2023 12:35Потому что людям несвойственно отвергать очевидные вещи.
break1 Автор
29.01.2023 14:25Так люди не отвергали, что земля не плоская, что люди имеющие разный цвет кожи должны иметь разные права (или другие формы рабства в разных государствах), а свободные выборы или медицину люди никогда не отвергали? - разве все это вам не кажется очевидным?
druidvav
29.01.2023 11:36Да, единственное адекватное решение было бы "бекпортирование" StrEnum. И функционал бы получили и решили бы проблему на будущее. Я вот не питонист, но нашел уже готовые решения на эту тему по запросу "strenum backport". впрочем, удачи топикстартеру, с таким отношением к критике далеко пойдет.
break1 Автор
29.01.2023 11:50-3У топикстартера нормальное отношение к критике. Иметь собственное мнение и отстаивать его, разумно аргументируя - это нормально, особенно, если пытаются критиковать люди не разобравшиеся в вопросе. Не питонисты, не программисты и так далее )))
druidvav
29.01.2023 11:52+2Ну вы свои инсинуации оставьте при себе, правда. Вам здесь пишут люди с явно большим опытом разработки. А на каком языке — совершенно не имеет значения, что подсказывают и другие комментаторы.
break1 Автор
29.01.2023 12:02Откуда у вас инфо об чьем-то опыте разработки? Слово явно - тут точно не уместно... )))
vadimr
29.01.2023 10:59+1Когда мы видим в программе строки:
if "subject" not in to_send: to_send["subject"] = "Hello, " + str(resp)
то с одного взгляда понятно, что здесь происходит.
А с вашим метаклассом будет будущий программист сидеть и думать, что это такое и зачем оно было здесь наворочено, вместо того, чтобы заниматься своей непосредственной задачей. Это называется “некуда девать фонд оплаты труда”.
break1 Автор
29.01.2023 11:04Да и в чем разница?
if c.subject not in to_send: to_send[c.subject] = "Hello, " + str(resp)
Так страшно наворочено, что аж не понять... Бедный программист. Фонд оплаты труда весь уйдет на разбирательство - что здесь и к чему...
А вообще странно, что вы ставите на первое место чтобы код был понятен недопрограммистам, вместо того, чтобы он содержал минимальное число ошибок и опасных мест.
Не думаете ли вы, что во всех либах, которые вы используете нет метаклассов? Кто-то их когда-то написал, и ничего все пользуются, никто фонд оплаты труда так и не исчерпал.vadimr
29.01.2023 11:11Разница в том, что надо ещё понять (а сделать это можно, только проследив по всему тексту), что c.subject остаётся неизменным в ходе выполнения программы. Или надо верить документации, что чревато.
А вообще странно, что вы ставите на первое место чтобы код был понятен недопрограммистам, вместо того, чтобы он содержал минимальное число ошибок и опасных мест.
Критикуемый вами код не содержит ни ошибок, ни опасных мест.
Ваше утверждение о том, что можно опечататься в литерале, не имеет под собой рациональных оснований. Опечататься можно в любом месте программы. Например, написать s.subject вместо c.subject.
Не думаете ли вы, что во всех либах, которые вы используете нет метаклассов?
Всякому инструменту своё место.
druidvav
29.01.2023 11:18+1я конечно не настоящий питонист, но такие вот литералы обычно используются не для полей и тут в условном c.subject я бы ожидал сам собственно сабжект, а не литерал со значением сабжект. те языки с каким я работал литералы принимают для значений, а не для ключей.
vadimr
29.01.2023 11:23Ну, в питоне никто не мешает проиндексировать строками. Другое дело, что это сама по себе гораздо более вольная практика кодирования, чем повторение литералов.
break1 Автор
29.01.2023 11:23+2Если вы опечатаетесь в имени переменной - вас спасет и линтер и интерпретатор выдав вам ясное и понятное сообщение об ошибке. Если вы ошиблись в строковом литерале - вы эту ошибку вообще никогда можете не увидеть, просто ваша программа работать не будет.
А по моему нажав F2 на c.subject я попаду в место его описания и сразу увижу - что это строковая константа. И если вы читали текст статьи - то должны были видеть, что приведен пример дополнения кода запрещающего редактировать атрибуты класса справочника констант. А вот в случае со строковыми литералами надо действительно ползать по всем утексту и проверять, что они все написаны одинаково. Это вообще такая очевидная вещь, что мне странно ее обсуждать.
Аргументация в том, что верить документации чревато - вообще - мое почтение. Наверное сторонние либы вы тоже не по документации изучаете, а сразу в исходники и дебажить, что там и где и как происходит.
Мое утверждение, что можно опечататься в строковом литерале подтверждено как минимум проверкой дублирующихся строковых литералов в SonarCube - ссылка в тексте. Не я один считаю такие места потенциально опасными.vadimr
29.01.2023 11:27Аргументация в том, что верить документации чревато - вообще - мое почтение. Наверное сторонние либы вы тоже не по документации изучаете, а сразу в исходники и дебажить, что там и где и как происходит.
Вообще-то рано или поздно обычно доходит до этого при углублённом использовании.
Скажу страшную вещь, и ошибки в компиляторах доводилось находить при отладке программ.
break1 Автор
29.01.2023 11:46+1Что-то вы из из крайности в крайность... То вам не ясно, что написано в коде, который отличается буквально на пару символов, то ошибки в компиляторах исправляете...
vadimr
29.01.2023 11:54Это не из крайности в крайность, а большой практический опыт. Верить нельзя никому (мне – можно). Именно потому, что ошибки могут быть везде, начиная с компилятора и уж точно включая весь пользовательский код, смысл кода может оказаться далеко не так очевиден, как может показаться на первый взгляд, и поэтому не надо код переусложнять. KISS.
В частности, ваше F2 в IDE – тоже не панацея, особенно для такого языка, как питон, где код может динамически вычисляться. Доверие к результату F2 – тоже плод некоторого априорного доверия к соглашениям о кодировании. Из всего огромного множества которых вы почему-то отнеслись с опасением только к воспроизводимости литералов.
break1 Автор
29.01.2023 11:59Это уже просто разговор ни о чем... F2 было приведено условно. Думаю адекватный программист найдет откуда импортится строковый литерал и увидит его значение. Вероятность ошибок где бы то ни было не дает право повсеместно дублировать то, что дублировать не нужно и опасно - строковые литералы. Почему вы меня пытаетесь убедить в обратном - убеждайте разработчикомвстатического анализатора кода - во многих из них эти проверки на дублирования строк есть.
invasy
29.01.2023 11:48недопрограммистам
Как вы лихо всех коллег описали.
Код пишется в основном для чтения другими разработчиками. Чем он понятнее, тем легче его поддерживать. Мы ведь о промышленной разработке говорим? Без олимпиадных задач, какой-то хитрой оптимизации (питон ведь) и других случаев write-only code.
break1 Автор
29.01.2023 11:54Приведенный мною пример читается ничуть не хуже оригинала. Добавляется всего несколько символов. Вместо строкового литерала по месту - переменная член класса. Если человек не может этого понять - пожалуй это недопрограммист. Бесконечное дублирование в том числе строковых констант - мало чем отличается от указания переменной в этих же самых местах с аналогичным именем.
Поддержка кода с дублями строк куда сложнее, чем его поддержка со словарем констант. Я привел конкретные кейсы в статье. Спорить с ними - на врят ли имеет смысл.invasy
29.01.2023 12:13Давайте развернём выпад в другую сторону: если человек не может понять, что «бесспорной» проблемы дублирования строковых литералов нет, то он считается недопрограммистом?
break1 Автор
29.01.2023 12:18Так она есть или нет? Зачем она есть в SonarCube? Разверните выпад в их сторону например, а не в мою. Я лишь описал вариант решения, которым пользуюсь я, т.к. я не считаю, что такой проблемы нет.
invasy
29.01.2023 12:21Прочитайте внимательно правило и исключения из него.
break1 Автор
29.01.2023 12:32Почему решили, что я не читал? Эта ссылка вообще то в статье и есть ))
Так проблемы нет, или она есть и есть исключения?invasy
29.01.2023 12:53+2В общем случае проблема есть. В приведённом в статье «куске кода» её нет. Строковые литералы могут быть предназначены для разных целей. Ключи в словарях, идентификаторы и т. п. проблемой не являются и записаны в исключения правила SonarQube. Для избежания опечаток можно воспользоваться type hints и статическими анализаторами (предложено в комментариях). Если литералы используются для других целей (regex, сообщения, вывода текста пользователю), для них могут применяться строковые перечисления из самых первых комментов. Или
gettext
и подобные способы i18n/l10n.В статье не обоснована проблема, приведён некорректный пример, предложено неоптимальное решение.
break1 Автор
29.01.2023 12:59Нормальные примеры в статье, проблема обоснована, решение тоже нормальное предложено.
У меня нет цели достучаться до каждого. Вы число ПИ константой юзаете или каждый раз в коде пишите 3.1415926.... ? ))) Оно же неизменно, достаточно везде без ошибки написать - зачем его в константы выносят во всех языках и мат либах? )))break1 Автор
29.01.2023 14:17А что же вам мешает его писать везде с одной и той же точностью, достаточной для ваших вычислений? Вы же вместо этого наверняка задефайнили в одном месте и используете константу оттуда, или из стандартной матч либы. А меня убеждаете, что со строками это и не нужно и проблемы такой нет, хотя строки еще более подвержены ошибкам неправильного написания.
invasy
29.01.2023 13:07А теперь уже совсем неинтересно. Аргументации как не было, так и нет. Ещё и непоследовательность в рассуждениях.
Сочувствую вашим коллегам.
break1 Автор
29.01.2023 14:06До того как вы соизволили прочитать ссылку на Sonar вы вообще утверждали, что такой проблемы нет. Теперь у вас не хватило воображения представить, что в коротких именах с цветами могут быть стрококвые литеры любой длины и любого содержания. И все равно непоследовательность рассуждений и отсутствие аргументации у меня )))
А если я вам дам ссылку на статический анализатор, который проверяет и считает ошибкой повторения строк любой длины - вы уже начнете топить и сами за строковые константы? ))
Вы не расстраивайтесь - и помните - первая стадия - отрицание!
invasy
29.01.2023 14:28Давайте ещё раз разберёмся. В примере из статьи проблемы дублирования строковых литералов нет. Об этом и писал.
Если для ключей простого словаря используются сложные литералы, стоит подумать над логикой программы, всё ли правильно реализовано. Если действительно есть необходимость в сложных ключах и поиска по хэшам, проблемы их дублирования, как правило, нет. Такие ключи обычно динамически создаются и литералы редко используются.
В любом случае, в статье построена ветряная мельница, с которой вы зачем-то героически боретесь.
alixa
29.01.2023 11:32Думаю вопрос не только к Python и касается не только строк. Пока вижу решение только в константах, например в одном из моих проектов в коде БД на T-SQL, есть таблица с сущностями, в коде используются ID.
Для каждого ID в таблице при создании новой сущности генерируется функция, функция потом используется в коде.
Примерно так
WHERE o.ID = Entities.Объект()
Функция возвращает число.
create function Entities.Объкт() as
Return 85
Код становиться читабельней, не надо вспоминать и смотреть каждый раз ID сущности.
В других платформах например Android от Google, генерируются классы с константами, этот подход уже как паттерн, возможно даже название уже придумано.
ertaquo
29.01.2023 13:00Вот из-за этого я не люблю Python - из-за тех, кто пишет такой код. А потом сиди и ковыряй чужие исходники, пытаясь узнать, что вообще должно быть в этих dict'ах.
Ну йо, есть же dataclass'ы! Почему бы не использовать их?
from dataclasses import dataclass from typing import Optional, List @dataclass class Message: body: str from_: Optional[str] = None subject: Optional[str] = None @property def crc(self): return crc_code(body) def send_notification(respondents: List[str], message: Message) -> None: for resp in respondents: to_send = Message( from_ = message.from_, subject = message.subject or ("Hello, %s" % resp), body = message.body.replace("@respondent@", resp) )
И сразу пропадает проблема и с дубликатами строк, и с типизацией полей, и с подсказками в IDE.
А ещё можно использовать pydantic - с ним можно делать мутабельные поля и ещё много всякого удобного.
break1 Автор
29.01.2023 13:08Это лишь частный случай, он никак не отменяет в принципе строковые константы.
skywalk7
Color = StrEnum('Color', ['RED', 'GREEN', 'BLUE'])
break1 Автор
Хороший вариант, начиная с 3.11 в стандартной библиотеке.
EdwardBatraev
Это прямо из доков питона.
Работает как минимум с 3.8.
break1 Автор
Да, хороший вариант.
Lauarvik
с Python 3.7.9 тоже работает