Реализация строкового типа в CPython

    Продолжу неспешный разбор реализации базовых типов в CPython, ранее были рассмотрены словари и целые числа. Тем, кто думает, что в их реализации не может быть ничего интересного и хитрого, рекомендуется приобщиться к данным статьям. Те, же, кто уже их прочёл, знают, что CPython хранит в себе множество интересностей и особенностей реализации. Их может быть полезно знать при написании своих скриптов, так и в качестве пособия по архитектурным и алгоритмическим решениям. Не являются исключением здесь и строки.



    Начнём с небольшого экскурса в историю. Python появился в 1990-91 годах. Изначально при разработке базовой кодировкой в python была однобайтовая, старая добрая ascii. Но, примерно в то же время (чуть позже), человечеству надоело разбираться с «зоопарком» кодировок и в 1991 году был предложен стандарт юникод. Однако с первого раза тоже не получилось. Началось внедрение двухбайтных кодировок, но довольно скоро стало понятно, что двух байт на всех не хватит, была предложена 4-байтная кодировка. К сожалению, выделение по 4 байта на каждый символ выглядело напрасной тратой дискового места и памяти, особенно, в тех странах, где раньше вполне хватало однобайтовой ascii. Было запилено несколько костылей в 2-байтную кодировку, чтобы поддерживать больше символов, и всё это стало напоминать предыдущую ситуацию с «зоопарком» кодировок.

    Но в 1993 году был представлен utf-8. Который являлся компромиссом: ascii была валидным подмножеством utf-8, все остальные символы, его расширяли, однако, для поддержки такой возможности, пришлось расстаться с фиксированной длиной каждого символа. Но именно ему суждено было править всеми стать именно юникодом, то есть единой кодировкой, поддерживаемой большинством программ, в которой хранится большинство файлов. Особенно на это повлияло развитие интернета, так как web-странички обычно использовали именно utf-8.

    Поддержка этой кодировки постепенно внедрялась в языки программирования, которые, как и python, были разработаны раньше utf-8, а потому использовали другие кодировки. Существует PEP с красивым номером 100, в котором обсуждалась поддержка юникода. А в PEP-0263 появилась возможность объявлять кодировку исходных файлов. Базовой кодировкой всё ещё оставалась ascii, для объявления юникодных строк использовался префикс `u`, работа с ними всё ещё не была достаточно удобной и естественной. Зато появилась возможность творить следующую ересь:

    class 비빔밥:
        моя_переменная = 2
    
    א = 비빔밥()
    print(א)
    

    3 декабря 2008 года произошло историческое событие для всего python-сообщества (а учитывая, как широко этот язык распространился сейчас, то, пожалуй, и для всего мира) — вышел python 3. В нём было решено раз и навсегда покончить с проблемами из-за множества кодировок, а потому базовой кодировкой стал юникод. Но мы же помним, что кодировки это сложно и с первого раза не выходит. Не получилось и в этот раз.

    Большим недостатком utf-8 является то, что длина символа не фиксированна, это приводит к тому, что такая простая операция, как обращение по индексу имеет сложность O(N), так как смещение элемента не известно заранее, кроме того, зная размер буфера, выделенного под хранение строки, нельзя вычислить её длину в символах.

    Во избежание всех этих проблем в python было решено использовать 2-ух и 4-байтные кодировки (в зависимости от платформы). Обращение по индексу упростилось — необходимо было лишь умножить индекс на 2 или 4. Однако, это повлекло свои проблемы:

    1. На каждой платформе была своя кодировка, что могло повлечь проблемы с переносимостью кода
    2. Повышенный расход памяти и/или проблемы с кодированием хитрых символов, не влезающих в два байта

    Решение этих проблем было предложено в PEP-393, о нём и поговорим.

    Было решено оставить строки как массив символов, для облегчения доступа по индексу и других операций, однако длина символов стала варьироваться. При создании строки, интерпретатор сканирует все символы и выделяет для каждого количество байт, которое необходимо для хранения самого «большого», то есть, если вы объявите ascii-строку, то все символы будут однобайтовыми, однако, если вы решите добавить к строке один знак из кириллицы, все символы уже будут двух-байтными. Всего возможны три варианта: 1, 2 и 4 байта на символ.

    Строковый тип (PyUnicodeObject) объявлен следующим образом:

    /* Strings allocated through PyUnicode_FromUnicode(NULL, len) use the
       PyUnicodeObject structure. The actual string data is initially in the wstr
       block, and copied into the data block using _PyUnicode_Ready. */
    typedef struct {
        PyCompactUnicodeObject _base;
        union {
            void *any;
            Py_UCS1 *latin1;
            Py_UCS2 *ucs2;
            Py_UCS4 *ucs4;
        } data;                     /* Canonical, smallest-form Unicode buffer */
    } PyUnicodeObject;
    

    В свою очередь PyCompactUnicodeObject представляет собой следующую структуру (приводится с некоторыми упрощениями и моими комментариями):

    /* Структура для не ascii строк */
    typedef struct {
        PyASCIIObject _base;
        Py_ssize_t utf8_length;     /* количество байт под utf-8 буфер (без учёта
                                                \0 терминатора)*/
        char *utf8;                 /* UTF-8 представление (с \0 терминатором)*/
        Py_ssize_t wstr_length;     /* Количество code point в wstr. */
    } PyCompactUnicodeObject;
    
    /* Структура для ascii строк */
    typedef struct {
        PyObject_HEAD     /* стандартный заголовок (указатель на тип и счётчик ссылок) */
        Py_ssize_t length;          /* количество code point в строке */
        Py_hash_t hash;             /* Хэш или -1, если хэш ещё не установлен */
        struct {
            /*
               SSTATE_NOT_INTERNED (0)
               SSTATE_INTERNED_MORTAL (1)
               SSTATE_INTERNED_IMMORTAL (2)
             */
            unsigned int interned:2;
            /* Размер символа:
               - PyUnicode_WCHAR_KIND (0):
                 * wchar_t (16 или 32 бита, в зависимости от платформы (старый формат))
               - PyUnicode_1BYTE_KIND (1):
               - PyUnicode_2BYTE_KIND (2):
               - PyUnicode_4BYTE_KIND (4):
             */
            unsigned int kind:3;
            /* Флаг компактной формы
              (если установлен, то вся структура лежит в обном блоке памяти,
    	  если сброшен - то data лежит в другом). */
            unsigned int compact:1;
            /* Строка содержит только символы из диапазона U+0000-U+007F (ASCII) */
            unsigned int ascii:1;
            /* установлен, если структура полностью инициализирована,
               использовался в старых реализациях */
            unsigned int ready:1;
            unsigned int :24;
        } state;
        wchar_t *wstr;              /* wchar_t представление ( с \0 терминатором) */
    } PyASCIIObject;
    

    Таким образом возможны 4 представления строк:

    1. legacy string, ready

              * structure = PyUnicodeObject structure
              * Проверка на тип: !PyUnicode_IS_COMPACT(op) && kind != PyUnicode_WCHAR_KIND
              * kind = PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND or
               PyUnicode_4BYTE_KIND
              * compact = 0
              * ready = 1
              * data.any is not NULL
              * utf8 разделяется с data.any и utf8_length = length если ascii = 1
              * utf8_length = 0 если utf8 is NULL
              * wstr разделяется с with data.any и wstr_length = length
               если kind=PyUnicode_2BYTE_KIND and sizeof(wchar_t)=2
               or if kind=PyUnicode_4BYTE_KIND and sizeof(wchar_4)=4
              * wstr_length = 0 если wstr is NULL
          
    2. legacy string, not ready

               * structure = PyUnicodeObject
               * Проверка на тип: kind == PyUnicode_WCHAR_KIND
               * length = 0 (use wstr_length)
               * hash = -1
               * kind = PyUnicode_WCHAR_KIND
               * compact = 0
               * ascii = 0
               * ready = 0
               * interned = SSTATE_NOT_INTERNED
               * wstr is not NULL
               * data.any is NULL
               * utf8 is NULL
               * utf8_length = 0
        
    3. compact ascii

               * structure = PyASCIIObject
               * Проверка на тип: PyUnicode_IS_COMPACT_ASCII(op)
               * kind = PyUnicode_1BYTE_KIND
               * compact = 1
               * ascii = 1
               * ready = 1
               * (length — длина utf8 и wstr строк)
               * (data располагается сразу за структурой)
               * (так как ascii является подмножеством поле utf8 string и есть data)
        
    4. compact

               * structure = PyCompactUnicodeObject
               * Проверка на тип: PyUnicode_IS_COMPACT(op) && !PyUnicode_IS_ASCII(op)
               * kind = PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND or
                 PyUnicode_4BYTE_KIND
               * compact = 1
               * ready = 1
               * ascii = 0
               * utf8 и data различны
               * utf8_length = 0 если utf8 is NULL
               * wstr разделяется с data и wstr_length=length
                 если kind=PyUnicode_2BYTE_KIND and sizeof(wchar_t)=2
                 or if kind=PyUnicode_4BYTE_KIND and sizeof(wchar_t)=4
               * wstr_length = 0 есди wstr is NULL
               * (data располагается сразу за структурой)
        

    Следует обратить внимание, что python 3 так же поддерживает синтаксис объявления юникодных строк через префикс `u`.

    >>> b = u"тут"
    >>> b
    'тут'
    

    Эта возможность была добавлена для облегчения портирования кода со второй версии на третью в PEP-414 в python 3.3 только в феврале 2012 года, напомню, что python 3 вышел в декабре 2008, да никто не спешил с переходом.

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

    import ctypes
    import enum
    import sys
    
    
    class Interned(enum.Enum):
        #            SSTATE_NOT_INTERNED (0)
        #            SSTATE_INTERNED_MORTAL (1)
        #            SSTATE_INTERNED_IMMORTAL (2)
        #            If interned != SSTATE_NOT_INTERNED, the two references from the
        #            dictionary to this object are *not* counted in ob_refcnt.
        SSTATE_NOT_INTERNED = 0
        SSTATE_INTERNED_MORTAL = 1
        SSTATE_INTERNED_IMMORTAL = 2
    
    
    class Kind(enum.Enum):
        PyUnicode_WCHAR_KIND = 0
        PyUnicode_1BYTE_KIND = 1
        PyUnicode_2BYTE_KIND = 2
        PyUnicode_4BYTE_KIND = 4
    
    
    # PyUnicodeObject
    class StrStruct(ctypes.Structure):
        _fields_ = [("refcnt", ctypes.c_long),
                    ("type", ctypes.c_void_p),
                    ("length", ctypes.c_long),
                    ("hash", ctypes.c_void_p),  # ascii fields
                    ("_interned", ctypes.c_uint, 2),
                    ("_kind", ctypes.c_uint, 3),
                    ("compact", ctypes.c_uint, 1),
                    ("ascii", ctypes.c_uint, 1),
                    ("ready", ctypes.c_uint, 1),
                    ("_rest_state", ctypes.c_uint, 16),  # for future use
                    ("wstr", ctypes.c_wchar_p),
                    # PyCompactUnicodeObject
                    ("utf8_length", ctypes.c_ssize_t),  # Number of bytes in utf8, excluding the terminating \0.
                    ("utf8", ctypes.c_char_p),
                    ("wstr_length", ctypes.c_ssize_t),  # Number of code points
                    ("data", ctypes.c_void_p)  # canonical, smallest-form Unicode buffer
                    ]
        _printable_fields = ("refcnt", "length", "hash", "interned", "kind", "compact", "ascii", "ready", "wstr",
                             "utf8_length", "utf8", "wstr_length", "data")
    
        @property
        def interned(self):
            return Interned(self._interned)
    
        @property
        def kind(self):
            return Kind(self._kind)
    
        def __repr__(self):
            new_line = '\n'  # f-string expression part cannot include a backslash
            return f"StrStruct({new_line.join(f'{key}={getattr(self, key)}' for key in self._printable_fields)})"
    
    
    if __name__ == '__main__':
        string = sys.argv[1]
        s = StrStruct.from_address(id(string))
        print(s)
    

    И даже «сломать» интерпретатор, как сделали это в предыдущей части.

    DISCLAIMER: Следующий код поставляется as is, автор не несёт никакой ответственности и не может гарантировать состояние интерпретатора, а также душевное здоровье вас и ваших коллег, после запуска этого кода. Код протестирован на версии cpython 3.7 и, к сожалению, не работает с ascii строками.

    Для этого надо изменить код, описанный вверху, на:

    def make_some_magic(str1, str2):
        s1 = StrStruct.from_address(id(str1))
        s2 = StrStruct.from_address(id(str2))
        s2.data = s1.data
    
    
    if __name__ == '__main__':
        string = "비빔밥"
        string2 = "háč"
        print(string == string2)          # False
        make_some_magic(string, string2)
        print(string == string2)          # True
    


    В данных примерах используется интерполяция строк, добавленная в python 3.6. Python далеко не сразу пришёл к данному способу вывода строк: были испробованны %-синтаксис, format, что-то perl подобное (более подробное описание с примерами можно найти здесь).
    Пожалуй это изменение для своего времени (до python 3.8 с его `:=` оператором) было самым противоречивым. Обсуждение (и осуждение) велось как на reddit и даже в виде PEP. Высказывались идеи улучшения/исправления в виде добавления i-строк, для которых пользователи смогли бы писать свои парсеры, для лучшего контроля и во избежание SQL-инъекций и прочих проблем. Однако данное изменение было отложено, для того, чтобы люди привыкли к f-строкам и выявили проблемы, если они есть.

    У f-строк, есть одна особенность(недостаток): в них нельзя указывать спец символы со слэшами, например, '\n' '\t'. Однако, это легко обойти, объявив отдельную строку, содержащую спец-символы и передав её в f-строку, что и было проделано в примере выше, зато можно использовать вложенные скобки.

    >>> number = 2
    >>> precision = 3
    >>>  f"{number:.{precision}f}"
    2.000
    # впрочем, format тоже позволяет такие приёмы
    >>> "{:{}f}".format(number, precision)
    2.000    
    

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

    При сравнении двух строк проверяется, ссылаются ли указатели на строки на один адрес, если нет — то запускается посимвольное сравнение или memcmp в случаях, когда это допустимо.

    int
    PyUnicode_Compare(PyObject *left, PyObject *right)
    {
        if (PyUnicode_Check(left) && PyUnicode_Check(right)) {
            if (PyUnicode_READY(left) == -1 ||
                PyUnicode_READY(right) == -1)
                return -1;
    
            /* переданы две идентичные строки */
            if (left == right)
                return 0;
    
            return unicode_compare(left, right);  // посимвольное сравнение или memcmp
        }
        Генерация ошибки
    }

    Однако, косвенным образом значение хэша влияет на сравнение. Дело в том, что в cpython строки интернируются, то есть хранятся в одном словаре. Это верно не для всех строк, интернируются все константы, ключи словарей, поля и переменные и ascii строки длиной меньше 20.

    if __name__ == '__main__':
        string = sys.argv[1]
        string2 = sys.argv[2]
        print(id(string) == id(string2))
    

    $ python check_interned.py a a
    True
    $ python check_interned.py 비빔밥 비빔밥
    False
    $ python check_interned.py aaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    False
    

    А пустая строка вообще синглтон

    static PyUnicodeObject *
    _PyUnicode_New(Py_ssize_t length)
    {
        PyUnicodeObject *unicode;
        size_t new_size;
    
        /* Optimization for empty strings */
        if (length == 0 && unicode_empty != NULL) {
            Py_INCREF(unicode_empty);
            return (PyUnicodeObject*)unicode_empty;
        }
        ...
    }
    

    Как мы видим, cpython смог создать простую, но в то же время, эффективную реализацию строкового типа. Удалось сократить используемую память и ускорить операции в некоторых случаях, благодаря функциям memcmp, memcpy, вместо посимвольных операций. Как видим строковый тип совсем не так прост в реализации, как может показаться с первого раза. Но разработчики cpython достаточно умело подошли к своему делу и поэтому мы можем им пользоваться и даже не задумываться, что там «под капотом».
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      +4
      У f-строк, есть одна особенность(недостаток): в них нельзя указывать спец символы со слэшами, например, '\n' '\t'.

      У меня всегда работали. Проверил сейчас на python 3.8. Следующий код выводит и перевод строки, и табуляцию.
      print(f'Line #1\n\tLine #2')
        +3
        Действительно, так работает, но вот в фигурных скобках, указывать нельзя:

        >>> a = ["a", "b"]
        >>> f"{'\n'.join(a)}"
        SyntaxError: f-string expression part cannot include a backslash
        
          +2

          Спасибо за уточнение. Буду знать


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

            +1
            Я сам с удивлением узнал, впрочем, это действительно затрудняет парсинг и создаёт возможные уязвимости.
            Вот здесь есть несколько примеров.
        –1
        Как мы видим, cpython смог создать простую, но в то же время, эффективную реализацию строкового типа.
        Это сарказм такой? Не знаю насчёт скорости, но наворотов тут даже больше, чем в C++ std::string, который тоже считается сложным (ради эффективности, понятно).
          +3
          Ждём от Вас статьи про std::string. Я не в курсе, слышал только про 2 реализации всех строковых алгоритмов в java: для ascii и для не ascii строк и флаг, указывающий на тип.

          Ну и самое простое — это хранить массив из 4 байт на каждый символ. Вот только эффективно ли это?

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

          А так ли важно, сколько там наворотов, если абстрация не течёт? :)
            –1
            А так ли важно, сколько там наворотов, если абстрация не течёт? :)
            Ну абстракция всё равно течёт, конечно. Они почти все текут. Да собственно ваш check_interned это показывает (там, кстати, можно писать не id(string) == id(string2), по-простому: string is strings… что, кстати, показывает как протечка в этой абстракции может вас «укусить» в реальной программе).

            Но самая большая проблема со строками в Python3 — это то, что все эти навороты решают непонятно какую проблему, но понятно какую проблему они не решают: работу с реальными данными.

            Потому что вся работа со строками в Python3 построена вокруг двух предположений:
            1. Строки (текст) и бинарные данные строго отделены друг от друга.
            2. Во всех случаях когда мы работаем со строками мы всегда заранее знаем кодировку.
            Поскольку в реальном мире оба этих предположения неверны, то строки в Python — это боль и содомия. Чего одна история с path-like объектами стоит!

            А в C++ строки не так интересны — хотя там они считаются сложными они на порядок меньше вот всего того, что тут навернули. Первый бит указывает на то короткая строка или длинная, оставшееся интерпретируется в зависимости от этого — собственно и всё.
              0
              Потому что вся работа со строками в Python3 построена вокруг двух предположений:
              1. Строки (текст) и бинарные данные строго отделены друг от друга.
              2. Во всех случаях когда мы работаем со строками мы всегда заранее знаем кодировку.
              Поскольку в реальном мире оба этих предположения неверны, то строки в Python — это боль и содомия. Чего одна история с path-like объектами стоит!


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

              Насчёт кодировок — константы всегда приходят из исходника с известной кодировкой, при чтении файлов, её тоже можно указать. Если он не известна то, что вы предлагаете делать?
                +1
                Ну это логично, строки — частный случай бинарных данных, вполне можно навесить дополнительных проверок и операций, чтобы облегчить работу программисту.
                Это у нормальных людей строки — это частный случай бинарных данных. В python3 — нет. Отсюда, собственно, появление path-like объектов: поскольку имя файлов (на на Linux, ни на Windows) не обязано быть валидной UTF-8 или UTF-16 строкой, то можно было легко создать файл, который в ранних версиях python-3 нельзя в принципе никак открыть.

                То же самое с HTTP-серверами и с массой других протоколов. Ситуация, когда вперемешлу «в потоке байт» идут строки и «сырые» данные — исключением совершенно не является. Ситуация, когда вы вначале должны распарсить строку, а потом — узнаёте в какой она кодировки… тоже типична.

                Всё это решается в Python3 втыканием всё большего и большего числа костылей в разные места (начиная с Python 3.5 поддерживается %, может со временем и format добавят).

                Как я уже упоминал, в некоторых языках даже индексирование не возможно.
                А в python возможно? Да неужели?
                >>> 'Ху**ня'[4]
                'н'
                >>> 'Ху*ня'[4]
                'я'
                
                Это, по вашему, нормальная индексация?

                Я вообще не знаю ни одного практически полезного алгоритма, для которого такая индексация годилась бы, а побайтовая индексация UTF-8 текста — не годилась бы.

                Но за эту блажь платится усложнением реализации, замедлением и усложнением языка. Впрочем это всё в духе Python, так что, в принципе, неудивительно.

                Python3 по сравнению с Python2 — это как C++ по сравнению с C: выстрелить в ногу слегка сложнее, но всё равно не очень сложно, а разрушений — намного больше.

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

                Какой процент строк из вашей программы у вас отображается на экране? Зачем засорять работу с теми из них (а это, в типичном случае 90%, а во многих программах и 99%) сущностью, которая вам не нужна?
                  +1
                  Я не понимаю ваших претензий.
                  Это у нормальных людей строки — это частный случай бинарных данных. В python3 — нет.

                  Давайте не переходить на уровень «настоящих шотландцев». Что вы подразумеваете, под тем, что в python3 строки не бинарные данные?

                  А в python возможно? Да неужели?
                  >>> 'Ху**ня'[4]
                  'н'
                  >>> 'Ху*ня'[4]
                  'я'

                  У вас в первом случае должна быть [3]? Ну выглядит как логичное и ожидаемое поведение, строка — последовательность символов, индексируясь по ней — получаем символы. Ещё раз есть языки, в которых на такой операции вы получите ошибку компиляции, это лучше?

                  Я вообще не знаю ни одного практически полезного алгоритма, для которого такая индексация годилась бы, а побайтовая индексация UTF-8 текста — не годилась бы.

                  1. encode вам в помощь.
                  2. ну и не работайте в таком случае не с python, зачем грызть кактус?) Есть C, который гораздо лучше подходит под обработку бинарных данных, python вообще не про это.

                  Но за эту блажь платится усложнением реализации, замедлением и усложнением языка. Впрочем это всё в духе Python, так что, в принципе, неудивительно.


                  Критикуешь — предлагай. Как вы посоветуете переписать реализацию? Можно хоть в виде PEP на оф. сайте. Всё сообщество будет вам благодарно.

                  Python3 по сравнению с Python2 — это как C++ по сравнению с C: выстрелить в ногу слегка сложнее, но всё равно не очень сложно, а разрушений — намного больше.


                  Всё смешалось, кони-люди. В C++ выстрелить в ногу сложнее или в C? Какие разрушения вы получите в случае с python? Я пытался, я менял служебные поля в рантайме, в большинстве случаев получал падение интерпретатора, никуда буферы не текли, стеки не переполнялись, произвольный код не выполнялся. Хотя достичь этого можно, но надо прям специально постараться, это не выстрел в ногу — это осознанный суицид.

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


                  Так используйте) Кто вам не разрешает?

                  Какой процент строк из вашей программы у вас отображается на экране? Зачем засорять работу с теми из них (а это, в типичном случае 90%, а во многих программах и 99%) сущностью, которая вам не нужна?


                  Вот здесь вообще не понял.
                    0
                    Что вы подразумеваете, под тем, что в python3 строки не бинарные данные?
                    То, что в них нельзя засунуть произвольную последовательность байт, очевидно. Например вы не можете имя файлы (которое в Windows не обязано быть валидной UTF-16 последовательностью, а в Linux валидной UTF-8 последовательностью) поместить в строку и проверить — не начинается оно с нужного вам префикса.

                    Ещё раз есть языки, в которых на такой операции вы получите ошибку компиляции, это лучше?
                    Лучше конечно. Отсуствие идиотского поведения всегда лучше его наличия. Хорошее поведение — ещё лучше, конечно, вот только при работе со строками его нет. В принципе нет — и всё.

                    Ну выглядит как логичное и ожидаемое поведение, строка — последовательность символов, индексируясь по ней — получаем символы.
                    Ну и кому и зачем это может быть нужно? Зачем оптимизировать работу под операции которые никогда не используются (или используются исключительно редко) за счёт операций, которые используются часто?

                    Есть C, который гораздо лучше подходит под обработку бинарных данных, python вообще не про это.
                    А про что он вообще тогда?

                    Как вы посоветуете переписать реализацию?
                    Никак — ошибка же в дизайне. Остаётся только смириться. Ну вот такой вот бессмысленный высер, который обошёлся индустрии в миллиарды и не дал никакой прибыли. Конструктивно — нужно убрать человека, который его затеял, чтобы больше такого не повторялось.

                    Это, впрочем, уже сделано, так что ничего менять уже не нужно.

                    В C++ выстрелить в ногу сложнее или в C?
                    В C++ сложнее наделать проблем гораздо сложнее, но если уж это удалось сделать — то разрушений намного больше.

                    То же самое с Python2 строками vs Python3 строками. Python2 позволяет легко сделать что-то не совсем правильно — но это быстро обваливается и легко чинится. В Python3 любое простое решение «правильно на 99.9%», но вот оставшийся 0.1% — исправить, зачастую, возможно только полными переписываением всей программы. Что дорого и сложно.

                    Так используйте) Кто вам не разрешает?
                    Python3. Масса операций, имеющих полный смысл для последовательностей байт — не работает с бинарными строками. А строковые операции не принимают произвольные последовательности байт. В Python2 — проблем не было.

                    Простейший пример, которым я всегда тыкаю в нос — простенький файл для работы со словариком и там буквально пяток функций для каждого языка. Которые просто возвращают список гласных, список согласных и так далее. Для английского, немецкого, польського и русского (там ещё что-то было — но эти ключевые). Функции, работающие с немецким рассчитаны на Latin-1, на польский — на Latin-2, Руссики — windows-1251… ну и комментарии в UTF-8 для удобства.

                    Всё это, ещё раз повторяю, в одном файле. Инструменты старого образца (Emacs, Far или тот же Python2) — вообще не проблема. Говорим, что это последовательность байт и всё. Что-то сделать в Python3… практически невоможно.

                    Ну потому что этот текстовый файл в Python3 строку — не лезет. Ну никак.

                    1. encode вам в помощь.
                    С этого момента — поподробнее: как его применять для редактирования подобного файла?
                      0
                      То, что в них нельзя засунуть произвольную последовательность байт, очевидно. Например вы не можете имя файлы (которое в Windows не обязано быть валидной UTF-16 последовательностью, а в Linux валидной UTF-8 последовательностью) поместить в строку и проверить — не начинается оно с нужного вам префикса.


                      Это проблемы python или ОС, которые сохраняют имена файлов в чём попало? Строка в python имеет определённую кодировку, вполне логично запретить туда записывать мусор. Для работы с файлами создали отдельный механизм, потому что это отдельная сущность. Зачем ломать из-за этого общий механизм?

                      Ну и кому и зачем это может быть нужно? Зачем оптимизировать работу под операции которые никогда не используются (или используются исключительно редко) за счёт операций, которые используются часто?


                      Ну если вам ненужно, очевидно, что всем не нужно. Напишите в почтовую рассылку, просветите заблудшие души.

                      Есть C, который гораздо лучше подходит под обработку бинарных данных, python вообще не про это.


                      А про что он вообще тогда?


                      Про читаемость. Это если одним словом, если несколькими — это к дзену, там про бинарные данные ничего нет.

                      Никак — ошибка же в дизайне. Остаётся только смириться. Ну вот такой вот бессмысленный высер, который обошёлся индустрии в миллиарды и не дал никакой прибыли. Конструктивно — нужно убрать человека, который его затеял, чтобы больше такого не повторялось.


                      Опять же, могу только посоветовать написать в рассылку. Кстати, а как вы подсчитали урон? Можете поделиться?

                      В Python3 любое простое решение «правильно на 99.9%», но вот оставшийся 0.1% — исправить, зачастую, возможно только полными переписываением всей программы. Что дорого и сложно.


                      Утверждение верное для любой технологии, даже молотка.

                      Простейший пример, которым я всегда тыкаю в нос — простенький файл для работы со словариком и там буквально пяток функций для каждого языка. Которые просто возвращают список гласных, список согласных и так далее. Для английского, немецкого, польського и русского (там ещё что-то было — но эти ключевые). Функции, работающие с немецким рассчитаны на Latin-1, на польский — на Latin-2, Руссики — windows-1251… ну и комментарии в UTF-8 для удобства.

                      Всё это, ещё раз повторяю, в одном файле. Инструменты старого образца (Emacs, Far или тот же Python2) — вообще не проблема. Говорим, что это последовательность байт и всё. Что-то сделать в Python3… практически невоможно.

                      Ну потому что этот текстовый файл в Python3 строку — не лезет. Ну никак.


                      Это простой пример? Зачем вы храните разные кодировки в одном файле? В чём скрытый смысл? Как вы его обрабатываете сейчас? Чем? Зачем?

                      С этого момента — поподробнее: как его применять для редактирования подобного файла?


                      Читаете файл, как бинарный блоками, на блоке вызываете decode/encode
                        0
                        Это проблемы python или ОС, которые сохраняют имена файлов в чём попало?
                        Python, конечно. OS вы сменить не можете (вернее можете, но поскольку проблемы есть во всех современных OS, то это ничего не даст), а язык — можете (пока что Go выглядит неплохим вариантом).

                        Строка в python имеет определённую кодировку, вполне логично запретить туда записывать мусор.
                        Только если ваша задача — не написание программ для работы с реальными данными, а то, что один мой хороший знакомый называет «интеллектуальная мастурбация».

                        Для работы с файлами создали отдельный механизм, потому что это отдельная сущность.
                        Создали отдельную сущность, заметим, только в 2016м. Через восемь лет после выхода Python 3.0

                        Зачем ломать из-за этого общий механизм?
                        Потому что «общий механизм» должен работать с «общими данными», очевидно. Где невалидный Unicode — норма, а не исключение. И если существующие программы с этими данными работают (а они работают), то никто ради вас «общие данные» переделывать не будет.

                        Про читаемость. Это если одним словом, если несколькими — это к дзену, там про бинарные данные ничего нет.
                        А про корректность там чего-нибудь есть?

                        Кстати, а как вы подсчитали урон? Можете поделиться?
                        Примерно — тупо по методике COCOMO посчитать сколько денег ушло на бессмысленное переписывание кучи кода с Python2 на Python3.

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

                        Это простой пример?
                        А что — сложный? Один текстовый файл, там меньше 1000 строк.

                        Зачем вы храните разные кодировки в одном файле?
                        Потому что так удобнее.

                        В чём скрытый смысл?
                        Работа с однобайтовыми кодировками банально быстрее, чем с многобайтовыми. А для словаря с одним языков (99% случаев) многобайтовая кодировка не нужна.

                        Как вы его обрабатываете сейчас?
                        Да чем угодно: emacs/vim/far/awk/perl/python2. Собственно его можно обрабатывать чем угодно, нельзя только Python3.

                        Зачем?
                        Ну например утилита для форматирования у нас на Python была до появления clang-format.

                        Читаете файл, как бинарный блоками, на блоке вызываете decode/encode
                        Ну и как, я извиняюсь, это может работать? Ни один из предложенных там вариантов не позволит мне сохранить данные после декодирования.

                        Понимаете — проблема Python3 в том, что он появился не в 1968м, а в 2008м. Когда наиболее распространённый тип данных в мире — это «поломанный Unicode». То есть это либо бинарные данные со вкраплениями UTF-8, либо UTF-8, где кое-где немножко что-то невалидно, либо «вроде-как-типа-что-то UTF-16, но на самом деле нет» (в Java и C#, теоретически, строки — это UTF-8, но на самом деле это почти нигде не энфорсится, так что любая последовательность 16-битовых значений принимается). В языках, которые предназначены для первого типа — легко обрабатываются и данные второго типа (WTF-8 в помощь). А вот с Python — вы никогда не можете быть уверены, что ваша проверенная и отлаженная программа вдруг не упадёт из-за того, что кто-то, вдруг, посмеет выбрать русский язык при установке вашей программы (была такая беда в Fedora — всё накрывалось медным тазом из-за того, что строка сообщавщая об успешном завершении установки выбрасывала UnicodeError… после чего инсталлятор успешно откатывал всю установку… решалось жёстким выключением компьютера в определённый момент).
                          0
                          Создали отдельную сущность, заметим, только в 2016м. Через восемь лет после выхода Python 3.0

                          Что подтверждает только то, что это не было первостепенной задачей ни для кого. Иначе ввели бы раньше.
                          Работа с однобайтовыми кодировками банально быстрее, чем с многобайтовыми. А для словаря с одним языков (99% случаев) многобайтовая кодировка не нужна.


                          Ещё раз, python не про скорость и не работу с бинарными данными, он про читаемость, файл с несколькими кодировками — это не к читаемости. Создатели python не предусматривали такой сценарий, естественно, он в таком формате неоптимален, но люди, смешивающие кодировки, должны страдать.
                          В тоже время куча людей создаёт сайты на django, тесты и скрипты автоматизации на python и не жалуется.

                          Зачем вы храните разные кодировки в одном файле?


                          Потому что так удобнее.


                          Вы уверенны? Новички, приходящие в вашу команду, как это воспринимают? Интуитивно понятно?

                          была такая беда в Fedora — всё накрывалось медным тазом из-за того, что строка сообщавщая об успешном завершении установки выбрасывала UnicodeError…


                          Текст — это сложно. Я про это отписал, как починили по итогу?

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

                          Почему? Вы прочли бинарные данные — перегнали их в текст, обработали — перегнали обратно в бинарные. Можно даже сразу обрабатывать бинарные, `b'my_string'` вам в помощь. Если формат файла более-менее постоянен и позволяет описать их константами.

                          И вы так и не написали, как реализованы строки в C++.
                            –1
                            Что подтверждает только то, что это не было первостепенной задачей ни для кого.
                            Нет — подтверждает то, что разработчиков языка проблемы его пользователей мало волновали. Иначе бы о них подумали раньше и перешли бы на третью версию «несколько» быстрее. А в результате — ещё и сегодня есть учебные курсы, которые используют Python2 для обучения… то никакой смерти Python2 в 2020м не будет.

                            Во многом именно потому что работа со строками в Python3 менее удобна чем в Python2.

                            В тоже время куча людей создаёт сайты на django, тесты и скрипты автоматизации на python и не жалуется.
                            Ровно до того момента, пока их тесты и скрипты не начинают падать. Потом — да, жалобы возникают, но поскольку авторы этих скриптов уже завязли…

                            Вы уверенны? Новички, приходящие в вашу команду, как это воспринимают? Интуитивно понятно?
                            Последним новчиком, которому это нужно было воспринимать был, собственно я (добавлял русский язык, очевидно). Не заметил проблем. И Emacs и Vim всё отлично обрабатывают. Если бы пользовал Eclipse или что-то помоднее — может и были бы проблемы, я не знаю.

                            Текст — это сложно. Я про это отписал, как починили по итогу?
                            Через два года вышла новая версия инсталлятора, там этой проблемы вроде не было. Хотя я, на всякий случай, зарёкся использовать что-то, кроме en_US во время установки… вряд ли это можно считать «красивым» решением проблемы… но мы ж про читабельность, нам же работоспособность не нужна.

                            Почему? Вы прочли бинарные данные — перегнали их в текст, обработали — перегнали обратно в бинарные.
                            Потому что если там будут-таки бинарные данные то после того, как вы «перегоните их в текст» — они будут безнадёжно испорчены. Либо будет брошено исключение и вы не получите ничего, либо разные байты будут заменены на один, либо ещё что-нибудь в этом же роде произойдёт.

                            Да, можно попробовать что-то такое изобразить через codecs.register_error — но всё это выглядит крайне неудобно.

                            Если формат файла более-менее постоянен и позволяет описать их константами.
                            Ну формат файла в данном случае вы придставляете…

                            И вы так и не написали, как реализованы строки в C++.
                            Почему не написал? Написал. Общая идея там в одно предложение укладывается, а подробных статей уже и так имеется «в количестве» — зачем ещё одна?
          0
          del

          Only users with full accounts can post comments. Log in, please.