Регулярно требуется преобразовать какой-либо текст в максимально совместимый текст для URL, имени файла, имени объекта в каком-то софте и тд. Требования совместимости простые: в тексте должны быть только допустимые символы. Обычно это a-z, 0-9 и "_" или "-". То есть, только прописные буквы латинского алфавита и цифры (как пример).
Допустим, нам нужно название статьи в блоге преобразовать в slug для добавления его в URL этой статьи. Как это лучше всего сделать?
В Django по умолчанию есть готовая функция slugify для таких случаев.
Но я её никогда не использую. Почему? Потому что её недостаточно!
Приведём пример
Так как я часто пишу сайты для русскоязычных пользователей эта проблема весьма актуальна. Я не использую стандартную функцию и всегда пишу свою.
Оригинал я не беру в расчёт и пишу полностью свою функцию. И так, по порядку:
🔸1. Исходный текст:
🔸2. Транслит
Необходимо сделать транслит всех символов в латиницу. Здесь очень выручает библиотека unidecode. Помимо простого транслита кириллицы в латиницу она умеет преобразовывать спец символы и иероглифы в текстовые аналоги.
В нашем случае получаем такое преобразование:
А еще наш код уже поддерживает любой язык, будь то хинди или корейский.
🔸4. Фильтр символов
Unidecode не занимается фильтрацией по недопустимым символам. Это мы делаем в следующем шаге через regex. Просто заменим все символы на "_" если они вне указанного диапазона.
🔸5. Slugify
Осталось удалить лишние символы по краям и сделать нижний регистр
🌎 Полный код в виде функции.
______________
PS. Проверку что в строке остался хоть один допустимый символ я бы вынес в отдельную функцию.
#libs #tricks #django
Допустим, нам нужно название статьи в блоге преобразовать в slug для добавления его в URL этой статьи. Как это лучше всего сделать?
В Django по умолчанию есть готовая функция slugify для таких случаев.
Но я её никогда не использую. Почему? Потому что её недостаточно!
Приведём пример
>>> from django.utils.text import slugify
>>> slugify('This is a Title')
'this-is-a-title'
Пока всё отлично>>> slugify('This is a "Title!"')
'this-is-a-title'
Спец символы удалились, всё хорошо.>>> slugify('Это заголовок статьи')
''
Вот и приехали 😢. Если текст не английский то буквы просто игнорируются. Можно это поправить>>> slugify('Это заголовок статьи', allow_unicode=True)
'это-заголовок-статьи'
Но тогда мы не вписываемся в условие. У нас появилась кириллица в тексте.Так как я часто пишу сайты для русскоязычных пользователей эта проблема весьма актуальна. Я не использую стандартную функцию и всегда пишу свою.
Оригинал я не беру в расчёт и пишу полностью свою функцию. И так, по порядку:
🔸1. Исходный текст:
>>> text = 'Мой заголовок №10 😁!'
Взял специально посложней со специальными символами.🔸2. Транслит
Необходимо сделать транслит всех символов в латиницу. Здесь очень выручает библиотека unidecode. Помимо простого транслита кириллицы в латиницу она умеет преобразовывать спец символы и иероглифы в текстовые аналоги.
from unidecode import unidecode
>>> unidecode("Ñ Σ ® µ ¶ ¼ 月 山")
'N S (r) u P 1/4 Yue Shan'
Очень крутая библиотека, советую👍В нашем случае получаем такое преобразование:
>>> text = unidecode(text)
>>> print(text)
'Moi zagolovok No. 10 !'
Отличный транслит. Смайл просто удалился, хотя я ждал что-то вроде :). Ну и ладно, всë равно невалидные символы. А еще наш код уже поддерживает любой язык, будь то хинди или корейский.
🔸4. Фильтр символов
Unidecode не занимается фильтрацией по недопустимым символам. Это мы делаем в следующем шаге через regex. Просто заменим все символы на "_" если они вне указанного диапазона.
>>> text = re.sub(r'[^a-zA-Z0-9]+', '_', text)
>>> print(text)
'Moi_zagolovok_No_10_'
Символ "+" в паттерне выручает когда несколько недопустимых символов идут рядом. Все они заменяются на один символ "_".🔸5. Slugify
Осталось удалить лишние символы по краям и сделать нижний регистр
>>> text = text.strip('_').lower()
>>> print(text)
'moi_zagolovok_no_10'
Получаем отличный slug! 😎🌎 Полный код в виде функции.
______________
PS. Проверку что в строке остался хоть один допустимый символ я бы вынес в отдельную функцию.
#libs #tricks #django
GitHub
django/django/utils/text.py at main · django/django
The Web framework for perfectionists with deadlines. - django/django
Python позволяет передавать любые объекты в качестве аргументов или возвращаемых значений.
А так как в Python всё объекты то функции и классы тоже входят в этот список.
Но как проверить что нам вернули именно функцию? Не класс, не None и не число и не строку.
(Да, динамическая типизация в Python даёт о себе знать)
Например, у нас есть функция, полученная из вне.
Лучше всего сравнить тип объекта с типом функции. Но как это сделать? Если бы у нас был int, то всё очевидно:
Функции, определяющие что объект это:
#libs
А так как в Python всё объекты то функции и классы тоже входят в этот список.
Но как проверить что нам вернули именно функцию? Не класс, не None и не число и не строку.
(Да, динамическая типизация в Python даёт о себе знать)
Например, у нас есть функция, полученная из вне.
>>> func = some_module.get_function()Надо убедиться что это именно функция. Какие есть варианты? Проверим, вызываемый ли это объект.
>>> hasattr(func, '__call__')Но это ничего не говорит о типе объекта. Вызываемым может быть и класс и генератор и lambda.
True
Лучше всего сравнить тип объекта с типом функции. Но как это сделать? Если бы у нас был int, то всё очевидно:
>>> isinstance(value, int)Но где взять ссылку на тип функции? Можно просто забрать его от любой функции
>>> # создаём пустую функциюКаждый раз нам этого делать не надо. Все нужные типы уже есть в модуле types, созданные именно таким способом. Нам остаётся только сделать сравнение
>>> def f():pass
>>> # сравниваем типы
>>> isinstance(func, type(f))
True
>>> import typesНо есть способ еще проще и понятней, это модуль inspect. Всё тоже самое но завёрнуто красиво.
>>> isinstance(func, types.FunctionType)
True
>>> import inspectПриведу неполный но часто используемый мною список функций этого модуля.
>>> inspect.isfunction(func)
True
Функции, определяющие что объект это:
.isfunction() — функция.isbuiltin() — стандартная функция Python.isclass() — класс (не инстанс класса).isabstract() — абстрактный класс.ismethod() — метод класса.isgenerator() — генератор.ismodule() — модуль#libs
Что делать если в файле записан текст не ASCII символами? Например кириллица или иероглифы. Вероятно, и кодировка у него будет не utf-8.
Попытка прочитать такой файл может завершиться ошибкой:
Полный код
Попытка прочитать такой файл может завершиться ошибкой:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc4 in position 25: invalid continuation byte
Всё просто, используем аргумент encoding при открытии файла>>> f = open(filepath, encoding="windows-1251")
А как быть когда кодировка неизвестна? Делать серию try-except перебирая разные варианты? Конечно нет! Можно использовать библиотеку для определение кодировки chardet. >>> import chardet
>>> chardet.detect(open(filepath, 'rb').read())
{'encoding': 'windows-1251', 'confidence': 0.9657063861040789, 'language': 'Russian'}
Функция detect() принимает байты а не строку.Полный код
>>> import chardet
>>> filepath = '...'
>>> enc = chardet.detect(open(filepath, 'rb').read())['encoding']
>>> text = open(filepath, encoding=enc)
#libsСловарь это очень распространённый тип данных в Python.
Он присутствует буквально в каждом скрипте.
Именованные аргументы (kwargs), атрибуты объекта (ˍˍdictˍˍ), любые неймспейсы и тд.
Одна из основных особенностей словаря была в том, что это неупорядоченное множество. То есть порядок добавления ключей не гарантирует что они сохранятся в той же последовательности. Но всё изменилось в Python3.6. Как это произошло?
Словарь, как часто используемый тип данных, стараются максимально оптимизировать. Про одну из таких оптимизация нам рассказывает PEP468 - Preserving the order of **kwargs in a function.
Хм, причем здесь оптимизация?
Всё начинается с отдельной имплементации Python под названием PyPy. В этой версии интерпретатора сделали довольно хорошую оптимизацию словарю.
Показательно разница описана на этой странице
Если вкратце, то дело вот в чём.
Словарь на стороне С это массив. Каждый элемент это тоже массив из 3х элементов (хеш ключа, ключ и значение).
Раньше, чтобы всякий раз при обновлении словаря не изменять размер массива в С (это затратно по времени), изначально он делался с запасом. Как только массив заполняется, его еще увеличивают с запасом, обычно на 1/3. При этом элементы, еще не занятые данными, заполнялись пустышками (полный пример на странице по ссылке выше)
🔸 Увеличилась скорость поиска и добавления ключей.
🔸 Сократился расход памяти в 3 раза
Python 2.x-3.5
🔸 Как бонус (или как побочный эффект), мы получаем упорядоченность ключей.
То есть одним выстрелом завалили трёх мамонтов!
#pep
Он присутствует буквально в каждом скрипте.
Именованные аргументы (kwargs), атрибуты объекта (ˍˍdictˍˍ), любые неймспейсы и тд.
Одна из основных особенностей словаря была в том, что это неупорядоченное множество. То есть порядок добавления ключей не гарантирует что они сохранятся в той же последовательности. Но всё изменилось в Python3.6. Как это произошло?
Словарь, как часто используемый тип данных, стараются максимально оптимизировать. Про одну из таких оптимизация нам рассказывает PEP468 - Preserving the order of **kwargs in a function.
Хм, причем здесь оптимизация?
Всё начинается с отдельной имплементации Python под названием PyPy. В этой версии интерпретатора сделали довольно хорошую оптимизацию словарю.
Показательно разница описана на этой странице
Если вкратце, то дело вот в чём.
Словарь на стороне С это массив. Каждый элемент это тоже массив из 3х элементов (хеш ключа, ключ и значение).
Раньше, чтобы всякий раз при обновлении словаря не изменять размер массива в С (это затратно по времени), изначально он делался с запасом. Как только массив заполняется, его еще увеличивают с запасом, обычно на 1/3. При этом элементы, еще не занятые данными, заполнялись пустышками (полный пример на странице по ссылке выше)
entries = [
['--', '--', '--'],
[-8522787127447073495, 'barry', 'green'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[-9092791511155847987, 'timmy', 'red'],
['--', '--', '--'],
[-6480567542315338377, 'guido', 'blue']
]
Перерасход памяти очевиден. И что было предложено? Переделать структуру данных словаря разделив его на данные и индексы.indices = [None, 1, None, None, None, 0, None, 2]
entries = [[-9092791511155847987, 'timmy', 'red'],
[-8522787127447073495, 'barry', 'green'],
[-6480567542315338377, 'guido', 'blue']]
Именно этот принцип повторили в Python 3.6. Что мы получаем в итоге?🔸 Увеличилась скорость поиска и добавления ключей.
🔸 Сократился расход памяти в 3 раза
Python 2.x-3.5
>>> d = {x: x*2 for x in range(100)}
>>> d.ˍˍsizeofˍˍ()
12536
Python 3.6>>> d = {x: x*2 for x in range(100)}
>>> d.ˍˍsizeofˍˍ()
4680
Ведь теперь вместо элемента ['--', '--', '--'] у нас просто None, который, кстати, является одним и тем же объектом где бы он не использовался.🔸 Как бонус (или как побочный эффект), мы получаем упорядоченность ключей.
То есть одним выстрелом завалили трёх мамонтов!
#pep
Python Enhancement Proposals (PEPs)
PEP 468 – Preserving the order of **kwargs in a function. | peps.python.org
The **kwargs syntax in a function definition indicates that the interpreter should collect all keyword arguments that do not correspond to other named parameters. However, Python does not preserved the order in which those collected keyword arguments w...
👍1
В PEP509 описано добавление в структуру данных словаря приватного поля с версией. Что это за версия? Она нужна для ускорения проверки изменений в словаре. Разные механизмы должны следить за целостностью данных (например неймспейса, который суть словарь). Чтобы каждый раз не проверять изменился ли словарь, мы просто можем проверить его версию.
На стороне реализации С в структуру данных словаря добавлена приватная переменная
Как посмотреть версию? Из самого словаря не получится. Есть код в тестах для получения свойства
Чтобы попробовать этот код достаточно повторить то что написано в тестах.
Для Windows следует добавить директорию Lib\test в PYTHONPATH.
Жаль только нет стандартного способа получения версии (или я не нашел?). Я думаю применение нашлось бы)
#pep #tricks
На стороне реализации С в структуру данных словаря добавлена приватная переменная
ma_version_tag, которая изменяется всякий раз при изменении словаря.clear()Если вызван один из этих методов, то версия изменяется. Версия это не хеш и не ID. Каждый словарь имеет свою уникальную версию, даже два одинаковых или два пустых словаря.
pop(key)
popitem()
setdefault(key, value)
__delitem__(key)
__setitem__(key, value)
update(...)
Как посмотреть версию? Из самого словаря не получится. Есть код в тестах для получения свойства
ma_version_tag, используется для прогонки тестов. Чтобы попробовать этот код достаточно повторить то что написано в тестах.
Для Windows следует добавить директорию Lib\test в PYTHONPATH.
>>> import _testcapiИнтересно то, что версия изменится даже если данные будут одинаковыми. Главное сам факт изменения.
>>> d1 = {}
>>> d2 = {}
>>> _testcapi.dict_get_version(d1)
12083
>>> _testcapi.dict_get_version(d2)
12099
>>> d = {1:2}
>>> _testcapi.dict_get_version(d)
12200
>>> d[1] = 2
>>> _testcapi.dict_get_version(d)
12239
Таким образом мы можем узнать а не пытался ли кто-то что-либо сделать с нашим словариком? Жаль только нет стандартного способа получения версии (или я не нашел?). Я думаю применение нашлось бы)
#pep #tricks
Python.org
PEP 509 -- Add a private version to dict
The official home of the Python Programming Language
Python по умолчанию кеширует числа int в диапазоне -5...256.
Думаю, это всем известный факт.
Вот вам подсказка, в интерактивной консоли это можно повторить, запустив обе команды как одну.
А оптимизация замечает нашу "бездарную писанину" и исправляет явные ошибки. Грубо говоря, такой код:
Если выполнять команды по отдельности то оптимизация не сработает, так как это отдельные объекты кода.
Если же это одна команда, введёная в консоль или код загружен из модуля, то интерпретатор увидит это как единый блок. Этап оптимизации изменит исходный код и это сведёт на нет наши исследования, выдав не то что мы ожидаем.
#triks
Думаю, это всем известный факт.
>>> a = 10
>>> b = 10
>>> a is b
True
>>> a = 270
>>> b = 270
>>> a is b
False
Но вот что интересно, это не сработает внутри модуля.# matchtest.py ####
a = 270
b = 270
print(a is b)
# end file ########>>> import matchtest
True
Почему одно и тоже работает по разному? Что за двойные стандарты?Вот вам подсказка, в интерактивной консоли это можно повторить, запустив обе команды как одну.
>>> a = 270; b = 270
>>> a is b
True
А разница в том как интерпретатор получает код, точней какими порциями. Каждую "порцию" он "интерпретирует", попутно оптимизируя логику кода.А оптимизация замечает нашу "бездарную писанину" и исправляет явные ошибки. Грубо говоря, такой код:
a = 270
b = 270
Превращается в нечто такое:a = b = 270
Зачем создавать два одинаковых объекта когда можно создать один объект и две ссылки?Если выполнять команды по отдельности то оптимизация не сработает, так как это отдельные объекты кода.
Если же это одна команда, введёная в консоль или код загружен из модуля, то интерпретатор увидит это как единый блок. Этап оптимизации изменит исходный код и это сведёт на нет наши исследования, выдав не то что мы ожидаем.
#triks
Кроме типа integer кешированию подвергаются и строки, но не все. Строки, которые больше всего подходят для использования в ключах словарей или как имена Python-объектов кешируются для оптимизации доступа к данным. А именно:
🔹 в словарях по ключу
🔹 для методов getattr и setattr
Чтобы строка попала в таблицу interned strings (закешировалась), она должна подходить под следующие правила:
🔸 символы должны входить в список "name characters"
Если коротко, это то что попадает под паттерн regex
🔸 строка должна быть длиной до 4096 символов включительно
В константу также входят строки, которые таковыми становятся в результате оптимизации на этапе компиляции байт кода .
Простые константы
Также к динамически созданным строкам относятся те, что прочитаны из файлов или получены по сети
🔹 в словарях по ключу
🔹 для методов getattr и setattr
Чтобы строка попала в таблицу interned strings (закешировалась), она должна подходить под следующие правила:
🔸 символы должны входить в список "name characters"
Если коротко, это то что попадает под паттерн regex
[a-zA-Z0-9_]
То есть строки, похожие на имена объектов. 🔸 строка должна быть длиной до 4096 символов включительно
>>> a = 'a'*4096
>>> b = 'a'*4096
>>> a is b
True
>>> a = 'a'*4097
>>> b = 'a'*4097
>>> a is b
False
🔸 строка должна быть определена в коде как константа но не создана динамически.В константу также входят строки, которые таковыми становятся в результате оптимизации на этапе компиляции байт кода .
Простые константы
>>> a = 'python'
>>> b = 'python'
>>> a is b
True
Динамически созданная строка>>> a = 'python'
>>> b = ''.join('python')
>>> a is b
False
Оптимизированный код>>> a = 'python'
>>> b = 'pyt'+'hon'
>>> a is b
True
Создание строки b оптимизировано в константу 'python' на этапе компиляции байт кода.Также к динамически созданным строкам относятся те, что прочитаны из файлов или получены по сети
>>> a = 'python'
>>> open(tempfile, 'w').write(a)
>>> b = open(tempfile).read()
>>> a is b
Flase
>>> a = requests.get(url).content()
>>> b = requests.get(url).content()
>>> a is b
Flase
#tricksВ прошлом посте мы узнали, что не все строки кешируются интерпретатором в момент создания. Даже если строка короткая но содержит недопустимые символы, она не закешируется.
Да, это успех! Но что то нам даёт? Узнаем в следующем посте.
#tricks #libs
>>> a = '😁'Но мы можем форсированно закешировать любую строку, обойдя эти правила. Мало ли, вдруг у вас будет словарь где ключ это смайл ))). Для этого просто используйте функцию sys.intern()
>>> b = '😁'
>>> a is b
False
>>> a = sys.intern('😁')
>>> b = sys.intern('😁')
>>> a is b
True
Теперь ваша строка добавлена в таблицу "interned" strings. Да, это успех! Но что то нам даёт? Узнаем в следующем посте.
#tricks #libs
В прошлом посте мы закешировали строки в таблицу “interned strings”
И что мы получаем от этого? Прирост скорости достаточно мал и не будет заметен. Экономия памяти уже получше, но реально увидеть различия можно только на больших массивах данных. Где тогда это применять?
🔸Пример 1
Если вы делаете синтаксический разбор большого текста, вполне имеет смысл закинуть в кеш часто встречающиеся части текста. Например, самые популярные слова. Если их наберется несколько миллионов по всему тексту, то уже хорошая экономия памяти. Да, короткие слова Python сам кеширует, но если вы прочитали их из файла то это следует сделать самостоятельно.
🔸Пример 2
В "этих ваших интернетах" часто приводят такой пример:
Функция intern() помещает строку в таблицу, либо возвращает ссылку на тот же объект если строка там уже есть. И это может очень пригодится для сравнения больших строк. Ведь оператор "is", проверяющий совпадение адреса в памяти, работает куда быстрей чем оператор "==", сравнивающий все символы в строке.
Мы можем закинуть в кеш две строки и просто сравнить их через оператор "is".
Синтетический тест сравнения показывает прирост скорости в 50-55 раз.
Но так ли часто нам надо сравнивать две большие и одинаковые строки столько раз? Этот тест лишь показывает разницу в скорости операторов и тот факт что intern() действительно делает две переменные одним объектом.
Давайте сделаем иначе, вторую строку будем создавать в каждой итерации и сравнивать с эталоном, созданным один раз.
И тут мы получаем просадку по скорости в 10 раз😕!
Почему?
Могу предположить, что intern() для добавления строки в таблицу делает обычное сравнение с другими элементами таблицы, и лишь потом выдаёт результат. То есть, для добавления строки в кеш проверка посимвольно всё равно происходит, но только добавляется еще ряд других операций. В итоге никакой выгоды не получаем.
Итого
Выходит, что самый модный пример про функцию intern() не очень-то пригоден в работе. Реальный профит мы получим если будем использовать эту функцию аналогично задумке её основному назначению — кеширование часто используемых строк, то есть первый пример.
#tricks #libs
И что мы получаем от этого? Прирост скорости достаточно мал и не будет заметен. Экономия памяти уже получше, но реально увидеть различия можно только на больших массивах данных. Где тогда это применять?
🔸Пример 1
Если вы делаете синтаксический разбор большого текста, вполне имеет смысл закинуть в кеш часто встречающиеся части текста. Например, самые популярные слова. Если их наберется несколько миллионов по всему тексту, то уже хорошая экономия памяти. Да, короткие слова Python сам кеширует, но если вы прочитали их из файла то это следует сделать самостоятельно.
🔸Пример 2
В "этих ваших интернетах" часто приводят такой пример:
Функция intern() помещает строку в таблицу, либо возвращает ссылку на тот же объект если строка там уже есть. И это может очень пригодится для сравнения больших строк. Ведь оператор "is", проверяющий совпадение адреса в памяти, работает куда быстрей чем оператор "==", сравнивающий все символы в строке.
Мы можем закинуть в кеш две строки и просто сравнить их через оператор "is".
Синтетический тест сравнения показывает прирост скорости в 50-55 раз.
Но так ли часто нам надо сравнивать две большие и одинаковые строки столько раз? Этот тест лишь показывает разницу в скорости операторов и тот факт что intern() действительно делает две переменные одним объектом.
Давайте сделаем иначе, вторую строку будем создавать в каждой итерации и сравнивать с эталоном, созданным один раз.
И тут мы получаем просадку по скорости в 10 раз😕!
Почему?
Могу предположить, что intern() для добавления строки в таблицу делает обычное сравнение с другими элементами таблицы, и лишь потом выдаёт результат. То есть, для добавления строки в кеш проверка посимвольно всё равно происходит, но только добавляется еще ряд других операций. В итоге никакой выгоды не получаем.
Итого
Выходит, что самый модный пример про функцию intern() не очень-то пригоден в работе. Реальный профит мы получим если будем использовать эту функцию аналогично задумке её основному назначению — кеширование часто используемых строк, то есть первый пример.
#tricks #libs
Все мы любим pathlib за его краткость, логичность и ООП-подход.
История его появления в стандартных библиотеках это пример как надо интегрировать новые принципы в архитектуру языка программирования или любого приложения.
Расскажу кратко, по этапам:
🔸 Сначала у нас был os.path. Это функциональный подход который выглядит громоздко и многословно.
🔸Возникла задача адаптации всех стандартных методов для работы с данной библиотекой. Чтобы каждый из них смог понять объект Path и правильно его обработать. И самое интересное, как это было реализовано.
🔸 Если объект Path конвертнуть в строку
Просить юзеров конвертить в str когда нужно? Тоже нет, не pythonic-way.
В результате в Python 3.6 появляется новый абстрактный класс os.PathLike и понятие path-like object, который понимают все стандартные методы работы с файлами. Теперь, при написании библиотеки для работы с путями, ваша задача следовать правилам этого типа чтобы аккуратно вписаться в экосистему Python-путей.
А правила там простые, magic-метод ˍˍfspathˍˍ (file system path), который возвращает валидный путь.
Все методы для обработки файлов используют os.fspath() для объекта пути перед его использованием.
А если наследоваться не получается то можно воспользоваться методом register
🔸 Вывод
В этой истории показательно то, что вместо внесений изменений под конкретный случай (читай костыль), разработчики создали подходящие условия для всех.
То есть не библиотека диктует правила как с ней обходиться, а язык создаёт правила как нужно подстроиться библиотеке чтобы все были довольны. В результате разработчики не только вписали удобную библиотеку в привычный нам код, но и мы получили возможность писать свои альтернативные системы работы с путями, которые понимаются всеми стандартными методами.
Это принцип за который я сам всегда всеми руками ЗА.
Низкоуровневые решения не должны заниматься частными случаями. Если мы попробуем подстроиться под каждый необычный случай то получим жуткую кашу из if-else, try-except или еще чего похуже.
Когда вас просят поправить ваш api, потому что вот тут в таком-то случае у юзера всё ломается, остановитесь на секунду и подумайте, а точно ли вам нужно делать именно то что просят?
Если ваше решение начинается с if, это неверное решение!
#libs #tricks #pathlib
История его появления в стандартных библиотеках это пример как надо интегрировать новые принципы в архитектуру языка программирования или любого приложения.
Расскажу кратко, по этапам:
🔸 Сначала у нас был os.path. Это функциональный подход который выглядит громоздко и многословно.
#пример переименования🔸 В версии 3.4 появилась библиотека pathlib которая поменяла ход игры. Теперь работаем с путями как с объектами. Кода стало меньше, счастья больше.
import os
my_path = '/path/to/file.ext'
dir_name = os.path.dirname(my_path)
new_name = 'file2' + os.path.splitext(my_path)[1]
new_path = os.path.join(dir_name, new_name)
os.rename(my_path, new_path)
# пример переименования с pathlib🔸 С приходом этой сущности появились и проблемы, старые методы для работы с путями просто не понимают этот тип. Они работают только со строками.
from pathlib import Path
my_path = Path('/path/to/file.ext')
new_path = my_path.with_name('file2').with_suffix(my_path.suffix)
my_path.rename(new_path)
my_path = Path('/path/to/file.ext')
open(my_path)
TypeError: invalid file: PosixPath('...')
То же самое с subprocess и остальными.🔸Возникла задача адаптации всех стандартных методов для работы с данной библиотекой. Чтобы каждый из них смог понять объект Path и правильно его обработать. И самое интересное, как это было реализовано.
🔸 Если объект Path конвертнуть в строку
str(Path) то мы получим правильный путь. Получается, что надо просто добавить форсированную конвертацию аргументов в str везде где это нужно? Нет!, так мы только всё усложим.Просить юзеров конвертить в str когда нужно? Тоже нет, не pythonic-way.
В результате в Python 3.6 появляется новый абстрактный класс os.PathLike и понятие path-like object, который понимают все стандартные методы работы с файлами. Теперь, при написании библиотеки для работы с путями, ваша задача следовать правилам этого типа чтобы аккуратно вписаться в экосистему Python-путей.
А правила там простые, magic-метод ˍˍfspathˍˍ (file system path), который возвращает валидный путь.
Все методы для обработки файлов используют os.fspath() для объекта пути перед его использованием.
class MyPath(os.PathLike):Это сработает и без наследования от os.PathLike, Достаточно и только метода ˍˍfspathˍˍ. Но лучше всё же наследоваться, чтобы добавить дополнительные проверки субклассов.
def __init__(self, val):
self.val = val
def __fspath__(self):
return self.val
path = MyPath('/path/to/file/ext')
f = open(path, 'w') # PROFIT!!!
А если наследоваться не получается то можно воспользоваться методом register
os.PathLike.register(MyPath)Кстати, именно так и поступили в pathlib
🔸 Вывод
В этой истории показательно то, что вместо внесений изменений под конкретный случай (читай костыль), разработчики создали подходящие условия для всех.
То есть не библиотека диктует правила как с ней обходиться, а язык создаёт правила как нужно подстроиться библиотеке чтобы все были довольны. В результате разработчики не только вписали удобную библиотеку в привычный нам код, но и мы получили возможность писать свои альтернативные системы работы с путями, которые понимаются всеми стандартными методами.
Это принцип за который я сам всегда всеми руками ЗА.
Низкоуровневые решения не должны заниматься частными случаями. Если мы попробуем подстроиться под каждый необычный случай то получим жуткую кашу из if-else, try-except или еще чего похуже.
Когда вас просят поправить ваш api, потому что вот тут в таком-то случае у юзера всё ломается, остановитесь на секунду и подумайте, а точно ли вам нужно делать именно то что просят?
Если ваше решение начинается с if, это неверное решение!
#libs #tricks #pathlib
GitHub
cpython/os.py at main · python/cpython
The Python programming language. Contribute to python/cpython development by creating an account on GitHub.
Как получить справку в Python имея только консоль?
🔸 Основная справка
🔸 Справка по объектам
Сначала нужно зайти в REPL (интерактивная консоль Python) и там вводить такой код
Специальный инструмент для работы с документаций в Python.
Доступные команды:
как использовать pydoc
То есть это не статичные HTML страницы из файлов, а сгенерированные на лету из докстрингов.
запустить веб сервер с документацией на порту 8000
Если вы на локальном хосте, то можно открыть браузер введя команду b, или добавить такой же флаг в команду чтобы браузер открылся сам сразу.
Запустить веб сервер и сразу открыть браузер
PS. Чтобы с помощью функции help() получить справку по ключевым словам, следует их писать в виде строки
🔸 Основная справка
python3 -h
Команда вводится в консоль. Даёт информацию по флагам и переменным интерпретатора.🔸 Справка по объектам
Сначала нужно зайти в REPL (интерактивная консоль Python) и там вводить такой код
>>> import some
>>> help(some)
Функция help() достаёт докстринги и распечатывает в консоль. Это самый очевидный способ получить справку по объекту не выходя из консоли. Но в такой метод нельзя передать директивы, например import или def>>> help(import)
SyntaxError: invalid syntax
🔸 модуль pydocСпециальный инструмент для работы с документаций в Python.
Доступные команды:
как использовать pydoc
python3 -m pydoc
показать справку по функции или классуpython3 -m pydoc os.path.join
показать справку по ключевым словам языкаpython3 -m pydoc <keyword>
напримерpython3 -m pydoc import
python3 -m pydoc def
справка по модулямpython3 -m pydoc <modulename>
поиск по документацииpython3 -m pydoc -k <request>
Есть еще одна интересная возможность — запускать веб сервер документации как оболочка для pydoc. То есть это не статичные HTML страницы из файлов, а сгенерированные на лету из докстрингов.
запустить веб сервер с документацией на порту 8000
python3 -m pydoc -p 8000
Теперь можете зайти на этот хост по указанному порту и получите простой сайт с документацией.Если вы на локальном хосте, то можно открыть браузер введя команду b, или добавить такой же флаг в команду чтобы браузер открылся сам сразу.
Запустить веб сервер и сразу открыть браузер
python3 -m pydoc -p 8000 -b
____________________PS. Чтобы с помощью функции help() получить справку по ключевым словам, следует их писать в виде строки
help('import')
#libsДопустим у нас есть какой-то список
Для сортировки этого списка у нас есть два пути:
🔸 функция sorted()
🔸 метод list.sort()
Надеюсь, уловили разницу? Но это было лишь вступление чтобы был ясна следующая тема.
На самом деле я хотел рассказать про операторы "=" и "+=" по отношению к спискам.
Все мы привыкли что запись
Дело в том, что со списками оператор "+" работает аналогично функции sorted(), то есть возвращает новый объект, после чего оператор "=" записывает значение в переменную. В то время как "+=" работает аналогично методу list.sort() — изменяет исходный список.
Вот небольшой пример для проверки:
Можете пройтись функцией id() чтобы точно всё проверить.
Данная фишка не сработает с кортежами, так как они неизменяемые. Оба варианта создают новый объект.
Пример, где это может вызвать неоднозначность. Класс, в атрибутах которого указывается список каких-то дефолтных полей. Во время создания инстанса мы можем их расширять через аргументы.
В конструкторе класса в первом случае мы создаём новый атрибут инстанса
Во втором случае мы меняем именно атрибут класса
#tricks
Для сортировки этого списка у нас есть два пути:
🔸 функция sorted()
>>> a = [3, 1, 2]Думаю, всем очевидно что теперь
>>> b = sorted(a)
>>> print(a, b)
[3, 1, 2] [1, 2, 3]
a и b это разные объекты. Так работает sorted(), то есть получает один список, и возвращает другой список с изменениями. Исходный список не изменяется.🔸 метод list.sort()
>>> a = [3, 1, 2]Метод list.sort() не возвращает новый список. Он вообще ничего не возвращает. Он просто сортирует исходный список.
>>> b = a.sort()
>>> print(a, b)
[1, 2, 3], None
Надеюсь, уловили разницу? Но это было лишь вступление чтобы был ясна следующая тема.
На самом деле я хотел рассказать про операторы "=" и "+=" по отношению к спискам.
Все мы привыкли что запись
x += 3Это просто более короткая версия записи
x = x + 3Но это не всегда так.
Дело в том, что со списками оператор "+" работает аналогично функции sorted(), то есть возвращает новый объект, после чего оператор "=" записывает значение в переменную. В то время как "+=" работает аналогично методу list.sort() — изменяет исходный список.
Вот небольшой пример для проверки:
>>> a = [1, 2]Во второй строке
>>> b = a
>>> a = a + [3, 4]
>>> print(a, b)
[1, 2, 3, 4] [1, 2]
a и b ссылаются на один и тот же обеъект. Но после присвоения результата оператора сложения в переменную a мы создали новый объект и переписали ссылку a.>>> a = [1, 2]А в этом примере переменная
>>> b = a
>>> a += [3, 4]
>>> print(a, b)
[1, 2, 3, 4] [1, 2, 3, 4]
a не перезаписалась, оператор отработал с исходным объектом. Поэтому мы изменили и b тоже.Можете пройтись функцией id() чтобы точно всё проверить.
Данная фишка не сработает с кортежами, так как они неизменяемые. Оба варианта создают новый объект.
Пример, где это может вызвать неоднозначность. Класс, в атрибутах которого указывается список каких-то дефолтных полей. Во время создания инстанса мы можем их расширять через аргументы.
class MyClass:Класс имеет два статических атрибута.
L1 = [0]
L2 = [0]
def __init__(self, fields):
self.L1 = self.L1 + fields
self.L2 += fields
В конструкторе класса в первом случае мы создаём новый атрибут инстанса
L1 который своим именем перекрывает атрибут класса. Такое значение L1 будет только у этого инстанса.Во втором случае мы меняем именно атрибут класса
L2, то есть это будет видно во всех инстансах данного класса.>>> obj1 = MyClass(fields=[1])
>>> print(obj1.L1, obj1.L2)
[0, 1] [0, 1]
>>> obj2 = MyClass(fields=[2])
>>> print(obj2.L1, obj2.L2)
[0, 2] [0, 1, 2]
>>> obj3 = MyClass(fields=[3])
>>> print(obj3.L1, obj3.L2)
[1, 3] [0, 1, 2, 3]
В атрибут класса L2 добавляется элемент при каждом создании инстанса.#tricks
У нас есть список с некоторыми значениями. Предположим что это какие-то дата-классы.
Нам требуется их отсортировать и сложить в словарь, где ключами будут порядковые номера.
Список:
Порядковый номер нам посчитал enumerate, значение сразу записали в словарь под этим номером, в результате для тела цикла действий не осталось 😁
#tricks
Нам требуется их отсортировать и сложить в словарь, где ключами будут порядковые номера.
Список:
values = ['a', 'c', 'f', 'e', 'b', 'g', 'd']"Сложная" функция получения ключа сортировки:
def get_key(obj):Есть вплоне очевидные способы это сделать, но я покажу неочевидный, который совершенно не советую к использованию!
return obj
rating = dict()Вопросы вызывают два момента. Что там делает
for i, rating[i] in enumerate(sorted(values, key=get_key)):
pass
rating[i] и почему цикл ничего не делает? Да-да, pass тут не для краткости примера. Это рабочий код который заполнит словарь rate.>>> print(rating)Запись
{0: 'a', 1: 'b', 2: 'c', ...}
rating[i] заменяет нам имя переменной для цикла. В простом случае нам бы пришлось писать так.for i, value in enumerate(sorted(values, key=get_key)):Но вместо создания переменной
rating[i] = value
value мы сразу записываем очередной элемент в словарь подставляя обращение к словарю по ключу вместо переменной. Python сам за нас выполняет выражение rating[i] = value на каждой итерации.Порядковый номер нам посчитал enumerate, значение сразу записали в словарь под этим номером, в результате для тела цикла действий не осталось 😁
rating = {i:x for i, x in enumerate(sorted(values, key=get_key))}
Но я очень НЕ советую писать такой неочевидный код. Лучше всего старый добрый генератор!#tricks
Думаете что curses крутая библиотека но сложная? Urwid тоже не очень помогает?
Тогда посмотрите на высокоуровневую библиотеку py_cui. Это обертка для curses которая предлагает простую схему построения интерфейсов основанную на GridLayout. Он позволяет располагать различные виджеты плиткой, что-то вроде интерфейса Metro.
Библиотека реализует простой принцип навигации:
🔸 переключение между плитками: стрелки на клавиатуре
В обычном режиме вы можете "гулять" по сетке выбирая нужный виджет.
🔸 активация виджета: Enter
В этом режиме вы входите в контекст виджета и можете с ним взаимодействовать
🔸 деактивация виджета: Esc
По нажатию на Esc вы возвращаетесь в режим выбора виджета
Посмотрите примеры и зацените приложение для работы с GIT. А еще на нем можно простые игры писать.
PS. При желании набор символов для рисования границ виджетов можно изменить.
#libs
Тогда посмотрите на высокоуровневую библиотеку py_cui. Это обертка для curses которая предлагает простую схему построения интерфейсов основанную на GridLayout. Он позволяет располагать различные виджеты плиткой, что-то вроде интерфейса Metro.
Библиотека реализует простой принцип навигации:
🔸 переключение между плитками: стрелки на клавиатуре
В обычном режиме вы можете "гулять" по сетке выбирая нужный виджет.
🔸 активация виджета: Enter
В этом режиме вы входите в контекст виджета и можете с ним взаимодействовать
🔸 деактивация виджета: Esc
По нажатию на Esc вы возвращаетесь в режим выбора виджета
Посмотрите примеры и зацените приложение для работы с GIT. А еще на нем можно простые игры писать.
PS. При желании набор символов для рисования границ виджетов можно изменить.
#libs
Я очень часто работаю в REPL. Удобная штука для разработки, поисков, тестов, дебага...
Иногда случается такая ситуация, когда я делаю вызов какой-либо функции и вижу распечатку результата в консоли. И только потом понимаю что нужно было это сохранить в переменную!
В этом случае выручает одна интересная особенность интерактивной консоли, это переменная "_" (нижнее подчеркивание).
Python по умолчанию сохраняет в неё результат последнего вызова если этот результат не был никуда сохранён.
🔸 Если у вас не REPL, то есть простой запуск скрипта.
🔸 Если вы самостоятельно объявили эту переменную или сделали импорт с этим именем.
Иногда случается такая ситуация, когда я делаю вызов какой-либо функции и вижу распечатку результата в консоли. И только потом понимаю что нужно было это сохранить в переменную!
>>> get_some()А почему бы не выполнить еще раз но уже сохранив в переменную?
<some result>
>>> result = get_some()Да, чаще всего так и делаю, но иногда это неудобно или недопустимо. Например, если результат считается долго или каждый раз он будет другой.
В этом случае выручает одна интересная особенность интерактивной консоли, это переменная "_" (нижнее подчеркивание).
Python по умолчанию сохраняет в неё результат последнего вызова если этот результат не был никуда сохранён.
>>> get_some()То есть, сразу после вызова достаточно скопировать значение из этой переменной
<some result>
>>> print(_)
<some result>
>>> get_some()Это не сработает в двух случаях:
<some result>
>>> result = _
>>> print(result)
<some result>
🔸 Если у вас не REPL, то есть простой запуск скрипта.
🔸 Если вы самостоятельно объявили эту переменную или сделали импорт с этим именем.
>>> _ = False#tricks
>>> get()
<some result>
>>> print(_)
False
Как прочитать файл из ZIP архива не распаковывая этот архив?
Недавно была задача достать данные из JSON файла который лежит в ZIP архиве.
Первое, что приходит в голову – распечатать архив в TEMP и найти нужный файл. Но с Python можно сделать проще: прочитать нужный файл в архиве не извлекая всё содержимое.
Например, есть некий архив
Вот код который это сделает:
Недавно была задача достать данные из JSON файла который лежит в ZIP архиве.
Первое, что приходит в голову – распечатать архив в TEMP и найти нужный файл. Но с Python можно сделать проще: прочитать нужный файл в архиве не извлекая всё содержимое.
Например, есть некий архив
archive.zip. Где-то внутри есть файл config.json который нам надо прочитать.Вот код который это сделает:
from zipfile import ZipFile#tricks #libs
from pathlib import Path
import json
def get_json_from_zip(archive, file_name):
zip = ZipFile(archive)
for zipname in zip.namelist():
if Path(zipname).name == file_name:
with zip.open(zipname) as f:
return json.load(f)
config_name = 'config.json'
archive_path = 'archive.zip'
conf = get_json_from_zip(archive_path, config_name)
Как узнать кто вызвал функцию?
Порой требуется выяснить, кто именно вызвал конкретную функцию?
Конечно, можно запустить дебаг и выполнять построчно логику отслеживая откуда мы пришли в конкретную точку. Но это долго.
Чтобы просто узнать имя функции которая вызвала текущую функцию можно сделать так.
Traceback удобен тем, что показывает цепочку вызовов, которая привела к ошибке.
Но модуль traceback также позволяет распечатать стек вызовов не выбрасывая исключение.
Порой требуется выяснить, кто именно вызвал конкретную функцию?
Конечно, можно запустить дебаг и выполнять построчно логику отслеживая откуда мы пришли в конкретную точку. Но это долго.
Чтобы просто узнать имя функции которая вызвала текущую функцию можно сделать так.
# myscript1.pyЗапускаем
import inspect
def function1():
# распечатаем имя вызывающей функции
print('Called from:', inspect.stack()[1][3])
def function2():
function1()
function2()
python3 myscript1.pyНо так мы увидим лишь имя предыдущей функции. А как узнать полный список вызовов?
Called from: function2
Traceback удобен тем, что показывает цепочку вызовов, которая привела к ошибке.
Но модуль traceback также позволяет распечатать стек вызовов не выбрасывая исключение.
# myscript2.pyЗапускаем файл
import traceback
def function1():
...
traceback.print_stack()
...
def function2():
function1()
function2()
python myscript2.py#tricks
File "myscript.py", line 9, in <module>
function2()
File "myscript.py", line 7, in function2
function1()
File "myscript.py", line 4, in function1
traceback.print_stack()
Какие ассоциации у вас вызывает число 404?
Сразу вспоминается ошибка 404 Not Found (не найдено).
Именно такой номер имеет PEP 404 Python 2.8 Un-release Schedule для несуществующего релиза Python 2.8.
В нём нам сообщают что релиз 2.8 никогда не выйдет и даются советы как перейти на ветку 3.х.
#pep #2to3
Сразу вспоминается ошибка 404 Not Found (не найдено).
Именно такой номер имеет PEP 404 Python 2.8 Un-release Schedule для несуществующего релиза Python 2.8.
В нём нам сообщают что релиз 2.8 никогда не выйдет и даются советы как перейти на ветку 3.х.
#pep #2to3
Роняем Python в одну строку
Как имитировать ошибку Segmentation Fault в коде и уронить процесс интерпретатора?
🔸 Форсированно завершаем процесс через kill и exit code 11.
🔸 Делаем что-то что вызовет ошибку со страшным сообщением что всё сломалось! Перегружаем стек вызова рекурсией, заведомо увеличив лимит до не приличия высоко.
Может кому-то хотите устроить подлянку 👹, а может тестируете дебагер.
Таким падением можно "указать" юзеру что он зашел куда не следует чтобы больше так не делал 😨
Всё это надуманные примеры. Но тем не менее, теперь вы теперь знаете как это сделать😉.
PS. Не советую использовать этот код в рабочих проектах!
#tricks
Как имитировать ошибку Segmentation Fault в коде и уронить процесс интерпретатора?
🔸 Форсированно завершаем процесс через kill и exit code 11.
__import__('os').kill(__import__('os').getpid(), 11)
Но это просто быстрый выход. 🔸 Делаем что-то что вызовет ошибку со страшным сообщением что всё сломалось! Перегружаем стек вызова рекурсией, заведомо увеличив лимит до не приличия высоко.
__import__('sys').setrecursionlimit(1<<30);f=lambda f:f(f);f(f)
🔸Ломаем парсер AST__import__('ast').literal_eval('1+1'*10**6)
Все эти вызовы приводят к такой ошибке:Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)Хм... а зачем это может быть нужно?
Может кому-то хотите устроить подлянку 👹, а может тестируете дебагер.
Таким падением можно "указать" юзеру что он зашел куда не следует чтобы больше так не делал 😨
Всё это надуманные примеры. Но тем не менее, теперь вы теперь знаете как это сделать😉.
PS. Не советую использовать этот код в рабочих проектах!
#tricks
Интересная библиотека tqdm, добавляющая прогресс бар для вашей итерации в CLI скриптах.
Что показывает прогресс бар?
- прогресс в процентах
- прогресс в количестве итераций
- потраченное и оставшееся время выполнения
- скорость выполнения в итерациях в секунду
Использовать очень просто, оберните итератор в tqdm и получите прогресс обработки!
#libs
Что показывает прогресс бар?
- прогресс в процентах
- прогресс в количестве итераций
- потраченное и оставшееся время выполнения
- скорость выполнения в итерациях в секунду
Использовать очень просто, оберните итератор в tqdm и получите прогресс обработки!
from tqdm import tqdmВсё сломается если внутри цикла есть какая-либо печать в stdout
for i in tqdm(iterable_obj):
# do something
#libs
Существуют фреймворки для создания десктоп приложений на базе WebGUI (HTML+CSS+JS).
Например Electron, NW.js и другие. Вся логика у них пишется на JavaScript.
Если хотите писать такие приложения с логикой на Python, то для вас есть аналог, это библиотека Eel.
🔸 Создавайте десктоп-GUI из HTML страниц со всем доступным для них функционалом.
🔸 Вызывайте Python-функции из JavaScript.
🔸 Вызывайте JavaScript функции из Python.
🔸 Есть встроенный сборщик приложения в Standalone через PyInstaller.
Eel запускает сервер и создаёт минималистичный chromium-браузер, который работает с этим сервером.
(Вы также можете открыть GUI вашего приложения и в обычном браузере, пока само приложение запущено)
Я за полчаса собрал нехитрое приложение, выполняющее Python-код введённый в HTML форме.
🌎 https://github.com/paulwinex/eel-webform-example
Для десктоп приложений идеально подойдет концепция SPA (Single Page App).
Мой элементарный пример на Vue.js
🌎 https://github.com/paulwinex/eel-vuejs-example
➡️ На почитать
#libs
Например Electron, NW.js и другие. Вся логика у них пишется на JavaScript.
Если хотите писать такие приложения с логикой на Python, то для вас есть аналог, это библиотека Eel.
🔸 Создавайте десктоп-GUI из HTML страниц со всем доступным для них функционалом.
🔸 Вызывайте Python-функции из JavaScript.
🔸 Вызывайте JavaScript функции из Python.
🔸 Есть встроенный сборщик приложения в Standalone через PyInstaller.
Eel запускает сервер и создаёт минималистичный chromium-браузер, который работает с этим сервером.
(Вы также можете открыть GUI вашего приложения и в обычном браузере, пока само приложение запущено)
Я за полчаса собрал нехитрое приложение, выполняющее Python-код введённый в HTML форме.
🌎 https://github.com/paulwinex/eel-webform-example
Для десктоп приложений идеально подойдет концепция SPA (Single Page App).
Мой элементарный пример на Vue.js
🌎 https://github.com/paulwinex/eel-vuejs-example
➡️ На почитать
#libs