Python Заметки
2.31K subscribers
59 photos
2 videos
2 files
211 links
Интересные заметки и обучающие материалы по Python

Контакт: @paulwinex

⚠️ Рекламу на канале не делаю!⚠️

Хештеги для поиска:
#tricks
#libs
#pep
#basic
#regex
#qt
#django
#2to3
#source
#offtop
Download Telegram
Знаете ли вы про "магические" методы классов ˍˍgetattributeˍˍ() и ˍˍgetattrˍˍ()?

ˍˍgetattributeˍˍ вызывается всякий раз когда идёт обращение к атрибуту объекта. Например метод или какая-то переменная.

ˍˍgetattrˍˍ вызывается когда атрибут не найден.

И вот тут-то начинается самое интересное. Вы можете написать алгоритм определения динамического атрибута. Вы получаете его имя и можете решить, что вернуть учитывая запрошенное имя. Например, можете сделать запрос в базу данных или просто вернуть значение по умолчанию.

Но пост на самом деле не об этом. Дело в том, что в Python 3.7 добавили возможность определять функцию ˍˍgetattrˍˍ() в модуле с аналогичной функциональностью! (PEP562)

То есть, создав внутри модуля функцию ˍˍgetattrˍˍ() мы определяем действие в случае запроса несуществующего атрибута модуля. Ровно так же как это делаем в классах. Тем самым можно динамически определять состав модуля.

А еще можно создать функцию ˍˍdirˍˍ(), определяющую поведение для стандартной функции dir().

Пример модуля:

# example.py

my_variables = {'var1': 1, "var2": 2}

def __getattr__(name):
try:
return my_variables[name]
except KeyError:
raise AttributeError

def __dir__():
return list(my_variables.keys())

Как использовать модуль

>>> import example
>>> print(example.var1)
1
>>> print(example.var3)
AttributeError

>>> print(dir(example)))
['var1', 'var2']

#tricks #pep
Вторая по частоте future-функция, которую я использовал, это абсолютный импорт

from __future__ import absolute_import

Что она делает?
Изменения, которые вносит эта инъекция описаны в PEP328
Покажу простой пример.

Допустим, есть такой пакет:

/my_package
/__init__.py
/main.py
/string.py

Смотрим код в my_package/main.py

# main.py
import string

Простой пример готов) Вопрос в том, какой модуль импортируется в данном случае? Есть два варианта:

1. модуль в моём пакете my_package.string
2. стандартный модуль string

И вот тут вступает в дело приоритет импортов. В Python2 порядок следующий: помимо иных источников, раньше ищется модуль внутри текущего пакета, а потом в стандартных библиотеках. Таким образом мы импортнём my_package.string.

Но в Python3 это поведение изменилось. Если мы указываем просто имя пакета, то ищется именно такой модуль, игнорируя имена в текущем пакете. Если мы хотим импортнуть именно подмодуль из нашего пакета то, мы должны теперь явно это указывать.

from my_package import string

или относительный импорт, но с указанием пути относительно текущего модуля main

from . import string

Еще одной неоднозначностью меньше 😎

Подробней про импорты здесь:
https://docs.python.org/3/tutorial/modules.html

#2to3 #pep #basic
Ранее я уже упоминал о другой фишке из ˍˍfutureˍˍ , это оператор деления.

from __future__ import division

Суть проста. Раньше сложность типа данных результата поределялась типом самого сложного операнда.
Например:

int/int => int
int/float => float

В первом случае оба операнда int, значит и результат будет int. Во втором float более сложный тип, поэтому результат будет float.

Если нам требуется получить дробное значение при делении двух int то приходилось форсированно один из операндов конверировать в float.

12/float(5) => float

Но с новой "философией" это не требуется. В Python3 "floor division" заменили на "true division" а старый способ теперь работает через оператор "//".

>>> 3/2
1.5
>>> 3//2
1


То есть теперь деление int на int даёт float если результат не целое число.
В классах теперь доступны методы __floordiv__() и __truediv__() для определения поведения с этими операторами.

Данный переход описан в PEP238.

#pep #2to3 #basic
Думаете ˍˍfutureˍˍ нужен только для Python2? Нет, это процесс постоянный. В Python3 тоже есть свои "future".

from __future__ import annotations

Похоже на бекпорт аннотаций типов из Python3 в Python2, но это не так. Это имплементация PEP 563 Postponed Evaluation of Annotations которая будет дефолтной только в Python4.

Что делает этот future?
Если вы активно используете аннотации типов в Python3 то знаете, как они обычно выглядят.

def func(x: int) -> int:
return x * 2

При этом у объекта функции появляется атрибут ˍˍannotationsˍˍ

>>> print(func.__annotations__)
{'x': <class 'int'>, 'return': <class 'int'>}

Видно, что в этой переменной находится словарь. Ключ словаря это имя аргумента функции, а значение это непосредственно ссылка на тип. А так же есть тип для return.

В более сложных случаях это будут инстансы объектов из модуля typing

from typing import List

def func(x: int) -> List[int]:
return [x] * 10

>>> print(func.__annotations__)
{'x': <class 'int'>, 'return': typing.List[int]}

Что происходит если мы импортим annotations из ˍˍfutureˍˍ? Изменяется способ определения аннотаций.

Дело в том, что все эти инстансы типов создаются в момент определения функции когда модуль импортируется. Так же как и значения по умолчанию они должны исполниться и вернуть какой-то объект. В нашем случае это ссылка на тип int или инстанс класса typing.List.
И тут возникает две проблемы, описаные в PEP.
Если коротко то звучит это так:

1. Если в подсказке указывается тип, который еще не определён, это вызовет ошибку. Приходится описывать его просто строкой или менять порядок определения.

2. Определение ссылок на типы порой занимает много времени, так как требуется импорт модулей и создание инстансов.

Но если вы импортируете наш future, то активируется так называемое отложенное определение ссылок (Postponed Evaluation of Annotations).

В результате вместо создания инстансов и ссылок в ˍˍannotationsˍˍ просто записывается строка с этой подсказкой.

from __future__ import annotations

def func(x: int) -> int:
return x * 2

>>> print(func.__annotations__)
{'x': 'int', 'return': 'int'}

В дальнейшем ваши IDE и ресолверы типов могут брать эти строки и исполнять самостоятельно где то на фоне, когда все необходимые типы уже определены.
Для преобразования этих строк в привычный вид есть готовая функция

>>> import typing
>>> typing.get_type_hints(func)
{'x': <class 'int'>, 'return': <class 'int'>}

Но делать это можно уже только по необходимости.

#pep
Каждый модуль в 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'

#pep
Python 3.9 готовит нам приятный сюрприз в PEP584.
Ранее мы обсуждали как можно удобно сложить вместе два словаря получив новый словарь. Было много вариатов, но ни одного идеального. Наконец-то в Python добавили оператор для слияния словарей!!! Это оператор "|".
А это значит, что начиная с 3.9 соединить словари можно таким синтаксисом:

dct3 = dct1 | dct2

Чтобы обновить словарь, можно использовать такой синтаксис

dct1 |= dct2

Данный функционал уже можно опробовать, установив первые релизы.

#pep
Есть такое понятие как Switch Statement. Это некоторая конструкция в языке программирования предназначенная для множественного ветвления алгоритма.
вот примеры реализаций в разных языках:

JavaScrpt
C++ (или здесь)
C#
Ruby
PHP
Go
Delphi
и даже Pascal

В целом, шаблон такой:

switch query:
case match1:
...
case match1:
...

А что у нас в Python?

if condition1:
...
elif condition2:
...
elif condition2:
...
else:
...

Вполне рабочий вариант. Но явно отличается от примеров выше.

И тут внезапно!!! 23 июня 2020г выходит в свет PEP622
И что мы видим? Планы на Python 3.10 по добавлению Switch Statement! Называется он Structural Pattern Matching, но по сути мы получаем тот же синтаксис что и в Switch Statement.

match some_expression:
case pattern_1:
...
case pattern_2:
...

В данный момент статус его еще Draft. Интересно как он еще изменится и доживет ли концепция до релиза? Учитывая что один из автором сам Guido van Rossum, можно сказать что внедрят точно!
Пока рано его разбирать, просто подождем...

#pep
Словарь это очень распространённый тип данных в Python.
Он присутствует буквально в каждом скрипте.
Именованные аргументы (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
👍1
В 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
Какие ассоциации у вас вызывает число 404?
Сразу вспоминается ошибка 404 Not Found (не найдено).

Именно такой номер имеет PEP 404 Python 2.8 Un-release Schedule для несуществующего релиза Python 2.8.

В нём нам сообщают что релиз 2.8 никогда не выйдет и даются советы как перейти на ветку 3.х.

#pep #2to3
Почему рекомендуют каждый импорт делать на новой строке?
Просто так написано в PEP8, скажете вы. Да, это действительно так. Но PEP8 это стилистические рекомендации. Какая практическая польза от такой записи?
Несколько раз мои студенты спрашивали зачем писать длинней когда можно короче? Не это ли один из основных принципов в Python?

Как мы хотим писать:

import os, sys, subprocess

Как рекомендуют

import os
import sys
import subprocess

Если "нельзя" писать в одну строку, то зачем добавили такую возможность - перечислять имена импорта?

Объясняю в два этапа:

🔸Перечисление добавлено для возможности импорта через from, когда из модуля требуется импортнуть несколько имён

from os.path import join, expanduser, sep

В PEP328 добавили возможность перечисление вставлять в скобки чтобы переносить на новую строку без экранирования символа новой строки

Было

from os.path import join, expanduser, \
sep, basename, exists

Стало

from os.path import (join, expanduser,
sep, basename, exists)

🔸 Практическая польза от импортов на разных строках заметна когда вы делаете слияние разных веток кода с конфликтами или просто с изменениями.

Допустим, есть два варианта кода

# file1.py
import fnmatch
import time
import json
import uuid

#file2.py
import fnmatch, time, json, uuid

Зачем-то нам потребовалось изменить импорт, вместо uuid импортим sys. Что покажет нам diff?

# file1.py
-import uuid
+import sys

# file2.py
-import fnmatch, time, json, uuid
+import fnmatch, time, json, sys

Заметили разницу? Пусть даже для GIT это совершенно не проблема, кодревью будет происходить удобней с точки зрения человека. Да, это объяснение человека, который сам делает кодревью (и регулярно сам же нарушает эту рекомендацию, о чем потом жалеет 😭)

Есть ли у вас еще доводы в пользу каждого импорта на новой строке?

#tricks #pep
По аналогии с PEP у Django есть DEP.
Самый интересный для меня на данный момент на это DEP 0009: Async-capable Django. Он про то, как будет внедряться поддержка аснихронности.

Начиная с версии 3 в Django начали появляться асинхронные плюшки. Это всё еще мало чтобы делать асинхронное приложение, но долгий путь начинается с одного маленького шага!

Всё должно пройти в несколько этапов и к 4й версии обещают сделать Django асинхронным!

Что это даёт разработчикам в случае если весь фреймворк станет поддерживать async?

- Ускорение работы web-приложения? Если правильно писать асинхронный код, то да.

- Усложнение кода? Возможно, но фреймворк на то и фреймворк, чтобы прятать сложности где-то внутри. Надеюсь код усложнится не сильно, посмотрим...

И когда нам этого ожидать? Судя по этой схемке Django 4 выйдет в Декабре 2021 года. А это значит, что у вас есть примерно год чтобы научиться понимать асинхронный код, если еще не умеете😁

#django #pep
Подразумеваемые неймспейсы или неявные пакеты.

Этот функционал добавлен в Python 3.3
Что он означает?

Ранее, до 3.3 пакетами считались лишь директории, в которых есть файл __init__.py.
Этот файл одновременно являлся свидетельством того, что директория это Python-пакет, и служил "телом" этого пакета. То есть местом, где можно написать код, как это делается внутри модуля. Этот код исполняется в момент импорта пакета, так что его принято называть "код инициализации пакета".

Начиная с версии 3.3 Любая директория считается пакетом и Python будет пытаться использовать любую директорию для импорта.

Конечно, не любую в файловой системе, а только те что находятся в sys.path.

Это значит, что теперь __init__.py нужно делать только если:

🔸 вам требуется создать код инициализации пакета
🔸 нужна совместимость со старыми версиями Python

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

repo_name/
my_library/
__init__.py
main.py
examples/
exam1.py
exam2.py

В этом репозитории пакетом является только my_library, остальные директории это не пакеты, это просто дополнительный код в файлах. Директория examples не добавлена в sys.path, в ней нет рабочих модулей. Но если она лежит рядом с my_library, то Python вполне сможет импортнуть из неё модули, так как посчитает что examples это валидный пакет.

Конечно, пример несколько надуманный. Никто не будет добавлять корень репозитория в sys.path. Но, я думаю, суть ясна. Иногда директория это просто директория а не пакет!

#basic #pep
Для Python3.8 в PEP0578 добавили функционал аудита Runtime операций. Это позволяет выполнять хуки (функции) при возникновении определённых событий в интерпретаторе.

Этот функционал разработан, прежде всего, для специалистов по безопасности. Аудит позволяет перехватывать различные действия и проверять их допустимость.

Полный список стандартных аудит-ивентов можно посмотреть здесь. Из названий становится ясно что мы можем перехватить. Например, мы можем перехватить факт открытия файла (open), импорта модуля (import), копирование файла (shutil.copyfile), запуск процесса (subprocess.Popen) и тд. Как минимум мы можем залогировать данное событие, как максимум, вызвать аварийное завершение программы.

Примеры использования:

▫️Представим, что после разработки и долгих тестирований веб сервиса вы могли где-то оставить функцию ручного ввода данных в консоль. На продакшене такое недопустимо. С помощью аудита можно вызвать исключение перехватив ивент builtins.input

▫️С помощью ивента socket.getaddrinfo можно определить на какие сайты юзер заходил с помощью вашего приложения.

▫️Ивент exec позволит проверить загруженный объект кода перед его исполнением. Например выявить потенциально опасный код, или оставленные API ключи в строковых переменных.

Как добавить свой хук?

Для первого теста выполните такой код в самом начале работы приложения

import sys

def hook(event, args):
print(f'EVENT: {event}{args}')

sys.addaudithook(hook)

Каждый хук вызывается для всех событий, поэтому мы можем с помощью одного хука увидеть всё что происходит в интерпретаторе.
Теперь давайте посмотрим какие web-коннекты создаются при работе нашего кода. Для запросов используем requests.

import sys
import requests

def socket_hook(event, args):
if event == 'socket.getaddrinfo':
print(args[0])

sys.addaudithook(socket_hook)
requests.get('https://google.com')

В аутпуте вы увидите домен, на который был сделан запрос.

А так же:
▫️есть платформозависимые хуки (например взаимодействие с winapi)
▫️можно писать хуки на Си
▫️так как это мера для обеспечения безопасности, нет способа удалить хуки после добавления.

Напоминаю, доступно в Python3.8+

#pep #tricks
Debian 12 не позволяет глобально устанавливать Python пакеты через pip. Не поможет даже sudo.
Такое поведение описано в PEP 668.
Это сделано для минимизации конфликтов версий системных пакетов.
Если вам действительно нужно поставить что-то глобально, используйте
apt install python-packagename
В остальных случаях всегда используйте виртуальное окружение.

#pep
👍18
В Python 3.14 появится реализация PEP 750 и новый способ форматирования: t-strings. Это так называемые Template Strings.

Синтаксис такой же как с f-strings, но форматирование происходит не сразу.
Вместо строки создаётся объект Template, который внутри себя содержит исходную информацию, сырую строку (template.strings) и переменные (template.values).
Это позволяет произвести дополнительную обработку данных перед форматированием, например для усиления безопасности.
В примерах можно увидеть как строка с HTML кодом дополнтиельно обрабатывается чтобы избежать инъекции JS кода за счет экранирования служебных символов.

Конечно, этим примером возможности не ограничивюатся. Более подробно про функционал будет понятно ближе к релизу в конце года. Сейчас доступно в сборках 3.14.0a7+ из этой ветки.

Простой пример создания шаблона

name = "World"
template = t"Hello {name}!"


Что является шорткатом для
from string.templatelib import Template, Interpolation

template = Template(
"Hello ",
Interpolation(value="World", expression="name"),
"!"
)


В обоих случаях объект получим идентичный
print(isinstance(template, Template))
# True
print(template.strings)
# ("Hello ", "!")
print(template.values)
#(name,)


Больше примеров ➡️ здесь

#pep
👍8🤔4👎2🔥1😢1