TGINSIGHT CHAT
Python Заметки
@pythonotes
EducationИнтересные заметки и обучающие материалы по Python Контакт: @paulwinex ⚠️ Рекламу на канале не делаю!⚠️ Хештеги для поиска: #tricks #libs #pep #basic #regex #qt #django #2to3 #source #offtop
Неодамнешни објави
Ознака: #tricks · 178 објави
Објавено 14 јан.
Ещё пара триков с форматированием! ▫️Порядки больших чисел можно разделять запятой (и только запятой) >>> '{:,}'.format(1_231_312) '1,231,312' ▫️Если оставить пробел в форматировании float после ":" то он добавится в строку >>> '{: .2f}'.format(15) ' 15.00' Но если подать отрицательное число, то знак минуса займёт этот пробел >>> '{: .2f}'.format(-15) '-15.00' Удобно для формирования строк одинаковой длины независимо от знака числа. #tricks
Hashtags
Објавено 12 јан.
При форматировании числа в строку можно задать паддинг - заполнение нулями до нужной длины. >>> '{}_v{:05d}'.format('name', 125) 'name_v00125' А что если паддинг тоже задан переменной? В таком случае мы можем добавить форматирование этой переменной внутри формата первой. Порядок переменных следует указывать в порядке появление открывающейся скобки. >>> '{}_v{:0{}d}'.format('name', 125, 5) 'name_v00125' Или вот так >>> '{}_v:{:{:02d}{}}'.format('name', 125, 5, 'd') 'name_v00125' А можно глубже? К сожалению (а может к счастью), уровень вложенности ограничен двумя. То есть, форматирование внутри форматирования внутри форматирования это ... ValueError: Max string recursion exceeded Но никто не запрещает использовать один тип форматирования внутри другого. (Пример странный, но зато рабочий) >>> ('{}_v{:{:%0.2dd}{}}' % 2).format('name', 125, 5, 'd') 'name_v00125' С f-string можно использовать 4 уровня, по количеству доступных типов кавычек. Можно, но не нужно 😕 f'''{f"""{f'{f"{x}"}'}"""}''' #tricks
Hashtags
Објавено 1 дек.
Если часто работаете с архивами, то наверняка знакомы с модулем zipfile. Нет ничего сложного добавить директорию в архив. import zipfile from pathlib import Path dir_name = '~/input_files' zip_name = '~/archive.zip' with zipfile.ZipFile(zip_name, 'w') as zip: for file in Path(dir_name).glob('**/*'): zip.write(file, file.relative_to(dir_name).as_posix()) Довольно немногословно. Но можно короче! В модуле shutil уже есть готовый метод dir_name = '~/input_files' zip_name = '~/archive' zip_file = shutil.make_archive(zip_name, 'zip', root_dir=dir_name) А что насчет распаковки? Здесь проще, даже с zipfile это одна строка zip_file = '~/archive.zip' out_dir = '~/out_dir' zipfile.ZipFile(zip_file).extractall(out_dir) Ну и тем более в shutil shutil.unpack_archive(zip_file, out_dir) В примерах не делается expanduser для краткости ▫️ В данном случае функция из shutil более универсальна, так как второй аргумент format задаёт алгоритм сжатия, от чего зависит выбор библиотеки. Если написать формат tar, то вместо zipfile будет использоваться tarfile. Поддерживаются форматы zip, tar, gztar, bztar, xztar. Но только если на текущем хосте доступны соответствующие библиотеки. ▫️ Из недостатков можно назвать невозможность запаковать просто один файл. Источником может быть только директория. Проблема легко решается, но всё же. ▫️ Интересный момент. При наличии в Python2 функции shutil.make_archive() там отсутствует shutil.unpack_archive(). А появился он только в 3.7! Не очень понятно почему, но это еще один повод переходить на Python3😊 #libs#tricks
Објавено 26 ное.
В модуле logging предусмотрен немного необычный способ форматирования строки без форматирования. logging.info('Message %s %s', arg1, arg2) На самом деле, если вы его не используете то вы делаете неправильно!⚠️ Если вам требуется указать в строке сообщения какой-либо аргумент то обычно это делается форматированием строки logging.info('New value is %s' % value) Или любой другой доступный нам способ logging.info(f'New value is {value}') logging.info(f'{value=}') Кажется, всё логично, все так делают. Но нет, это ошибка! 😫 Функция записи сообщения должна быть очень быстрой. Да, она в любом случае занимает время, но чем меньше тем лучше. И особенно, когда это сообщение не проходит по фильтру уровня логирования. Например, у меня установлен уровень WARNING и выполняется вот такой вызов logging.debug(f'Current user: {user}') Что произойдет? Сообщение не попадает под установленный уровень логирования и будет проигнорировано. Это обрабатывается сразу же первой командой в вызываемой функции debug. Но при этом форматирование строки всё равно произойдёт! И проблема не в самом форматировании, которое достаточно быстрое (даже при складывании строк через "+"), а в тех возможных действиях, которые придется вызвать для преобразования объекта user в строку. Возможно, там будет запрос в БД, разбор больших массивов данных или еще что-то не очень быстрое (или не очень умное🤪). Нам всё это придётся посчитать чтобы потом.....ничего с этим не сделать. Поэтому правильно писать так: logging.debug('Current user: %s', user) Мы просто передаём подготовленную неформатированную строку и аргументы для форматирования, это не требует вычислений. Но само форматирование и сопутствующие вызовы произойдут только в случае когда это действительно потребуется, то есть уровень сообщения попадает под условия настройки текущего логгера. #libs#tricks
Објавено 1 окт.
Особенно внимательно за закрытием файлов нужно следить в задачах где вы обрабатываете много файлов. Операционная система имеет ограничение на количество открытых файлов процессом и вы быстро можете дойти до этого предела. import os lst = [] for i in range(100000): lst.append(open(os.devnull, 'w')) OSError: [Errno 24] Too many open files: 'nul' Чтобы узнать лимит на Linux вызовите команду ulimit -n (Полный список лимитовulimit -a) Изменить лимит можно командой ulimit -n 2048 На Windows можно это сделать с помощью кода >>> import ctypes >>> ctypes.windll.msvcrt._getmaxstdio() 512 >>> ctypes.windll.msvcrt._setmaxstdio(2048) 2048 ➡️ Отсюда вывод: Всегда явно закрывайте файлы! Иначе можете упереться в ограничения системы. #tricks
Hashtags
Објавено 29 сеп.
В Python есть удобный режим, определяющий код с неверно закрытыми ресурсами. Этот режим называется Development Mode и включается двумя способами: Переменная окружения export PYTHONDEVMODE=1 python3 app.py Аргументы python3 -X dev app.py Если не закрыть файл должным образом, то вы получите в консоль ResourceWarning. Вот пример файла # app.py import psutil, os open('testfile', 'w') 123 print('process handlers:', psutil.Process(os.getpid()).open_files()) В этом примере я использую пакет psutil, чтобы убедиться, что перед выходом открытых файлов в моём процессе не осталось. При этом ResourceWarning всё равно будет выброшен, причём еще до использования psutil. app.py:3: ResourceWarning: unclosed file <_io.TextIOWrapper name='testfile' mode='w' encoding='UTF-8'> open('testfile', 'w') Object allocated at (most recent call last): File "app.py", lineno 3 open('testfile', 'w') process handlers: [] Для отображения строки с ошибкой требуется включить tracemalloc, тоже с помощью переменой или аргументов запуска. Смотрите примеры в доке. ➡️ Отсюда вывод: Всегда явно закрывайте файлы! Пишите чистый и предсказуемый код. #tricks#libs
Објавено 27 сеп.
В одном из прошлых постов был вопрос в комментариях по поводу закрытия файла. Тогда я бегло пояснил что Python сам собирает мусор в памяти. Давайте пройдемся по этому вопросу более внимательно. Когда вы открываете файл не сохраняя его в переменную, на самом деле файл остаётся открытым. Ссылка на него теряется где-то в памяти пока сборщик мусора не доберется до него. Давайте сделаем несколько экспериментов. Откроем файл не сохраняя его в переменную и поищем этот объект в памяти. Для поиска будем использовать вот такую функцию def check_file(): import io, gc for obj in gc.get_objects(): if isinstance(obj, io.TextIOWrapper) and \ obj.name == 'testfile': print('File', obj.name, 'is closed:', obj.closed) return print('Not found') Тест первый: файл, открытый в функции def func(): open('testfile', 'w') Вызываем функцию и проверяем наличие файла >>> func() >>> check_file() Not found Здесь всё предсказуемо. Все локальные переменные функции удаляются когда функция завершится. А при удалении файл закрывается. Тест второй: теперь откроем файл просто в коде, можно в интерактивной консоли. open('testfile', 'w') И снова он не сохранён в переменную, а значит будет удалён сборщиком мусора. Проверим: >>> check_file() File testfile is closed: False Бывалые сразу же скажут, что файл улетел в переменную "_", и будут правы! >>> print(_) <_io.TextIOWrapper name='testfile' mode='w' encoding='UTF-8'> Давайте обнулим её, можно просто распечатать любое число в консоли >>> 123 >>> print(_) 123 Проверим >>> check_file() Not found ➡️ Отсюда вывод: Всегда явно закрывайте файлы! Не рассчитывайте что кто-то (gc) сделает это за вас. _________________ Мои примеры порой содержат оптимизации в угоду краткости но в ущерб правильности. #tricks
Hashtags
Објавено 20 сеп.
Наверняка вы знаете что такое перегруженная функция в С++. Если нет, то всё просто. В С++ можно создать несколько функций с одинаковым названием но разными типами аргументов. И это будут разные функции. Во время вызова функции будет выбрана та её версия которая подходит по типам аргументов. Такая конструкция называется параметрический полиморфизм. Это удобно, когда мы точно не знаем какого типа прилетит аргумент и хотим обработать разные ситуации. Как мы это поведение можем повторить в Python? Обычно через проверку типов. def func(value): if isinstance(value, int): return func_int(value) elif isinstance(value, str): return func_str(value) else: raise NotImplementedError Не очень красиво 🧐 Начиная с версии 3.4 в Python добавили способ делать "перегруженные" функции более элегантно. Это декоратор singledispatch. Создадим нашу исходную функцию которая по умолчанию выбрасывает ошибку. @singledispatch def func(value) raise NotImplementedError Декоратор добавил для объекта func новую функцию register() с помощью которой можем регистрировать перегруженные функции. В качестве аргумента указывайте тип который данная функция обрабатывает @func.register(int) def func_int(x): print("INT:", x) Вместо указания типа в аргументах можно использовать аннотации аргумента функции @func.register def func_str(x: str): print("STR:", x) Если не указать тип одним из этих способов то получите ошибку TypeError. Имя новой функции не имеет значения, часто её называют просто "_" @func.register(list) def _(x): print("LIST", x) Если одна функция должна обработать несколько типов, то просто наслаиваем декоратор @func.register(float) @func.register(Decimal) def _(x): print(f'{type(x).__name__.upper()}:', x) Теперь у нас есть 5 отдельных функций которые вызываются в зависимости от типа передаваемого аргумента. При этом всё выглядит логично и компактно. Обработка каждого случая находится в своей отдельной функции! >>> func(1) INT: 1 >>> func('Python') STR: Python >>> func(1.2) FLOAT: 1.2 >>> func({}) NotImplementedError ------ ◽️ Данный способ работает только с первым аргументом. Все остальные аргументы будут переданы как есть и не участвуют в выборе нужной функции. ◽️ В версии 3.8 доступен декоратор singledispatchmethod с таким же функционалом но для методов класса. #tricks#libs
Објавено 30 авг.
Какой тип данных выбрать для оптимального хранения информации? Имеется в виду объем занимаемой памяти. Можем создать 4 разных варианта объектов и сравнить сколько они занимают оперативки. Будем создавать простой класс, класс со слотами, именованный кортеж и словарь. from collections import namedtuple from sys import getsizeof NT = namedtuple('NT', 'v1 v2 v3') class CLASS: def init(self, x1, x2, x3): self.v1 = x1 self.v2 = x2 self.v3 = x3 class SLOTS: slots = ['x1', 'x2', 'x3'] def init(self, x1, x2, x3): self.x1 = x1 self.x2 = x2 self.x3 = x3 d = dict(x1=1, x2=2, x3=3) c = CLASS(1, 2, 3) s = SLOTS(1, 2, 3) t = NT(1, 2, 3) Теперь распечатаем что там по памяти print(' CLS\t\tSLT\t\tDCT\t\tTPL') print(f'System: {getsizeof(c)}\t\t' f'{getsizeof(s)}\t\t' f'{getsizeof(d)}\t\t' f'{getsizeof(t)}' ) CLS SLT DCT TPL System: 48 56 232 64 Хм, в этой статистике обычный класс самый экономный! Но что-то здесь не так. Неужели он экономичней класса со слотами, который рассчитан на скорость и оптимизацию? Дело в том, что функция sys.getsizeof() показывает не совсем то что мы ожидаем. Она берет результат метода __sizeof__ у объекта и добавляет кое-чего от gc. __sizeof__ возвращает размер, занимаемый данными. Но не учитывает размер обвязки этих данных. А еще он не гарантирует точность размера типов для third-party расширений. Для точного измерения размера лучше использовать модуль pympler. Помимо данных он считает сколько места занимает вся структура классов и другая обвязка объекта. from pympler import asizeof print(f'Pympler: {asizeof.asizeof(c)}\t\t' f'{asizeof.asizeof(s)}\t\t' f'{asizeof.asizeof(d)}\t\t' f'{asizeof.asizeof(t)}' ) CLS SLT DCT TPL Pympler: 416 152 496 160 И вот тут класс со слотами оказывается самым оптимальным решением! И это правильно. А словарь в этом тесте оказался самый расточительный. #tricks#libs
Објавено 18 авг.
Что-то вы гоните насчет "привычного вида формата 755 и 644". Я вот вообще не понял что это!😳 Действительно, что означают цифры которые мы получили в прошлом посте? Это кодировка, заключающая в себе режимы доступа к файлу. Подробней можно почитать в статье про chmod. Там можно увидеть альтернативное обозначение того же самого с помощью символов r w x, что значит чтение, запись, исполнение. Чтобы преобразовать восьмеричное число в такое обозначение в Python есть готовая функция >>> stat.filemode(0o755) '?rwxr-xr-x' Мы видим 3 группы по 3 символа, дающие 3 типа доступа для 3 типов юзеров. А что за знак вопроса в начале? Давайте передадим в эту функцию необрезанное значение от os.stat >>> stat.filemode(os.stat(path).st_mode) 'drwxr-xr-x' Это данные, которые мы безжалостно обрезали в прошлый раз😼 Первый символ обозначает тип объекта. Это может быть файл (-), директория (d) или симлинк (l). Вот простая схема данной кодировки [1][3][3][3] │ │ │ │ │ │ │ └──> Others Permissions │ │ └─────> Group Permissions │ └────────> Owner Permissions └───────────> File Type (разверните экран если вы с телефона) Если вы попробуете получить пермишены для симлинка то получите пермишены для файла >>> path = '.venv/bin/python3' >>> stat.filemode(os.stat(path).st_mode) '-rwxr-xr-x' Чтобы получить свойства именно симлинка, нужно это явно указать >>> stat.filemode(os.stat(path, follow_symlinks=False).st_mode) 'lrwxrwxrwx' #tricks#basic
Објавено 16 авг.
Как получить значение прав доступа к файлу в виде привычного формата записи 755 или 644? В целом, способ вот такой: >>> path = '...' >>> print(oct(os.stat(path).st_mode & 0o777).split('o')[-1]) '755' Теперь разберёмся что всё это значит. До того как мы сделали split() строка была следующего вида: 0o755 Что это за тип данных? Это тип int в восмиричной системе исчисления. Для преобразования в такой формат есть builtin функция oct() >>> oct(493) '0o755' Как видите, результат возвращается виде строки. Но это не мешает нам создавать переменные синтаксисом восьмеричных чисел, то есть с префиксом 0o. Хотя, при распечатке мы всё равно получим int. >>> x = 0o777 >>> print(x) 511 Преобразовать эту строку в int можно функцией int(), указав базис 8 >>> int('0o755', 8) 493 Теперь посмотрим что нам возвращает os.stat >>> perm = os.stat(path).st_mode 33261 Преобразуем в oct >>> oct(perm) '0o100755' Уже почти то что надо. Чтобы оставить только нужное, отрезаем лишнее с помощью оператора & (AND). Для этого последние 3 значения ставим максимальными, остальные нулевыми. >>> perm & 0o777 493 Оператор работает с бинарным представлением чисел, то есть операция была вот такая: 0b100000111101101 & 0b111111111 = 0b111101101 Вспоминаем побитовые операторы Остаётся преобразовать в восьмеричное представление и убрать префикс >>> oct(493) '0o755' >>> oct(493).split('o')[-1] '755' Именно в таком виде пермишены файла в коде обычно не используются. Хранить и использовать их удобно в виде восьмеричного int os.chmod(path, 0o755) или строки os.chmod(path, int('0o755', 8)) А зачем в строке если достаточно в int? Наприрмер чтобы в json была удобочитаемая запись. Так как сериализатор запишет восьмеричное число как обычный десятичный int >>> json.dumps(0o755) '493' Поэтому для удобства пишем его строкой а потом преобразуем в восьмеричный int. #tricks
Hashtags
Објавено 4 авг.
У Python есть очень удобная штука - стек вызовов. Благодаря нему модуль traceback может показать где и что сломалось в момент выброса исключений. Но иногда Python просто падает без какой-либо информации. Лично меня это регулярно настигает на Windows и очень бесит. Совершенно не понятно что где произошло и как искать причину. Ставить сишные дебаггеры? Чтобы быстро понять в какой строке ошибка иногда достаточно использовать встроенный модуль faulthandler Базовое использование очень простое: python.exe -q -X faulthandler main.py Если интерпретатор упадёт, то вы хотя бы узнаете какая строчка привела к такому событию. Попробуйте уронить Python без faulthandler и с ним. #libs#tricks