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

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)


  1. skywalk7
    29.01.2023 07:08
    +7

    Color = StrEnum('Color', ['RED', 'GREEN', 'BLUE'])


    1. break1 Автор
      29.01.2023 07:38
      +1

      Хороший вариант, начиная с 3.11 в стандартной библиотеке.


      1. EdwardBatraev
        29.01.2023 09:06
        +8

        from enum import Enum, auto
        
        class AutoName(Enum):
            def _generate_next_value_(name, start, count, last_values):
                return name
        
        class Color(str, AutoName):
            red = auto()
            blue = auto()
            green = auto()
        
        print(set(Color))
        
        print(Color.blue == "blue")
        


        Это прямо из доков питона.
        Работает как минимум с 3.8.


        1. break1 Автор
          29.01.2023 09:24

          Да, хороший вариант.


        1. Lauarvik
          29.01.2023 14:10

          с Python 3.7.9 тоже работает


  1. druidvav
    29.01.2023 07:25
    +15

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


    1. break1 Автор
      29.01.2023 07:40
      +1

      В чем муть и что надо поддерживать? Неужели код метакласса на 3 строчки кода это так сложно?


      1. druidvav
        29.01.2023 07:43
        +6

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


        1. 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 = Color

          print( C.msg + C.red )

          Или вы можете использовать только то, что написано в стандартных или сторонних либах, оправдывая это тем, что поддерживать будет кто-то другой?

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


          1. druidvav
            29.01.2023 07:55
            +6

            Если что-то можно реализовать стандартными средствами — нужно реализовать стандартными средствами.

            Проблемы в дублировании строк даже и нет как таковой, проблема надумана и вы сами это понимаете судя по оговоркам в статье.


            1. break1 Автор
              29.01.2023 07:57
              -6

              Какими оговорками? Вы ее хоть читали? Или просто поспорить ни о чем есть большое желание? Подробно описал в чем проблемы с дублированием строк. Спорить с этим просто не имеет смысла.

              Стандартное решение есть в 3.11, для меня на большинстве проектов доступен пока только 3.9. Можно поиграться с модификацией стандартного энама (что и сделано в 3.11 с наследованием от str и переопределением функции генерации следующего значения), но очевидно лично вам это покажется еще сложнее чем метакласс на 3 строчки кода. Это ведь тоже надо будет поддерживать.


              1. druidvav
                29.01.2023 08:02
                +9

                Зачем решать проблему? у вас 1 пункт вообще не проблема, 2, 3, 4 решаются иде, а если не нравится решение иде — есть решение стандартным функционалом которое вы сами же и привели. Потом, когда перейдете на 3.11 — переделаете на новое стандартное решение.

                Поменьше агрессии, а то так можно и инфаркт на код-ревью получить.

                Предложенное вами "решение" это яркий пример overengineering, почитайте на досуге что это такое


                1. break1 Автор
                  29.01.2023 08:06
                  -2

                  Агрессии? С моей стороны ее вообще нет. 2,3,4 - не решаются IDE, что я подробно и описал.
                  До 3.11 какое есть стандартное решение?


                  1. druidvav
                    29.01.2023 08:09
                    +3

                    То самое которое вы описали "Конечно получше, но все таки остается дурацкое дублирование в описание класса".

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


                    1. break1 Автор
                      29.01.2023 08:15

                      Я привел пример с 3 цветами для простоты, а в реальных проектах у меня сотни строковых констант. И они отлично мозолят глаза тупым копированием имени переменной в ее значение. И когда эта простыня занимает весь экран (точнее несколько экранов) это очень неудобно поддерживать, гораздо сложнее, чем когда справа будет одно и то же значение "init_copy" или "auto()". Если бы этой проблемы не было, никто бы и не думал вводить StrEnum - который по сути делает примерно то же, да и обычные энамы зачем с автогенерацией значений, можно было бы везде заменить старым добрым присвоением в каждый элемент.


                      1. druidvav
                        29.01.2023 08:21
                        +2

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


                      1. break1 Автор
                        29.01.2023 08:31
                        -4

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

                        Внутри StrEnum делается ровно то же, что и внутри StrProps_Meta - а именно в атрибут присваивается значение равное имени атрибута. Но в одном случае это почему то велосипед, а в другом сахар ))))


                      1. invasy
                        29.01.2023 11:12
                        +1

                        Давайте попробуем разобраться.


                        StrEnum:


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

                        Ваше решение:


                        • не имеет стабильного интерфейса, будет жить и поддерживаться только при вашем желании, зависит от знаний возможностей языка конкретного разработчика;
                        • дублирует функциональность из стандартной библиотеки — велосипед;
                        • появилось для неочевидной проблемы: она выглядит надуманной и действительно решается другими инструментами, перечисленными в статье.

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


                      1. break1 Автор
                        29.01.2023 11:40

                        StrEnum нет в питон до 3.11, кроме того, наличие класса в стандартной библиотеке еще не означает, что его поведение не будет меняться с версиями интерпретатора. Собсвтенно из документации на тот самый метод автогенерации значений:

                        Note 

                        The goal of the default _generate_next_value_() method is to provide the next int in sequence with the last int provided, but the way it does this is an implementation detail and may change.

                        ой... Может быть изменен...

                        По поводу синтаксический сахар или нет - я уже высказался. По поводу стабильности интерфейса - у вас что проект без классов, которые есть только в ваших проектах и поддерживаются только вами?
                        Лично мне вообще без разницы что будет использовано в проекте справочник с метаклассом или энам. Энам вносит оверхед. Класс с метаклассом проще. Использовать можно и то и то. Главное избавляться от дублирования строк.

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


                      1. invasy
                        29.01.2023 12:01
                        +1

                        отрицают проблему дублирования

                        Может, её и нет?


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


                        Заменять литералы объектами в принципе оверхед и оверинжениринг в этой ситуации (как замечено выше в комментах).


                      1. break1 Автор
                        29.01.2023 12:10
                        -1

                        Ну вообще в питоне все объект, если что. И просто строка тоже.
                        Если проблемы дублирования строк нет - почему она есть в SonarCube?


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


          1. shpaker
            29.01.2023 09:51
            +1

            Енамы с 3.8 (об этом уже писали выше) нормально будут работать. Велкам ту class SomeEnum(str, Enum).


            1. break1 Автор
              29.01.2023 09:58

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


              1. shpaker
                29.01.2023 10:07
                +1

                Да понял. Ступил. Но в любом енаме питона всегда можно читать ключи как строки.


      1. druidvav
        29.01.2023 07:47
        +3

        Вы кроме принципа SOLID забыли очень важный принцип KISS.


        1. break1 Автор
          29.01.2023 07:56
          -1

          Я про SOLID вообще ничего не говорил. Оправдывать любое дублирование в коде одним лишь KISS слабая аргументация.


          1. druidvav
            29.01.2023 07:57
            +2

            вы в коде очень много раз продублировали if, сделаете макрос, чтобы не дублировать if?


            1. break1 Автор
              29.01.2023 08:07

              Я вижу только 2 ифа в коде, каждый нужен на своем месте и это не дублирование.


              1. druidvav
                29.01.2023 08:10

                Ровно столько же упоминаний subject и from :)


                1. break1 Автор
                  29.01.2023 08:23
                  +2

                  Ну если вам не видна разница между операторами языка и строковыми константами, которые вводит программист пишущий на этом языке, то вам я ничего не докажу.


                  1. pfffffffffffff
                    29.01.2023 09:28

                    Вообще ваш словарь выполняет функцию объекта у которого эти свойства и должны быть описаны. Использовать константы для имен полей это глупость. Это должно быть инкапсулировано в адаптере.


                  1. druidvav
                    29.01.2023 10:38
                    +1

                    Это конечно совершенно не в тему замечание, но обсессивная борьба с «повторами» она иррациональна.


  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 укажет на ошибку.


    1. break1 Автор
      29.01.2023 08:34
      +1

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


      1. shpaker
        29.01.2023 09:54

        Почему вынос литералов в константы вам не угоден в питоне?


        1. break1 Автор
          29.01.2023 10:04

          В питоне это можно улучшить. Это везде не удобно до конца, но там улучшить это нельзя.


          1. shpaker
            29.01.2023 10:10
            +2

            Улучшение это когда что-то становится лучше, а у вас просто какая-то странная штука которая лучше то ничего и не делает.


            1. break1 Автор
              29.01.2023 10:21

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


  1. MentalBlood
    29.01.2023 09:53
    +1

    Многократно повышается вероятность ошибки опечаткой

    Можно использовать Pylance в strict режиме и типизировать (type-hintить) ключи словаря объединением Literalов. Или typed dict использовать. Или dataclass


    1. break1 Автор
      29.01.2023 10:07

      Не очень ясно как это применить по теме статьи.


  1. Dair_Targ
    29.01.2023 10:00
    +1

    Рекомендую посмотреть в сторону имеющихся стандартных и околостандартных средств питона:

    1. TypedDict позволяет описывать, что у такого-то словаря должен быть такой-то ключ с такими-то значениями

    2. mypy (статическая проверка типов) позволяет п.1 проверять автоматом. Можно даже в IDE проверку на лету организовать.

    3. Вменяемые IDE и language servers поддерживают как проверку, так и автодополнение кода с использованием п.1

    Наконец для меня основной недостаток решения в том, что вместе с метаклассами и сокрытием строк получается код, которые себя ведёт неочевидным способом. При работе с кодовой базой с таким решением всегда придётся держать в голове, как оно работает. И всегда кто-то может "улучшить" метакласс - тогда нужно будет переучиваться :)

    p.s. При написании кода лучше придерживаться pep8


    1. break1 Автор
      29.01.2023 10:06

      Каким же образом вам typedDict или mypy поможет избежать описанных мною кейсов с ошибками? Очевидно что никаким. Линтеру то какое дело, что вы там в строковых литералах храните.


      1. Dair_Targ
        29.01.2023 10:18

        Вы заявляли проблемы:

        2) Многократно повышается вероятность ошибки опечаткой.<...>

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

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

        Ровно эти проблемы связка TypedDict и mypy в случае работы со словарями покрывает, а это очень частый случай - Вы с него начали. Утверждение "Линтеру то какое дело, что вы там в строковых литералах храните" говорит о том, что Вы ни разу не пробовали использовать эту возможность.


        1. break1 Автор
          29.01.2023 10:23

          Ды Вы кажется сами не пользуетесь тем, что описали. У меня то линтер постоянно включен. Я еще раз повторю вопрос - каким образом типизированный словарь и линтер может покрыть вопрос опечатки внутри строкового литерала?


      1. 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'])


        1. break1 Автор
          29.01.2023 10:33

          А если в разных файлах эти строковые литералы будут - что он скажет? Да и решение с описанием литералов, чтобы потом опять их везде писать как литералы и надеяться лишь на линтер - явно уступает строковым константам, даже в самом простом варианте.
          Вы будете все исходники просматривать на предмет подчеркнул ли линтер красным строчку с очередным литералом или нет? И это вместо нормальной проверки интерпретатором в рантайме?


          1. MentalBlood
            29.01.2023 10:42

            А если в разных файлах эти строковые литералы будут — что он скажет?

            Он умеет работать сразу со всеме файлами в проекте (workspace). Главное указать соответствующие type-hintы


            Вы будете все исходники просматривать

            Нет, я напишу в конфиге pylance "python.analysis.diagnosticMode": "workspace" и для перехода к следующей (в том числе первой) ошибке нажму f8. Еще кол-во "проблем" в vscode пишется внизу слева


            1. break1 Автор
              29.01.2023 10:52
              -1

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

              А если другой разработчик без линтера исправит что-то в коде? Или с другими настройками линтера? Пусть ломается в рантайме? С каких пор линтер, который лишь вспомогательное средство разработчика имеет приоритет над контролем в рантайме.

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


              1. MentalBlood
                29.01.2023 10:59

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

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


                А если другой разработчик без линтера исправит что-то в коде?

                Для решения таких проблем обычно настраивают и используют CI


                P.S. Прямо в рантайме некоторые вещи можно валидировать с помощью pydantic, хотя он конечно не такой умный как линтер


                1. break1 Автор
                  29.01.2023 11:11
                  -1

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


  1. Survtur
    29.01.2023 10:11

    Что-то я не понял, как будет выглядеть вот этот участок кода, если в нём применить ваше решение?

    if "subject" not in to_send:
        to_send["subject"] = "Hello, " + str(resp)

    Строки "subject" заменятся на C.subject ? Если да, то как это поможет поиску ошибок случайного различного написания? Или смысл только в том, чтобы при необходимости автопереименование поля сработало?


    1. break1 Автор
      29.01.2023 10:26
      +2

      В смысле как поможет - эти ошибки сможет найти интерпретатор если вы опечатаетесь. И линтер вам подсветит такую опечатку. Ведь переменной с именем c.subjectt - не будет существовать, в то время как в строковом литерале вы можете написать что угодно ("subjectt"), но при этом ни интерпретатор, ни линтер вам никак не помогут.


      1. Survtur
        29.01.2023 10:34

        Виноват-с. Глядя на кусочки кода приведённые в статье я подумал, что значение С.subject иницилизируется при первом обращении. Соотетственно я подумал, что и С.subjectt тоже просто инициализировалось бы.

        А если смысл не в этом, то, почему было не оставить обычный enum... Ну ОК.


        1. break1 Автор
          29.01.2023 10:45

          C.subject инициализируется при создании класса в котором оно будет описано. В дальнейшем при любом обращении оно уже содержит свое значение равное имени атрибута.

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


          1. Survtur
            29.01.2023 12:27

            [DEL]


      1. dakuan
        29.01.2023 14:45

        Зачем вы перескочили с одного примера на другой?
        В примере с цветами целесообразность использования чего-то типа enum очевидна. Например, потому что цвет можно использовать в качестве параметра функции. В то время как в примере с subject - не совсем. Скорее всего "subject", "from" - это внутренние константы, которые кроме send_notification нигде больше не используются. Т.е. вполне возможно, что вам придется часто менять цвет с белого на черный, с черного на красный и т.д. Но какова вероятность, что придется менять "subject" на "from" или "from" на "crc"?
        Логично было бы показать как будет выглядеть проблемный кусок кода с использованием вашего подходом.


  1. Z55
    29.01.2023 10:17
    +3

    Вы сходу привели пример "не оптимального" кода. Было бы логично, в конце статьи (ну или где-то ещё), показать переработанный вариант этого кода. Добавите?


    1. break1 Автор
      29.01.2023 10:29

      Можно сделать, но я привел другие примеры, думал будет и так понятно. Описываем строковые константы "subject", "body", "from" и т.д. предложенным способом и далее используем их вместо строковых литералов. c.subject, c.body, c.from и т.д.
      Просто в моем примере это цвета. Читаемость куска кода приведенного в начале статьи никак не ухудшится с применением констант.


      1. Survtur
        29.01.2023 10:37

        [del]


  1. shpaker
    29.01.2023 10:20
    +1

    Конечно получше, но все таки остается дурацкое дублирование в описание класса, которое в 90% случаев будет именно таким и будет мозолить глаза

    Вкусовщина. Пока 3.11 не используется на проекте - можно и потерпеть.

    а также является местом для ошибки, хотя и вероятность ее сильно сокращается.

    Прям сильно вытянуто из пальца если честно.

    Надо использовать метаклассы - так я сказал своему коллеге и он через 5 мин выдал простое и логичное решение

    У меня бы оба бы сразу по ушам за такое получили и таску на выпиливание этого простого и логичного чуда. Ну и для статьи это мелковато - возможно вы там оба открываете для себя питон, и столкнулись с чудным миром языка идеально подходящего для написания велосипедов всех сортов, но кажется и тут мимо кассы. Я понимаю еще, если бы вы довели свое решение до какой-то завершенности. Например, написали бы пакетик, который бы работал как StrEnum, только для питонов от 3.8 до 3.11... Но нужно ли реально оно кому-то? Судя по комментам выше - нет. Но возможно, что само решение под таким углом заиграло бы другими красками.


    1. break1 Автор
      29.01.2023 10:39
      +1

      Людям свойственно отвергать очевидные вещи. Я не считаю своей целью донести очевидное до каждого. По комментариям так же видно, что люди не отличают линтер, что является лишь вспомогательным средством проверки для разработчика от рантайма.
      Мой простенький велосипед выполняет лишь одну функцию - заполняет атрибут значением равным его имени, ровно то же делает StrEnum в 3.11, но с присущим ему оверхедом т.к. в нем есть все от Enum. Но тот вариант вы врят ли столь настырно будете критиковать и называть велосипедом - хоть там метаклассы будут, хоть что-то еще, ведь это стандартная библиотека и ее велосипеды - это норма.


      1. Survtur
        29.01.2023 10:44
        +1

        Людям свойственно отвергать очевидные вещи.

        Эта фраза очевидно некорректна.


        1. break1 Автор
          29.01.2023 10:55

          Почему? А фраза, что кто-то кому-то там даст по ушам за какое-то там чудо - корректна полностью? )))


          1. druidvav
            29.01.2023 11:32

            по ушам он дать может и в этом он корректен. а вот "Людям свойственно отвергать очевидные вещи." это ложь.


            1. break1 Автор
              29.01.2023 11:48

              Да, а не отвергали ли люди, что земля круглая? Ложь? )))) Сотни примеров ))) Да с теми же строковыми литералами.


              1. druidvav
                29.01.2023 11:50

                Я вам настоятельно советую успокоиться. Можете, например, почитать что значит очевидный. Земля, кстати, не круглая.


                1. break1 Автор
                  29.01.2023 12:05

                  А почему вы мне что-то там решили советовать? Опять мои аргументы оказались сильнее?)


          1. Survtur
            29.01.2023 12:35

            Потому что людям несвойственно отвергать очевидные вещи.


            1. break1 Автор
              29.01.2023 14:25

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


    1. druidvav
      29.01.2023 11:36

      Да, единственное адекватное решение было бы "бекпортирование" StrEnum. И функционал бы получили и решили бы проблему на будущее. Я вот не питонист, но нашел уже готовые решения на эту тему по запросу "strenum backport". впрочем, удачи топикстартеру, с таким отношением к критике далеко пойдет.


      1. break1 Автор
        29.01.2023 11:50
        -3

        У топикстартера нормальное отношение к критике. Иметь собственное мнение и отстаивать его, разумно аргументируя - это нормально, особенно, если пытаются критиковать люди не разобравшиеся в вопросе. Не питонисты, не программисты и так далее )))


        1. druidvav
          29.01.2023 11:52
          +2

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


          1. break1 Автор
            29.01.2023 12:02

            Откуда у вас инфо об чьем-то опыте разработки? Слово явно - тут точно не уместно... )))


  1. vadimr
    29.01.2023 10:59
    +1

    Когда мы видим в программе строки:

    if "subject" not in to_send:
                to_send["subject"] = "Hello, " + str(resp)

    то с одного взгляда понятно, что здесь происходит.

    А с вашим метаклассом будет будущий программист сидеть и думать, что это такое и зачем оно было здесь наворочено, вместо того, чтобы заниматься своей непосредственной задачей. Это называется “некуда девать фонд оплаты труда”.


    1. break1 Автор
      29.01.2023 11:04

      Да и в чем разница?

      if c.subject not in to_send: to_send[c.subject] = "Hello, " + str(resp)

      Так страшно наворочено, что аж не понять... Бедный программист. Фонд оплаты труда весь уйдет на разбирательство - что здесь и к чему...

      А вообще странно, что вы ставите на первое место чтобы код был понятен недопрограммистам, вместо того, чтобы он содержал минимальное число ошибок и опасных мест.

      Не думаете ли вы, что во всех либах, которые вы используете нет метаклассов? Кто-то их когда-то написал, и ничего все пользуются, никто фонд оплаты труда так и не исчерпал.


      1. vadimr
        29.01.2023 11:11

        Разница в том, что надо ещё понять (а сделать это можно, только проследив по всему тексту), что c.subject остаётся неизменным в ходе выполнения программы. Или надо верить документации, что чревато.

        А вообще странно, что вы ставите на первое место чтобы код был понятен недопрограммистам, вместо того, чтобы он содержал минимальное число ошибок и опасных мест.

        Критикуемый вами код не содержит ни ошибок, ни опасных мест.

        Ваше утверждение о том, что можно опечататься в литерале, не имеет под собой рациональных оснований. Опечататься можно в любом месте программы. Например, написать s.subject вместо c.subject.

        Не думаете ли вы, что во всех либах, которые вы используете нет метаклассов?

        Всякому инструменту своё место.


        1. druidvav
          29.01.2023 11:18
          +1

          я конечно не настоящий питонист, но такие вот литералы обычно используются не для полей и тут в условном c.subject я бы ожидал сам собственно сабжект, а не литерал со значением сабжект. те языки с каким я работал литералы принимают для значений, а не для ключей.


          1. vadimr
            29.01.2023 11:23

            Ну, в питоне никто не мешает проиндексировать строками. Другое дело, что это сама по себе гораздо более вольная практика кодирования, чем повторение литералов.


        1. break1 Автор
          29.01.2023 11:23
          +2

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

          А по моему нажав F2 на c.subject я попаду в место его описания и сразу увижу - что это строковая константа. И если вы читали текст статьи - то должны были видеть, что приведен пример дополнения кода запрещающего редактировать атрибуты класса справочника констант. А вот в случае со строковыми литералами надо действительно ползать по всем утексту и проверять, что они все написаны одинаково. Это вообще такая очевидная вещь, что мне странно ее обсуждать.

          Аргументация в том, что верить документации чревато - вообще - мое почтение. Наверное сторонние либы вы тоже не по документации изучаете, а сразу в исходники и дебажить, что там и где и как происходит.

          Мое утверждение, что можно опечататься в строковом литерале подтверждено как минимум проверкой дублирующихся строковых литералов в SonarCube - ссылка в тексте. Не я один считаю такие места потенциально опасными.


          1. vadimr
            29.01.2023 11:27

            Аргументация в том, что верить документации чревато - вообще - мое почтение. Наверное сторонние либы вы тоже не по документации изучаете, а сразу в исходники и дебажить, что там и где и как происходит.

            Вообще-то рано или поздно обычно доходит до этого при углублённом использовании.

            Скажу страшную вещь, и ошибки в компиляторах доводилось находить при отладке программ.


            1. break1 Автор
              29.01.2023 11:46
              +1

              Что-то вы из из крайности в крайность... То вам не ясно, что написано в коде, который отличается буквально на пару символов, то ошибки в компиляторах исправляете...


              1. vadimr
                29.01.2023 11:54

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

                В частности, ваше F2 в IDE – тоже не панацея, особенно для такого языка, как питон, где код может динамически вычисляться. Доверие к результату F2 – тоже плод некоторого априорного доверия к соглашениям о кодировании. Из всего огромного множества которых вы почему-то отнеслись с опасением только к воспроизводимости литералов.


                1. break1 Автор
                  29.01.2023 11:59

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


      1. invasy
        29.01.2023 11:48

        недопрограммистам

        Как вы лихо всех коллег описали.


        Код пишется в основном для чтения другими разработчиками. Чем он понятнее, тем легче его поддерживать. Мы ведь о промышленной разработке говорим? Без олимпиадных задач, какой-то хитрой оптимизации (питон ведь) и других случаев write-only code.


        1. break1 Автор
          29.01.2023 11:54

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


          Поддержка кода с дублями строк куда сложнее, чем его поддержка со словарем констант. Я привел конкретные кейсы в статье. Спорить с ними - на врят ли имеет смысл.


          1. invasy
            29.01.2023 12:13

            Давайте развернём выпад в другую сторону: если человек не может понять, что «бесспорной» проблемы дублирования строковых литералов нет, то он считается недопрограммистом?


            1. break1 Автор
              29.01.2023 12:18

              Так она есть или нет? Зачем она есть в SonarCube? Разверните выпад в их сторону например, а не в мою. Я лишь описал вариант решения, которым пользуюсь я, т.к. я не считаю, что такой проблемы нет.


              1. invasy
                29.01.2023 12:21

                Прочитайте внимательно правило и исключения из него.


                1. break1 Автор
                  29.01.2023 12:32

                  Почему решили, что я не читал? Эта ссылка вообще то в статье и есть ))
                  Так проблемы нет, или она есть и есть исключения?


                  1. invasy
                    29.01.2023 12:53
                    +2

                    В общем случае проблема есть. В приведённом в статье «куске кода» её нет. Строковые литералы могут быть предназначены для разных целей. Ключи в словарях, идентификаторы и т. п. проблемой не являются и записаны в исключения правила SonarQube. Для избежания опечаток можно воспользоваться type hints и статическими анализаторами (предложено в комментариях). Если литералы используются для других целей (regex, сообщения, вывода текста пользователю), для них могут применяться строковые перечисления из самых первых комментов. Или gettext и подобные способы i18n/l10n.


                    В статье не обоснована проблема, приведён некорректный пример, предложено неоптимальное решение.


                    1. break1 Автор
                      29.01.2023 12:59

                      Нормальные примеры в статье, проблема обоснована, решение тоже нормальное предложено.
                      У меня нет цели достучаться до каждого. Вы число ПИ константой юзаете или каждый раз в коде пишите 3.1415926.... ? ))) Оно же неизменно, достаточно везде без ошибки написать - зачем его в константы выносят во всех языках и мат либах? )))


                      1. vadimr
                        29.01.2023 13:07

                        Число пи невозможно написать без ошибки.


                      1. break1 Автор
                        29.01.2023 14:17

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


                      1. invasy
                        29.01.2023 13:07

                        А теперь уже совсем неинтересно. Аргументации как не было, так и нет. Ещё и непоследовательность в рассуждениях.


                        Сочувствую вашим коллегам.


                      1. break1 Автор
                        29.01.2023 14:06

                        До того как вы соизволили прочитать ссылку на Sonar вы вообще утверждали, что такой проблемы нет. Теперь у вас не хватило воображения представить, что в коротких именах с цветами могут быть стрококвые литеры любой длины и любого содержания. И все равно непоследовательность рассуждений и отсутствие аргументации у меня )))
                        А если я вам дам ссылку на статический анализатор, который проверяет и считает ошибкой повторения строк любой длины - вы уже начнете топить и сами за строковые константы? ))

                        Вы не расстраивайтесь - и помните - первая стадия - отрицание!


                      1. invasy
                        29.01.2023 14:28

                        Давайте ещё раз разберёмся. В примере из статьи проблемы дублирования строковых литералов нет. Об этом и писал.

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

                        В любом случае, в статье построена ветряная мельница, с которой вы зачем-то героически боретесь.


  1. alixa
    29.01.2023 11:32

    Думаю вопрос не только к Python и касается не только строк. Пока вижу решение только в константах, например в одном из моих проектов в коде БД на T-SQL, есть таблица с сущностями, в коде используются ID.

    Для каждого ID в таблице при создании новой сущности генерируется функция, функция потом используется в коде.

    Примерно так

    WHERE o.ID = Entities.Объект()

    Функция возвращает число.

    create function Entities.Объкт() as

    Return 85

    Код становиться читабельней, не надо вспоминать и смотреть каждый раз ID сущности.

    В других платформах например Android от Google, генерируются классы с константами, этот подход уже как паттерн, возможно даже название уже придумано.


  1. 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 - с ним можно делать мутабельные поля и ещё много всякого удобного.


    1. break1 Автор
      29.01.2023 13:08

      Это лишь частный случай, он никак не отменяет в принципе строковые константы.