TGINSIGHT CHAT
Python Заметки
@pythonotes
EducationИнтересные заметки и обучающие материалы по Python Контакт: @paulwinex ⚠️ Рекламу на канале не делаю!⚠️ Хештеги для поиска: #tricks #libs #pep #basic #regex #qt #django #2to3 #source #offtop
Неодамнешни објави
Ознака: #tricks · 178 објави
Објавено 24 авг.
Как прочитать файл из ZIP архива не распаковывая этот архив? Недавно была задача достать данные из JSON файла который лежит в ZIP архиве. Первое, что приходит в голову – распечатать архив в TEMP и найти нужный файл. Но с Python можно сделать проще: прочитать нужный файл в архиве не извлекая всё содержимое. Например, есть некий архив archive.zip. Где-то внутри есть файл config.json который нам надо прочитать. Вот код который это сделает: from zipfile import ZipFile 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) #tricks#libs
Објавено 21 авг.
Я очень часто работаю в 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 >>> get() <some result> >>> print(_) False #tricks
Hashtags
Објавено 17 авг.
У нас есть список с некоторыми значениями. Предположим что это какие-то дата-классы. Нам требуется их отсортировать и сложить в словарь, где ключами будут порядковые номера. Список: 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
Hashtags
Објавено 14 авг.
Допустим у нас есть какой-то список Для сортировки этого списка у нас есть два пути: 🔸 функция 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] >>> b = a.sort() >>> print(a, b) [1, 2, 3], None Метод list.sort() не возвращает новый список. Он вообще ничего не возвращает. Он просто сортирует исходный список. Надеюсь, уловили разницу? Но это было лишь вступление чтобы был ясна следующая тема. На самом деле я хотел рассказать про операторы "=" и "+=" по отношению к спискам. Все мы привыкли что запись 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
Hashtags
Објавено 10 авг.
Все мы любим pathlib за его краткость, логичность и ООП-подход. История его появления в стандартных библиотеках это пример как надо интегрировать новые принципы в архитектуру языка программирования или любого приложения. Расскажу кратко, по этапам: 🔸 Сначала у нас был os.path. Это функциональный подход который выглядит громоздко и многословно. #пример переименования 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) 🔸 В версии 3.4 появилась библиотека pathlib которая поменяла ход игры. Теперь работаем с путями как с объектами. Кода стало меньше, счастья больше. # пример переименования с 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): def __init__(self, val): self.val = val def __fspath__(self): return self.val path = MyPath('/path/to/file/ext') f = open(path, 'w') # PROFIT!!! Это сработает и без наследования от os.PathLike, Достаточно и только метода ˍˍfspathˍˍ. Но лучше всё же наследоваться, чтобы добавить дополнительные проверки субклассов. А если наследоваться не получается то можно воспользоваться методом register os.PathLike.register(MyPath) Кстати, именно так и поступили в pathlib 🔸Вывод В этой истории показательно то, что вместо внесений изменений под конкретный случай (читай костыль), разработчики создали подходящие условия для всех. То есть не библиотека диктует правила как с ней обходиться, а язык создаёт правила как нужно подстроиться библиотеке чтобы все были довольны. В результате разработчики не только вписали удобную библиотеку в привычный нам код, но и мы получили возможность писать свои альтернативные системы работы с путями, которые понимаются всеми стандартными методами. Это принцип за который я сам всегда всеми руками ЗА. Низкоуровневые решения не должны заниматься частными случаями. Если мы попробуем подстроиться под каждый необычный случай то получим жуткую кашу из if-else, try-except или еще чего похуже. Когда вас просят поправить ваш api, потому что вот тут в таком-то случае у юзера всё ломается, остановитесь на секунду и подумайте, а точно ли вам нужно делать именно то что просят? Если ваше решение начинается с if, это неверное решение! #libs#tricks#pathlib
Објавено 7 авг.
В прошлом посте мы закешировали строки в таблицу “interned strings” И что мы получаем от этого? Прирост скорости достаточно мал и не будет заметен. Экономия памяти уже получше, но реально увидеть различия можно только на больших массивах данных. Где тогда это применять? 🔸Пример 1 Если вы делаете синтаксический разбор большого текста, вполне имеет смысл закинуть в кеш часто встречающиеся части текста. Например, самые популярные слова. Если их наберется несколько миллионов по всему тексту, то уже хорошая экономия памяти. Да, короткие слова Python сам кеширует, но если вы прочитали их из файла то это следует сделать самостоятельно. 🔸Пример 2 В "этих ваших интернетах" часто приводят такой пример: Функция intern() помещает строку в таблицу, либо возвращает ссылку на тот же объект если строка там уже есть. И это может очень пригодится для сравнения больших строк. Ведь оператор "is", проверяющий совпадение адреса в памяти, работает куда быстрей чем оператор "==", сравнивающий все символы в строке. Мы можем закинуть в кеш две строки и просто сравнить их через оператор "is". Синтетический тест сравнения показывает прирост скорости в 50-55 раз. Но так ли часто нам надо сравнивать две большие и одинаковые строки столько раз? Этот тест лишь показывает разницу в скорости операторов и тот факт что intern() действительно делает две переменные одним объектом. Давайте сделаем иначе, вторую строку будем создавать в каждой итерации и сравнивать с эталоном, созданным один раз. И тут мы получаем просадку по скорости в 10 раз😕! Почему? Могу предположить, что intern() для добавления строки в таблицу делает обычное сравнение с другими элементами таблицы, и лишь потом выдаёт результат. То есть, для добавления строки в кеш проверка посимвольно всё равно происходит, но только добавляется еще ряд других операций. В итоге никакой выгоды не получаем. Итого Выходит, что самый модный пример про функцию intern() не очень-то пригоден в работе. Реальный профит мы получим если будем использовать эту функцию аналогично задумке её основному назначению — кеширование часто используемых строк, то есть первый пример. #tricks#libs
Објавено 5 авг.
В прошлом посте мы узнали, что не все строки кешируются интерпретатором в момент создания. Даже если строка короткая но содержит недопустимые символы, она не закешируется. >>> a = '😁' >>> b = '😁' >>> a is b False Но мы можем форсированно закешировать любую строку, обойдя эти правила. Мало ли, вдруг у вас будет словарь где ключ это смайл ))). Для этого просто используйте функцию sys.intern() >>> a = sys.intern('😁') >>> b = sys.intern('😁') >>> a is b True Теперь ваша строка добавлена в таблицу "interned" strings. Да, это успех! Но что то нам даёт? Узнаем в следующем посте. #tricks#libs
Објавено 3 авг.
Кроме типа integer кешированию подвергаются и строки, но не все. Строки, которые больше всего подходят для использования в ключах словарей или как имена Python-объектов кешируются для оптимизации доступа к данным. А именно: 🔹 в словарях по ключу 🔹 для методов 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
Hashtags
Објавено 29 јул.
В PEP509 описано добавление в структуру данных словаря приватного поля с версией. Что это за версия? Она нужна для ускорения проверки изменений в словаре. Разные механизмы должны следить за целостностью данных (например неймспейса, который суть словарь). Чтобы каждый раз не проверять изменился ли словарь, мы просто можем проверить его версию. На стороне реализации С в структуру данных словаря добавлена приватная переменная ma_version_tag, которая изменяется всякий раз при изменении словаря. clear() pop(key) popitem() setdefault(key, value) __delitem__(key) __setitem__(key, value) update(...) Если вызван один из этих методов, то версия изменяется. Версия это не хеш и не ID. Каждый словарь имеет свою уникальную версию, даже два одинаковых или два пустых словаря. Как посмотреть версию? Из самого словаря не получится. Есть код в тестах для получения свойства 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
Објавено 20 јул.
Регулярно требуется преобразовать какой-либо текст в максимально совместимый текст для URL, имени файла, имени объекта в каком-то софте и тд. Требования совместимости простые: в тексте должны быть только допустимые символы. Обычно это a-z, 0-9 и "_" или "-". То есть, только прописные буквы латинского алфавита и цифры (как пример). Допустим, нам нужно название статьи в блоге преобразовать в 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
Објавено 15 јул.
Так что же такое этот Magic Number? Это набор байтов, уникальный для определённого типа файла. Он еще называется Сигнатура файла. Не каждый файл имеет магическое число, например текстовые файлы в них не нуждаются. По этому набору битов можно точно определить какого типа бинарный файл открыт. Если программе очень важно не перепутать тип файла, то она будет определять его именно по сигнатуре, а не по имени файла. В Python для скомпилированных PYС-файлов магическое число отличается от версии к версии. По нему можно определить версию интерпретатора, которым скомпилирован этот байт-код. Пример библиотеки для определения версии Получить magic number текущей версии можно так: Python 3 >>> from importlib import util >>> util.MAGIC_NUMBER.hex() Python 2 >>> import imp >>> imp.get_magic().encode('hex') Интерпретатор использует это значение для проверки PYC-файлов перед импортом. Если версия не подходящая вы увидете ошибку: RuntimeError: Bad magic number in .pyc file То есть, помимо типа файла, магическое число может также обозначать разные версии одного типа. Итого, сигнатура файла помогает: 🔸 быстро определить формат файла вне зависимости от имени (например для запуска соответствующего приложения в OS) 🔸 обозначать совместимость одного и того же бинарного формата с разными версиями софта (пошло из Unix систем) 🔸разделить бинарники программ по вариантам сборки 🔸 восстанавливать файлы при потере таблицы файлов диска 🔸 использовать быстрый поиск файлов по типу без обращения к таблице файлов 🔸 получить типа файла, передаваемого по сети, не качая его целиком Список этим, конечно же, не ограничивается. #libs#tricks
Објавено 13 јул.
fleep не поддерживает нужный тип файла? Не нашли подходящую сигнатуру в интернете? Тогда пробуйте ➡️puremagic, еще больше типов! Возможно самая актуальная библиотека по данной теме. Всё ещë нет нужной сигнатуры? Видимо, у вас сложный случай. Остаëтся только найти "магическое число" вашего файла самостоятельно. Делается это достаточно просто. Нужно посмотреть на файл в шестнадцатеричном представлении. Первые биты файла будут вашим искомым значением. Для просмотра можно использовать: 🔸mcedit. Редактор который идёт в поставке с mc (Linux). Жмем F3 для просмотра и сразу F4 для переключения режима. 🔸xxd (что это?) Пример для Linux xxd myfile.ext | head head не даёт прочитать весь файл. Нам нужно лишь начало. Для Windows тот же xxd, который идет в поставке с Git ...\Git\usr\bin\xxd.exe -l 100 myfile.ext Флаг "l" аналогичен head на Linux Теперь проходимся по нескольким файлам этого формата и ищем совпадающие первые биты, которые всегда одинаковы. Нужное число найдено! #libs#tricks