Python Заметки
前往频道在 Telegram
Интересные заметки и обучающие материалы по Python Контакт: @paulwinex ⚠️ Рекламу на канале не делаю!⚠️ Хештеги для поиска: #tricks #libs #pep #basic #regex #qt #django #2to3 #source #offtop
显示更多2 373
订阅者
无数据24 小时
-17 天
-1130 天
帖子存档
2 373
Порой бывает необходимо работать с JSON файлами ручками, читая или изменяя данные. И очень не удобно, когда юникод в файле записан в виде кодированных символов.
>>> import json
>>> data = {'title': 'Привет Медвед!'}
>>> print(json.dumps(data))
'{"title": "\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442 \\u041c\\u0435\\u0434\\u0432\\u0435\\u0434!"}'
Эх, безобразие! Ни прочитать нормально, ни поправить.
Чтобы такое поведение изменить, достаточно добавить аргумент ensure_ascii=False
>>> json.dumps(data, ensure_ascii=False)
'{"title": "Привет Медвед!"}'
Теперь символы не кодируются в Unicode. В файл запишется в таком же виде.
____________________
Для тех кто в танке (всё еще на Python 2 🚂 ).
Строку следует делать как unicode, и для записи в файл использовать модуль codecs.
>>> import json, codecs
>>> data = {'title': u'Привет Медвед!'}
>>> with codecs.open(path, "w", encoding='utf-8') as f:
>>> json.dump(data, f, ensure_ascii=False)
#libs #tricks2 373
Часто используете Python в терминале? Скорее всего вас не особо устраивает дефолтный REPL.
Советую попробовать прокаченные версии интерактивного шела:
🔸bpython
- подсветка синтаксиса
- автокомплиты
- инлайн подсказки параметров функций
- история команд
- авто отступы
Установка:
pip3 install bpython
Сайт 🌎
https://bpython-interpreter.org/
🔸 ptpython
- подсветка синтаксиса
- автокомплиты
- поддержка мышки
- авто отступы
- цветовые темы
Установка:
pip3 install ptpython
Сайт 🌎
https://github.com/prompt-toolkit/ptpython
Также можно глянуть:
➡️ www.asmeurer.com/mypython
➡️ xon.sh
➡️ ipython.org
#libs2 373
В стандартной поставке Python есть один полезный инструмент в библиотеке collections, это класс deque.
он очень похож на простой список но он намного быстрее работает в некоторых случаях.
Например для обработки элементов в начале списка у него есть дополнительные методы: extendleft(), appendleft() и popleft().
Запустим пару тестов!!! 🚀
>>> from collections import deque
>>> import time
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l1 = deque()
>>> for i in range(5000):
>>> l1.append(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test deque: {en}sec')
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l2 = list()
>>> for i in range(5000):
>>> l2.append(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test list: {en}sec')
Test deque: 0.452sec
Test list: 0.436sec
Добавление в конец списка работает примерно одинаково. Теперь попробуем вставлять элемент в начало массива.
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l1 = deque()
>>> for i in range(5000):
>>> l1.appendleft(i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test deque: {en}sec')
>>>
>>> st = time.perf_counter()
>>> for _ in range(1000):
>>> l2 = list()
>>> for i in range(5000):
>>> l2.insert(0, i)
>>> en = round(time.perf_counter()-st, 3)
>>> print(f'Test list: {en}sec')
Test deque: 0.435sec
Test list: 6.347sec
Прирост производительности почти в 15 раз! 😲
Тестим функцию pop() (опустим код теста для краткости)
Test deque: 0.48sec Test list: 0.529secТеперь pop(0) для list и popleft() для deque
Test deque: 0.476sec Test list: 3.101secБыстрей примерно в 6.5 раз. Почему так быстро? Дело в том что deque это некий аналог такого типа данных как "linked list data structure", в Python это занывается "двусвязные списки" (doubly-linked lists). Это список, но не в привычном представлении, а с особой оптимизированной структурой. При создании такого массива данные никуда не переносятся а только линкуются оттуда где были. Да, это похоже на Python-лист, но линковка происходит иначе. Вместо того чтобы собирать некий стек ссылок и назвать его списком, в doubly-linked list каждый элемент просто ссылается на следующий. Такая структура позволяет значительно ускорить, создание списка, изменение с любой стороны. Где это может пригодиться? Конечно же в двусторонних очередях (double-ended queue). То есть когда мы добавляем элементы в начало а забираем с конца, или наоборот. Минус такого подхода в просадке производительности для произвольного доступа к элементам в середине очереди. Полный код тестов 🌎 ______________ Реальное описание несколько сложней, я постарался передать основную суть. #libs #tricks
2 373
Почему не стоит в коде использовать assert для проверки данных?
Действительно, команда очень удобна для быстрой проверки правдивости какого-либо факта.
assert isinstance(value, int), "Value must be type int"
Но почему советуют делать это только в тестах? Дело в том, что эта команда сделана именно для тестов и есть специальный режим когда она глобально отключается и не работает.
Есть такая builtin константа __debug__, которая по умолчанию имеет значение True (и это не изменить в коде). Именно она указывает, будут ли работать ваши assert'ы.
Стоит запустить интерпретатор в режиме оптимизации (флаг -O), константа __debug__ будет равна False, и все ваши проверки будут проигнорированы.
python -O script.py
Поэтому всегда используйте raise.
if not isinstance(value, int):
raise TypeError("Value must be type int")
#tricks2 373
Вы всё еще проверяете секретные данные оператором сравнения?
>>> if password == user_password: >>> ...Это небезопасный способ сравнения для стендалон приложений. Он уязвим к такому типу атаки как timing attack, позволяющий делать выводы и угадывать пароль на основании времени проверки. Чтобы сделать безопасное сравнение используйте метод secrets.compare_digest(). Он защитит операцию проверки от подобных атак.
>>> import secrets >>> if secrets.compare_digest(password, user_password): >>> ...Возможно вы ранее слышали про метод hmac.compare_digest(). Он не только делает то же самое, это один и тот же метод!
>>> import secrets >>> import hmac >>> hmac.compare_digest is secrets.compare_digest True#libs
2 373
В прошлом посте мы рассмотрели синхронный планировщик задач sched. Функционал вроде не плох, но синхронное выполнение с блокировкой всё портит☹️. Можем ли мы как-то поправить ситуацию?
На самом деле можем (помимо отправки всего в subprocess).
Функция run() принимает аргумент blocking, который по умолчанию True.
То есть, если мы укажем
blocking=False то получим неблокирующее выполнение? Нет. Этот параметр работает иначе.
Если мы делаем неблокирующий запуск, то после вызова метода run(blocking=False) планировщик выполнит все задачи, которым пришло время исполниться в обычном блокирующем синхронном режиме и вернёт время, через которое следует запуститься следующей задаче. То есть через какое время нужно запустить run(...) еще раз.
Получается, что вместо ожидания таймаута без полезной нагрузки планировщик освобождает поток и сообщает через сколько ему пора будет продолжить работу. А что делать с этой информацией, решаете сами.
Выполнение следующих задач произойдет после следующего вызова run() и если пришло их время выполниться.
Например, вместо ожидания следующей задачи будем делать что-то полезное:
import sched, time
def func(name):
# задача
t = round(time.time()-start_time, 2)
print(f"Execute {name} ({t}s)")
start_time = time.time()
# создаём планировщик
s = sched.scheduler(time.time, time.sleep)
s.enter(0, 1, func, argument=('ev1',))
s.enter(1, 1, func, argument=('ev2',))
s.enter(2, 1, func, argument=('ev3',))
delay = 0
last_run_time = time.time()
while True:
# выполнение заданий планировщика
if last_run_time + delay < time.time():
delay = s.run(blocking=False)
if delay is None:
break
last_run_time = time.time()
# здесь делаем что-то полезное
print('Делаем что-то полезное...')
time.sleep(0.1)
print('Complete all tasks')
Планировщик остаётся по-прежнему синхронным, но теперь вместо бесполезного ожидания мы можем запустить другой код на исполнение и знаем когда следует вернуться к планировщику.
___________________
PPS. Да, в этих примерах я нагло использую глобальные переменные))) Не делайте так на реальных проектах2 373
В Python есть стандартный модуль sched для синхронного планировщика задач. Что??? Синхронных??? В наш-то век "асинхронщины" и "параллельщины"!
Спокойно, сначала смотрим код, потом разбираемся.
Работает это так:
- создаём планировщик
- добавляем задачи в очередь с таймаутом и приоритетом
- запускаем и ждём пока завершится вся очередь
Смотрим пример:
import sched, time
def func(name):
t = round(time.time()-start_time, 2)
print(f"Execute {name} ({t}s)")
# просто отметка времени старта
start_time = time.time()
# создаём планировщик
s = sched.scheduler(
time.time, # функция замера времени
time.sleep # функция ожидания
)
# добавляем задачи
s.enter(0, 1, func, argument=('ev1',))
s.enter(2, 1, func, argument=('ev2',))
s.enter(1, 1, func, argument=('ev3',))
# запускаем очередь на исполнение
s.run()
Описание аргументов
Вывод получаем в соответствии со временем задержки:
Execute ev1 (0.0s)
Execute ev3 (1.0s)
Execute ev2 (2.0s)
Функция run() запускает планировщик и начинается выполнение задач в порядке очереди по времени и приоритету.
Время указывается с момента старта очереди. Если время совпадает то сортировка идёт по приоритету.
Что значит синхронный планировщик? Это значит что задачи будут выполняться строго по очереди в одном потоке. Никаких мультипотоков и мультипроцессов, модуль прост как бревно!
Если у первой задачи стоит задержка 0сек а у второй 1сек, и при этом первая задача выполняется 3сек, то вторая задача выполнится только через 3 сек. Никакого параллельного запуска не будет.
Параметр delay следует понимать не как "запусти через N сек" а как "запусти не раньше чем через N сек". Время запуска следующей зависит от выполнения предыдущих задач.
Где это может пригодиться? Очередь задач, между которыми должен быть промежуток времени по какой-либо причине.
🔸 несколько синхронных задач, которые должны выполняться друг за другом но требующие ожидания обновления какой-либо инфраструктуры (тормозная сеть?)
🔸 сетевой API который имеет лимит на количество команд в единицу времени
🔸 фейковая задержка для генерации тестов или имитация поведения юзера.
Конечно, всё это можно решить банальным time.sleep() в нужном месте, но sched даёт несколько более удобный интерфейс управления задачами.
#libs2 373
Python 3.9 готовит нам приятный сюрприз в PEP584.
Ранее мы обсуждали как можно удобно сложить вместе два словаря получив новый словарь. Было много вариатов, но ни одного идеального. Наконец-то в Python добавили оператор для слияния словарей!!! Это оператор "|".
А это значит, что начиная с 3.9 соединить словари можно таким синтаксисом:
dct3 = dct1 | dct2
Чтобы обновить словарь, можно использовать такой синтаксис
dct1 |= dct2
Данный функционал уже можно опробовать, установив первые релизы.
#pep2 373
Допустим, имеется у нас задача: разделить одно число на другое и получить отдельно целое число и остаток от деления.
Например, исходные числа 15 и 2. В результате должны получить 7 и 1. То есть 7 раз двойка входит в состав 15 целиком и потом остаётся еще 1.
Как будем действовать?
Очевидно же, целое число вхождений получаем через floor division
>>> 15//2
7
Остаток через деление по модулю
>>> 15%2
1
Но можно сделать проще (ох, ну куда уж проще то 😄). В Python есть builtin функция divmod которая делает эти два действия в одно.
>>> divmod(15, 2)
(7, 1)
#tricks2 373
"Ну и как же нам перекидывать строки ви числа?" спросите вы. Проверять каждый символ, очистив строку от лишних знаков и точек. Потом конвертить допустимые символы в числа и восстанавливать знак, дробную чусть и тд???
Самый быстрый способ это просто "попробовать" 😜
text = "-0.3"
try:
num = float(text)
except ValueError:
print('Dough!')
Всё остальное это уже парсинг и разбор символов для иных целей.
#basic2 373
В продолжение прошлого поста про цифры в мире строк.
Почему методы isdigit() и isnumeric() не определяют в строке float и отрицательные значения?
Дело в том, что эти методы работают с ЦИФРАМИ, то есть с единичным символом. А строка "-2" или "3.4" это уже ЧИСЛО. То есть не символ а значение, записанное несколькими символами.
Все озвученные методы проходятся по каждому символу строки и проверяют их индивидуально.
В юникоде есть символы цифр с точками "🄀⒈⒉⒊⒋⒌⒍⒎⒏⒐"
Каждая из них это ОДИН СИМВОЛ, поэтому он будет считаться цифрой
>>> '⒌'.isdigit(), '⒌'.isnumeric() True, TrueНо когда мы пишем это выражение в два символа ( 5+точка), то это не работает.
>>> '5.'.isdigit(), '5.'.isnumeric() False, FalseА еще есть такие символы
>>> '⑴⑵⑶⑷⑸'.isdigit() True >>> '🄁🄂🄃'.isdigit() TrueНо они не преобразуются в десятичные цифры
>>> '⒈'.isdecimal()
False
>>> '🄃'.isdecimal()
False
>>> '⑶'.isdecimal()
False
#basic2 373
У строки в Python есть два очень похожих метода. На столько похожих что кажется они делают одно и тоже.
Это метод isdigit() и isnumeric()
Давайте посмотрим зачем нам два одинаковых метода? И так ли они одинаковы?
Очевидно что isdigit() говорит нам, состоит ли строка только из чисел 0-9
>>> '12'.isdigit()
True
>>> '12x'.isdigit()
False
>>> '-12'.isdigit()
False
>>> '12.5'.isdigit()
False
Можно предположить что isnumeric() делает более глубокий анализ и распознаёт в строке float или отрицательное число.
>>> '15'.isnumeric()
True
>>> '-15'.isnumeric()
False
>>> '15.2'.isnumeric()
False
Нет, всё так же как и с другим методом. В чем же тогда разница? Для начала посмотрим следующие примеры:
>>> '5'.isdigit(), '5'.isnumeric() # Обычная цифра 5
# True, True
>>> '꧕'.isdigit(), '꧕'.isnumeric() # Яванская 5
# True, True
>>> '෩'.isdigit(), '෩'.isnumeric() # Синхала 3
# True, True
>>> '৩'.isdigit(), '৩'.isnumeric() # Бенгальская 3
# True, True
>>> '༣'.isdigit(), '༣'.isnumeric() # Тибетская 3
# True, True
>>> '³'.isdigit(), '³'.isnumeric() # 3 верхний индекс (степень)
# True, True
>>> '𝟝'.isdigit(), '𝟝'.isnumeric() # Математическая двойная 5
# True, True
>>> '๔'.isdigit(), '๔'.isnumeric() # Тайская 4
# True, True
>>> '➑'.isdigit(), '➑'.isnumeric() # 8 в круге
# True, True
А теперь примеры в которых, по мнению Python, результаты не равны
>>> '¾'.isdigit(), '¾'.isnumeric() # дробь три четверти
# False, True
>>> '⅕'.isdigit(), '⅕'.isnumeric() # дробь одна пятая
# False, True
>>> '𒐶'.isdigit(), '𒐶'.isnumeric() # клинопись 3
# False, True
>>> '三'.isdigit(), '三'.isnumeric() # 3 из унифицированной идеограммы
# False, True
>>> '⑩'.isdigit(), '⑩'.isnumeric() # цифра 10 в круге
# False, True
>>> 'Ⅳ'.isdigit(), 'Ⅳ'.isnumeric() # Римская 4
# False, True
>>> '𑇪'.isdigit(), '𑇪'.isnumeric() # Сенегальская архаическая 10
# False, True
>>> '𐌢'.isdigit(), '𐌢'.isnumeric() # Этрусская цифра 10
# False, True
>>> 'ↂ'.isdigit(), 'ↂ'.isnumeric() # Римская цифра 10000
# False, True
>>> '〇'.isdigit(), '〇'.isnumeric() # Символ ККЯ ноль
# False, True
Получается, что isdigit() говорит нам, является ли символ десятичной цифрой или спецсимволом, имеющим цифирное значение после преобразования.
В свою очередь isnumeric() включает все дополнительные символы юникода которые имеют отношения к числовым и цифровым представлениям.
Ну и пара примеров в которых в обоих случаях символ не является числом, это эмодзи.
>>> '🕙'.isdigit(), '🕙'.isnumeric() # эмодзи 10 часов
# False, False
>>> '7️⃣'.isdigit(), '7️⃣'.isnumeric() # эмодзи 7
# False, False
Также есть еще один дополнительный и весьма полезный метод isdecimal().
Он нам сообщает, можно ли из указанного символа сделать простую десятичную цифру. То есть сработает ли метод int(x)
>>> '෩'.isdecimal(), int('෩') # Синхала 3
# True, 3
>>> '➑'.isdecimal(), int('➑') # 8 в круге
# False, ValueError
Какие выводы?
🔸 При определении цифры в строке isdigit() подходит лучше чем isnumeric(), но оба не гарантируют успешную конвертацию в int
🔸 Для однозначного определения возможности преобразования строки в int лучше подходит метод isdecimal()
🔸 Для однозначного определения символов 0...9 лучше использовать regex
Полный список символов юникода которые определяются как numeric
#basic2 373
В Python всё является объектами.
Это значит что у каждой сущности есть тип и какие-либо методы.
Мы знаем что есть методы у строк
>>> 'string'.upper()
у списков
>>> [1,2,3].count(2)
у словарей
>>> {"key": 123}.items()
А есть ли какие-то методы у простых чисел? Не много, но есть!
Например, возьмём простой int
>>> a = 22
Метод bit_length() покажет сколько потребуется бит для отображения данного числа в двоичном представлении, исключая ведущие нули.
>>> a.bit_length()
5
Проверяем
>>> bin(a).lstrip('-0b')
'10110'
Всё верно.
Проверим float
>>> b = 10.5
Мы можем проверить есть ли у числа дробная часть
>>> b.is_integer()
False
Получить наш float в виде простой десятичной дроби
>>> b.as_integer_ratio()
(21, 2)
Конечно же Python не имеет типа "десятичная дробь", поэтому мы просто получаем кортеж из двух элементов: числитель и знаменатель.
У int тоже есть такой метод (Python3.8+), но он работает "хитро". Целое число всегда равно дроби где в числителе это же число а в знаменателе 1. Поэтому данный метод у int всегда возвращает (x, 1). 😕
Кстати, чтобы обойтись без переменной просто возьмите число в скобки
>>> (10.0).is_integer()
True
#tricks2 373
Срез это довольно удобная штука для получения определённого диапазона элементов из списка.
Но срезы не так просты как кажется, их можно использовать и для изменения оригинального списка так же как мы это делаем просто по индексу.
Для примера возьмем простой список
ls = [1, 2, 3, 4, 6]
И поехали!
>>> ls[-1] = 5
[1, 2, 3, 4, 5]
Обычное изменение по индексу.
>>> ls[2:4]
[3, 4]
Обычный срез который создаёт новый список на основе старого.
>>> ls[::-1]
[5, 4, 3, 2, 1]
Реверс списка, тоже создаёт новый. Оригинальный список остался без изменений.
Воспользуемся оператором присвоения:
>>> ls[2:4] = [7, 8]
[1, 2, 7, 8, 5]
Заменили диапазон элементов в оригинальном списке.
>>> ls[4:] = [0, 0, 0, 0]
[1, 2, 7, 8, 0, 0, 0, 0]
Указав диапазон сверх имеющегося мы расширили список по аналогии с методом extend(), но при этом еще и немного захватили конец списка. Всё это в одно действие!
>>> ls[:0] = [9, 8, 7]
[9, 8, 7, 1, 2, 7, 8, 0, 0, 0, 0]
Добавили элементы в начало
>>> del ls[-4:]
[9, 8, 7, 1, 2, 7, 8]
Удалили часть элементов списка
>>> ls[1:3] = []
[9, 1, 2, 7, 8]
Еще один способ удалить элементы
>>> ls[:] = []
А этим способом можно пользоваться для очистки списка в Python2, в котором еще не было метода clear().
Кажется мы "потратили" весь наш список)))
Сделаем новый и продолжим.
>>> ls = [1, 2, 3, 4, 5, 6]
>>> ls[::2] = [7,8,9]
[7, 2, 8, 4, 9, 6]
И конечно же мы можем использовать шаг в срезе, но тут требуется соблюдать количество подаваемых элементов. Заменять элементы с каким-то шагом можно, а добавлять нельзя.
s = slice(3, 4)
Как было показано ранее, срез можно сохранять как переменную и использовать в дальнейших манипуляциях
>>> ls[s] = [0]*5
[7, 2, 8, 0, 0, 0, 0, 0, 9, 6]
Заменили один элемент на несколько элементов, расширив исходный список из центра.
И напоследок. Следующие два действия равнозначны и дадут одинаковый результат — полная замена списка.
>>> ls[:] = [1,2,3,4]
>>> ls = [1,2,3,4]
#tricks2 373
Наверняка вы часто используете генераторы списков (List Comprehension). Я тоже, даже порой злоупотребляю ими) Но что поделать, если они такие удобные!
Давайте разберёмся как работают составные итерации в генераторе списка.
Для начала посмотрим простой вид.
>>> array = [1, 2, 3, 4]
>>> [x for x in array]
Этот код просто делает копию списка. Добавляем некоторое выражение:
>>> [x*2 for x in array]
Этот генератор создаёт новый список элементы которого в 2 раза больше чем в оригинальном списке. Добавим условие:
>>> [x*2 for x in array if x%2]
Предыдущему примеру добавили фильтр, который проходят только нечётные числа.
В целом, я бы советовал на этом и остановиться. Не нужно усложнять простое!
Мы можем итерировать в генераторе сразу два списка так, чтобы каждый элемент одного списка повстречался с каждым элементом из другого.
>>> array1 = [1, 2, 3]
>>> array2 = [4, 5, 6]
>>> print([f'{i}-{j}' for i in array1 for j in array2])
['1-4', '1-5', '1-6', '2-4', '2-5', '2-6', '3-4', '3-5', '3-6']
Давайте обозначу каждый цикл условными скобками чтобы было понятней (синтаксически это неверно).
[f'{i}-{j}' (for i in array1) (for j in array2)]
Можем добавить условие и сюда? Можем!
>>> print([f'{i}-{j}' (for i in array1 if i >= 2) (for j in array2 if j < 5)])
['2-4', '3-4']
При этом во второй итерации мы можем использовать переменную из первой
>>> array = [(1, 2, 3), (4, 5, 6)]
>>> new = [y for x in array for y in x]
Кстати, мы разложили вложенные кортежи в один плоский список.
Можем ли мы использовать больше двух итераций? Да хоть десять!
>>> a1 = [9, 2, 4, 5]
>>> a2 = [4, 5, 9, 1]
>>> a3 = [5, 9, 3, 0]
>>> match = [x for x in a1 for y in a2 for z in a3 if x == y and x == z]
[9, 5]
Нашли числа которые встречаются во всех списках.
Можно еще сложней? Конечно, Python довольно многое позволяет делать.
Но, как говорят про Python: если ЭТО можно сделать то еще не значит что ЭТО нужно делать.
#tricks2 373
В прошлом посте мы получали полное имя метода. В атрибут ˍˍqualnameˍˍ записано имя только до класса, к которому метод принадлежит. Как быть если нужно полное имя с модулем, а мы имеем только объект метода без инстанса или ссылки на класс?
func = get_my_method()
В результате этого псведо-выражения мы имеем только сам метод класса. Есть ли в нём информация к какому классу он принадлежит? Конечно есть, это атрибут ˍˍselfˍˍ. Он ссылается на инстанс. А оттуда, как не трудно догадаться, можно получить и ссылку на класс через атрибут ˍˍclassˍˍ:
cls = func.__self__.__class__
итого полное имя будет выглядеть так
full_name = '.'.join([
func.__self__.__class__.__module__,
func.__qualname__]
)
#tricks2 373
Каждый модуль в Python имеет атрибут ˍˍnameˍˍ. В него записывается строка, содержащая полное имя модуля. Например:
>>> from my_package import my_module
>>> print(my_module.__name__)
'my_package.my_module'
Очень удобно для логгинга когда в имя логгера требуется записать имя модуля, в котором происходят события лога.
Но однажды мне потребовалось создать логгер не для модуля в целом, а для отдельной функции. Что в этом случае можно сделать?
Такой объект как функция тоже имеет атрибут ˍˍnameˍˍ
>>> class MyClass:
>>> def func(self):
>>> pass
>>> print(MyClass.func.__name__)
'func'
Итого нам следует собрать полное имя таким образом:
>>> full_name = '.'.join([__name__, MyClass.__name__, MyClass.func.__name__])
>>> print(full_name)
'my_package.my_module.MyClass.func'
А что если класс вложен в другой класс?
>>> class MyClass:
>>> class SubClass:
>>> def func(self):
>>> pass
>>> full_name = '.'.join([__name__, MyClass.__name__, MyClass.SubClass.__name__, MyClass.SubClass.func.__name__])
>>> print(full_name)
'my_package.my_module.MyClass.SubClass.func'
А что если вложений несколько?
>>> class MyClass:
>>> class SubClass1:
>>> class SubClass2:
>>> def func(self):
>>> pass
Даже не буду писать пример получения имени 😭.
А что если функция не из этого класса а унаследована и вы работаете с внешней библиотекой?
>>> from somewhere.something import OtherClass
>>> class MyClass2(OtherClass):
>>> pass
Cтрашно подумать как нам достать настоящее "полное имя" функции! А если там динамическое определение наследования или метаклассы? Вообще можно забить на решение и придумать другой способ 😵
На помощь приходит PEP3155 и его имплементация "Qualified name".
Всё очень просто, начиная с Python 3.3 классы и функции имеют атрибут ˍˍqualnameˍˍ, который и содержит полное или "честное" или "квалифицированное" имя объекта. Именно в него уже записан готовый полный адрес объекта. И теперь даже такая конструкция сработает правильно:
>>> # some_module.py
>>>
>>> class OtherClass:
>>> class SubClass1:
>>> class SubClass2:
>>> def func(self):
>>> pass
>>>
>>> # my_module.py
>>> from some_module import OtherClass
>>> class FinalClass(OtherClass.SubClass1.SubClass2):
>>> pass
>>>
>>> full_name = '.'.join([__name__, FinalClass.func.__qualname__])
'my_package.my_module.OtherClass.SubClass1.SubClass2.func'
Тут стоит заметить, что если функция находится не в текущем модуле а откуда-то импортирована (как в моём примере) то собирать имя с локальным атрибутом ˍˍnameˍˍ уже не имеет особого смысла. Тогда лучше использовать имя модуля исходного класса:
>>> full_name = '.'.join([FinalClass.__module__, FinalClass.func.__qualname__])
'some_module.OtherClass.SubClass1.SubClass2.func'
#pep2 373
Ранее я делал серию постов про битовые операторы.
Вот вам ещё один наглядный пример как это используется в Python в модуле
re.
Чтобы указать флаг для компилятора нам надо указать его после передаваемой строки. Например, добавляем флаг для игнорирования переноса строки.
pattern = re.compile(r"(\w+)+")
words = pattern.search(text, re.DOTALL)
А как указать несколько флагов? Ведь явно будут ситуации когда нам потребуется больше одного. Кто читал посты по битовые операторы уже понял как.
pattern.search(text, re.DOTALL | re.VERBOSE)
А теперь смотрим исходники, что находится в этих атрибутах?
Не удивительно, степени двойки. Почему? Потому что каждое следующее значение это сдвиг единицы влево.
>>> for n in [1, 2, 4, 8, 16, 32, 64, 128, 256]:
>>>
print(bin(n))
0b1
0b10
0b100
0b1000
0b10000
0b100000
0b1000000
0b10000000
0b100000000
Чтобы было понятней, давайте напишем тоже самое но иначе, добавим ведущие нули:
000000001
000000010
000000100
000001000
000010000
000100000
001000000
010000000
100000000
Не понятно что тут происходит? Читай три поста про битовые операторы начиная с этого ➡️ https://t.me/pythonotes/45
В общем, это пример применения побитовых операций в самом Python.
Теперь вы знаете Python еще немного лучше)
#tricks #regex #libs2 373
Хотите оформить свои CLI скрипты красивым прогрессбаром?
Стоит посмотреть на библиотеку progress.
Она даёт возможность создавать красивые прогрессбары легко и быстро.
Но что если не хочется добавлять лишнюю зависимость только ради одной бегущей полоски?
Создать свою функцию с подобным функционалом — не проблема!
Вот базовый набросок:
import time
for i in range(0, 100+1, 4):
time.sleep(0.1)
print('\r{:>3}% [{}{}]'.format(i, "#"*i, '-'*(100-i)), end='')
print()
Запустите код в интерактиве и увидите что работает не хуже.
Вся хитрость в отсутствии переноса на новую строку в конце строки и в символе "\r" — возврат "каретки" (читай "курсора") в начало строки. После чего мы перезаписываем предыдущую строку.
Главное в самом конце не забыть обычный print() чтобы перейти на новую строку.
Расширенный вариант в виде контекст менеджера. 🌎
#libs #tricks