Метка: pyway

Нечеткое сравнение текстов на Python с FuzzyWuzzy

Недавно мы обсуждали расчет расстояния Левеншейна, настало время испытать его применение на практике. Библиотека FuzzyWuzzy содержит набор функций для нечеткого поиска строк, дедупликации (удаления копий), корректировки ошибок. Она позволяет стать поиску умнее, помогая преодолеть влияние человеческого фактор.

Начнем с установки:

pip install fuzzywuzzy python-Levenshtein

Модуль python-Levenshtein можно устанавливать по желанию: работать будет и без него, но с ним гораздо быстрее (в разы). Поэтому советую его установить, он мелкий, порядка 50 Кб.

Похожесть двух строк

Задача: есть две строки, требуется вычислить степень их похожести числом от 0 до 100. В FuzzyWuzzy для этого есть несколько функций, отличающихся подходом к сравнению и вниманием к деталям. Не забудем импортировать:

from fuzzywuzzy import fuzz

Функция fuzz.ratio – простое посимвольное сравнение. Рейтинг 100 только если строки полностью равны, любое различие уменьшает рейтинг, будь то знаки препинания, регистр букв, порядок слов и так далее:

>>> fuzz.ratio("я люблю спать", "я люблю спать")
100
>>> fuzz.ratio("я люблю спать", "Я люблю cпать!")
81
>>> fuzz.ratio("я люблю спать", "я люблю есть")
88

Обратите внимание, что рейтинг второго примера ниже, чем у третьего, хотя по смыслу должно быть наоборот.

Следующая функция fuzz.token_sort_ratio решает эту проблему. Теперь акцент именно на сами слова, игнорируя регистр букв, порядок слов и даже знаки препинания по краям строки.

>>> fuzz.token_sort_ratio("я люблю спать", "я люблю есть")
56
>>> fuzz.token_sort_ratio("я люблю спать", "Я люблю спать!")
100
>>> fuzz.token_sort_ratio("я люблю спать", "спать люблю я...")
100

>>> fuzz.token_sort_ratio("Мал да удал", "удал да МАЛ")
100
>>> fuzz.token_sort_ratio("Мал да удал", "Да Мал Удал")
100

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

Функция fuzz.token_set_ratio пошла еще дальше: она игнорирует повторяющиеся слова, учитывает только уникальные.

>>> fuzz.token_set_ratio("я люблю спать", "люблю я спать, спать, спать...")
100
>>> fuzz.token_set_ratio("я люблю спать", "люблю я спать, спать и спать...")
100
>>> fuzz.token_set_ratio("я люблю спать", "но надо работать")
28

# повторы в token_sort_ratio роняют рейтинг! 
>>> fuzz.token_sort_ratio("я люблю спать", "люблю я спать, спать и спать.")
65

# но вот это странно:
>>> fuzz.token_set_ratio("я люблю спать", "люблю я спать, но надо работать")
100
>>> fuzz.token_set_ratio("я люблю спать", "люблю я спать, люблю я есть")
100

Последние два примера вернули 100, хотя добавлены новые слова, и это странно. Тут следует вспомнить о fuzz.partial_ratio, которая ведет себя также. А именно, проверяет вхождение одной строки в другую. Лишние слова игнорируются, главное – оценить, чтобы ядро было одно и тоже.

>>> fuzz.partial_ratio("одно я знаю точно", "одно я знаю")
100
>>> fuzz.partial_ratio("одно я знаю точно", "одно я знаю!")
92
>>> fuzz.partial_ratio("одно я знаю точно", "я знаю")
100

Еще еще более навороченный метод fuzz.WRatio, который работает ближе к человеческой логике, комбинируя несколько методов в один алгоритм в определенными весами (отсюда и название WRatio = Weighted Ratio).

>>> fuzz.WRatio("я люблю спать", "люблю Я СПАТЬ!")
95
>>> fuzz.WRatio("я люблю спать", "люблю Я СПАТЬ и есть")
86
>>> fuzz.WRatio("я люблю спать", "!!СПАТЬ ЛЮБЛЮ Я!!")
95

Нечеткий поиск

Задача: найти в списке строк одну или несколько наиболее похожих на поисковый запрос.

Импортируем подмодуль и применим process.extract или process.extractOne:

from fuzzywuzzy import process

strings = ['привет', 'здравствуйте', 'приветствую', 'хай', 'здорова', 'ку-ку']
process.extract("Прив", strings, limit=3)
# [('привет', 90), ('приветствую', 90), ('здравствуйте', 45)]

process.extractOne("Прив", strings)
# ('привет', 90)

Удаление дубликатов

Очень полезная функция для обработки данных. Представьте, что вам досталась 1С база номенклатуры запчастей, там полный бардак, и вам нужно поудалять лишние повторяющиеся позиции товара, но где-то пробелы лишние, где-то буква перепутана и тому подобное. Тут пригодится process.dedupe.

dedupe(contains_dupes, threshold=70, scorer=token_set_ratio)

Первый аргумент – исходный список, второй – порог исключения (70 по умолчанию), третий – алгоритм сравнения (token_set_ratio по умолчанию).

Пример:

arr = ['Гайка на 4', 'гайка 4 ГОСТ-1828 оцинкованная', 'Болты на 10', 'гайка 4 ГОСТ-1828 оцинкованная ...', 'БОЛТ']

print(list(process.dedupe(arr)))
# ['гайка 4 ГОСТ-1828 оцинкованная ...', 'Болты на 10', 'БОЛТ']

FuzzyWuzzy можно применять совместно с Pandas. Например так (без особых подробностей):

def get_ratio(row):
    name = row['Last/Business Name']
    return fuzz.token_sort_ratio(name, "Alaska Sea Pilot PAC Fund")

df[df.apply(get_ratio, axis=1) > 70]

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Импорт модулей из разных мест

Несложно импортировать встроенный модуль или пакет, установленный через pip, или тот, который лежит в директории с нашим кодом (import something). Но что если нужно импортировать код из произвольного места? Конечно, можно было бы скопировать код оттуда в своей проект, но так не рекомендуется делать. Есть и другие решения.

В модуле sys есть переменная path. Она содержит список путей, в которых Python ищет названия модулей для импорта. Пожалуйста, не путайте sys.path и переменную окружения PATH (которая, кстати, доступна через os.environ['PATH']). Это разные вещи, последняя не имеет отношения к поиску модулей Python.

>>> import sys
>>> sys.path
['', '/usr/local/Cellar/python@3.8/3.8.1/Frameworks/Python.framework/Versions/3.8/lib/python38.zip', ..., '/usr/local/lib/python3.8/site-packages']

Пустая строка в начале означает текущую рабочую директорию (pwd).

Мы можем влиять на эту переменную, например, добавляя туда свои пути. Если добавить в начало списка, то поиск модулей начнется именно с нового пути.

import sys
sys.path.insert(0, '/Users/you/Projects/my_py_lib')
import my_module  # этот модуль лежит в my_py_lib

Порядок тут важен. Нельзя сделать сначала import, потому что на момент импорта my_module система еще не знает, где его можно найти.

import sys
import my_module  # ModuleNotFoundError

sys.path.insert(0, '/Users/you/Projects/my_py_lib')  # поздно

Модуль site

Функция site.addsitedir тоже модифицирует sys.path, добавляя путь в конец списка. Еще она делает некоторые дополнительные вещи, но мы их не касаемся. Пример:

import site
site.addsitedir('/Users/you/Projects/my_py_lib')

import my_module

Также, набрав команду python3 -m site в командной строке, вы можете узнать пути для импорта в текущим интерпретаторе Python.

Минус способов с добавлением путей через sys.path и site – IDE скорее всего не будет видеть и индексировать эти пути, а значит будет много красных подчеркиваний и отсутствие автодополнения, даже если код прекрасно выполняется.

PYTHONPATH

PYTHONPATH – переменная окружения, которую вы можете установить перед запуском интерпретатора. Будучи заданной, она также влияет на sys.path, добавляя пути поиска модулей в начало списка.

На Windows можно использовать команду set. Если надо задать два и более путей, разделите их точкой с запятой:

set PYTHONPATH=C:\pypath1\;C:\pypath2\
python -c "import sys; print(sys.path)"

# Пример вывода:
['', 'C:\\pypath1', 'C:\\pypath2', 'C:\\opt\\Python36\\python36.zip', 'C:\\opt\\Python36\\DLLs', 'C:\\opt\\Python36\\lib', 'C:\\opt\\Python36', ..., 'Python36\\lib\\site-packages\\Pythonwin']

На Linux и macOS можно использовать export. Два и более путей разделяются двоеточием:

export PYTHONPATH='/some/extra/path:/foooo'
python3 -c "import sys; print(sys.path)"

# Пример вывода
['', '/some/extra/path', '/foooo', ...]

Или даже в одну строку:

PYTHONPATH='/some/path' python3 -c "import sys; print(sys.path)"

Кто не знал, ключ -c для python3 просто выполняет строчку кода. И да, лишних пробелов вокруг знака равно не должно быть, это не эстетическая прихоть автора, а такой синтаксис.

PyCharm

Если дополнительные пути заранее известные (не динамические), то в IDE обычно есть возможность задать их из настроек. Покажу на примере PyCharm 2019-2020.

Способ 1

Идем в настройки – Project Interpreter – Нажимаете на выпадающий список сверху – Show All.

project interpreter - show all

Там находите в списке нужный интерпретатор (тот, что задействован в текущем проекте) и внизу нажимаете иконку с папками.

python list

Затем нажимаете на плюсик и добавляете нужные папки, ОК.

Способ 2

Идем в настройки – Project: ваш проект – Project Structure – Add Content Root.

Способ 2

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

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Комплексные числа в Python. Бонус: фрактал

В Python есть встроенный тип данных complex, который моделируют комплексные числа. По-моему, теория комплексных чисел – настоящий прорыв в математике, оказавший колоссальное влияние на современную физику. Неудивительно, что комплексные числа оказались в стандартной библиотеке такого языка, как Python.

Фрактал

Ко́мпле́ксные чи́сла — числа вида a + bi, где a, b — вещественные числа, i — мнимая единица, то есть число, для которого выполняется равенство: i2 = -1.

Графическое представление комплексного числа
Графическое представление комплексного числа

Цель этой статьи – рассказать про комплексные числа с точки зрения программирования на Python, если вам хочется узнать математическую теорию или просто освежить воспоминания о комплексных числах, то смело переходите по ссылке: Комплексные числа (откроется в новом окне), а потом возвращайтесь сюда.

В Python комплексное число состоит из пары чисел с плавающей запятой, которые отвечают за реальную и мнимые части. В исходнике на Си – это структура из пары чисел типа double. (Вы же помните, что float в Python это числа двойной точности?):

typedef struct {
    double real;
    double imag;
} Py_complex;

✅ Давайте посмотрим, какими способами мы можем задать комплексное число. Во-первых, с помощью встроенной функции complex(real[, imag]):

>>> complex()   # нуль! (комплексный)
0j
>>> complex(1)   # из обычного числа int или float (мнимая часть будет 0)
(1+0j)
>>> complex(2, 3)   # из пары чисел (реальная и мнимая части)
(2+3j)
>>> complex('2+3j')  # из строки (без пробелов)!
(2+3j)

❌ Когда создаете комплексное число из строки, то в ней не должно быть пробелов, иначе будет ошибка ValueError!

✅ Во-вторых, что очень круто, комплексное число можно задать особым синтаксисом в форме a+bj. Примеры:

>>> 8+5j
(8+5j)
>>> -1-1j
(-1-1j)
>>> 0j
0j
>>> -4.4e+5 + 1.5e+6j
(-440000+1500000j)

❌ Но! Нельзя задавать их так:

>>> 1+j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'j' is not defined
>>> a = 5
>>> 2 + aj
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'aj' is not defined

В первом случае надо писать 1j, вместо j, потому что j – это может быть название какой-то иной переменной, ведь имя j не зарезервировано под мнимую единицу. Чтобы задать мнимое число, обязательно требуется перед j ставить цифры.

Во втором случае, мы желаем задать мнимую часть через переменную, но Python думает, что у нас должна быть переменная с именем aj, которой нет. Правильно писать в таком случае a*1j.

✅ Правильные варианты:

>>> 1+1j
(1+1j)
>>> a = 5
>>> 2 + a * 1j
(2+5j)

Кстати, буква j может быть и заглавной J.

Какие свойства есть у типа complex?

Можно извлекать из комплексного числа его мнимую (imag) и реальную (real) части – это обычные float:

>>> z = -3+7j
>>> z.real, z.imag
(-3.0, 7.0)
>>> z == z.real + z.imag * 1j
True

Комплексно-сопряженное число – то же самое, только с другим знаком у мнимой части:

>>> z.conjugate()
(-3-7j)
>>> (4-1j).conjugate()
(4+1j)

Модуль комплексного числа – фактически длина вектора на комплексной плоскости – вычисляется обычной функцией abs:

>>> abs(4+3j)
5.0

Операции

✅ К комплексным числам применимы обычные арифметический операторы, такие как +, -, *, /.

Как вы догадались из формы синтаксиса 1+2j, можно без проблем складывать комплексные числа с обычными вещественными float или целыми int. Т.е. полное комплексное число в этой форме представляется суммой действительного числа и чисто мнимого. Аналогично, умножение комплексного числа на вещественное просто масштабирует пропорционально его компоненты на это число.

Пару комплексных чисел можно складывать и вычитать. Это просто, они работают подобно двумерным векторам на плоскости. Комплексные числа можно умножать и делить. Тут математика несколько сложнее, не буду повторять ее, читайте вики. А примеры кода вот:

>>> z1 = 3 + 4j
>>> z2 = 5 - 2j
>>> z1 + z2
(8+2j)
>>> z1 - z2
(-2+6j)
>>> z1 * z2
(23+14j)
>>> z1 / z2
(0.24137931034482757+0.896551724137931j)

Не забывайте ставить скобки:

>>> 1+2j * 5-4j
(1+6j)
>>> (1+2j) * (5-4j)
(13+6j)

❌ Целочисленные деления, взятие остатка и подобное не применимы к комплексным числам.

✅ Комплексные числа можно сравнивать на равенство и неравенство.

>>> 1+2j == 2j+1
True
>>> 3 + 4j != 2 - 2j
True

❌ Но нельзя к ним применять знаки больше, меньше.

>>> 1j > 2j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'complex' and 'complex'

✅ Можно возводить одно комплексное число в степень другого следующими способами*:

>>> z1 ** z2
(3046.7438186304985+19732.04597993193j)
>>> pow(z1, z2)
(3046.7438186304985+19732.04597993193j)

Комплексный 0 в степени комплексного 0 даст… барабанная дробь… единицу:

>>> 0j ** 0j
(1+0j)

*) О многозначности замолвлено будет ниже, математики приберегите свои помидоры, пока не дочитаете до конца.

cmath

Модуль, который отвечает за стандартные функции над комплексными числами называется cmath (документация на английском). Обычный math не умеет извлекать корень из минус единицы, а cmath – запросто:

>>> import math, cmath
>>> math.sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> cmath.sqrt(-1)
1j

Рассмотрим основные функции из cmath:

cmath.phase(x) – фаза (или у нас она более известна, как аргумент Arg z) – эквивалент math.atan2(x.imag, x.real) – иными словами, это угол поворота φ вектора на комплексной плоскости. Результат лежит в промежутке [-ππ]. При этом разрез на комплексной области (т. е. луч, через который результат функции разрывается и перепрыгивает) выбирается вдоль отрицательной части реальной оси).

Представление комплексного числа в полярных координатах
Представление комплексного числа в полярных координатах
>>> phase(complex(-1.0, 0.0))
3.141592653589793
>>> phase(complex(-1.0, -0.0))  # минус ноль! подходим с другой стороны к точке разреза
-3.141592653589793

За модуль отвечает обычная abs (без cmath).

cmath.polar(x) – перевод комплексного числа в полярные координаты (кортеж из двух float): (abs(x), cmath.phase(x)).

cmath.rect(rphi) – наоборот выдает комплексное число по его полярным координатам – эквивалент: r * (math.cos(phi) + math.sin(phi)*1j).

cmath.sqrt(x) – корень квадратный из комплексного числа.

>>> cmath.sqrt(1j)
(0.7071067811865476+0.7071067811865475j)

Те, кто разбирается в математике сразу заметят, что многие функции от комплексной переменной – многозначны. (Видео о многозначных функциях) В частности у корня квадратного из комплексного числа всегда ровно два ответа. Но! Python всегда возвращает ровно одно значение. Он берет всегда «главную ветвь», а всякие вращения на 2πk/n остаются на совести программиста. Вот теория, как брать корень из комплексного числа. Я тщетно пытался нагуглить функцию, возвращающую множество значений корня n-й степени, и в итоге написал свою:

import cmath

def roots(z: complex, n):
    assert isinstance(n, int) and n > 1
    r, phi = cmath.polar(z)
    r **= 1 / n
    for k in range(n):
        yield cmath.rect(r, (phi + 2 * cmath.pi * k) / n)

z1, z2 = roots(1j, 2)
print(z1, z2)
print(cmath.isclose(z1 * z1, 1j))
print(cmath.isclose(z2 * z2, 1j))

# (0.7071067811865476+0.7071067811865475j) (-0.7071067811865477-0.7071067811865475j)
# True
# True
Разрез комплексной плоскости
Разрез комплексной плоскости

Функция cmath.isclose(ab*rel_tol=1e-09abs_tol=0.0) проверяет близки ли два комплексных числа между собой с некоторой точностью (относительной или абсолютной). Так как complex строится на базе чисел с плавающей точкой, то из-за ошибок округления редко можно получить точное равенство, приходится проверять близость. Код эквивалентен: abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol). Числа NaN не близки ни к кому, включая самих себя. А inf и -inf близки только каждое само к себе.

cmath.exp(x),cmath.log(x[, base]), cmath.log10(x), cmath.acos(x), cmath.asin(x), cmath.atan(x), cmath.cos(x), cmath.sin(x), cmath.tan(x), cmath.acosh(x), cmath.asinh(x), cmath.atanh(x), cmath.cosh(x), cmath.sinh(x), cmath.tanh(x) –   обычный набор функций, только для комплексных чисел. Каждая из функций возвращает один результат. Думаю, если вам действительно нужны в работе комплексные функции, значит вы и так знаете, как достать все значения функций.

Множество Мандельброта

Даже если вы не математик или физик, комплексные числа могут поразвлечь и вас. Решил, что прикладной пример о комплексных числах должен быть впечатляющим. Мы нарисуем множество Мандельброта – пожалуй самый знаменитый фрактал.

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

Реккурентное соотношение
Реккурентное соотношение

Иными словами мы берем каждую точку с = x + yj на выбранном участке комплексной плоскости и возводим в квадрат, потом снова прибавляем c, опять возводим в квадрат и так несколько раз. Смотрим, улетает ли результат в бесконечность. Если не улетает, значит точка принадлежит множеству, красим ее в черный, а если улетает, то красим в белый.

Нам понадобятся библиотеки для работы с изображениями и для полосы прогресса:

 pip install pillow tqdm 

Пишем код:

from PIL import Image
from tqdm import tqdm

W, H = 1024, 768  # размеры картинки
ITER = 100  # максимальное число итераций, чтобы убедиться расходится или нет формула в данной точке
LIMIT = 2.0  # предельное значение, выше которого уже наверняка расходится

img = Image.new('RGB', (W, H))

for px in tqdm(range(W)):
    for py in range(H):
        # преобразование координат
        x = px / W * 3 - 2  # x = -2..1
        y = py / H * 2 - 1  # y = -1..1

        color = (0, 0, 0)  # черный
        c = x + 1j * y  # смещение из координат
        z = 0j  # начальная точка
        for n in range(ITER):
            z = z ** 2 + c
            if abs(z) > LIMIT:  # разошлось
                color = (255, 255, 255)  # белый цвет
                break
        img.putpixel((px, py), color)

img.save('mand0.png')  # сохраним
img.show()  # покажем

Результат:

Фрактал в ЧБ
Фрактал в ЧБ

Добавим цвета. Будем считать, сколько итераций прошел цикл перед тем, как последовательность разошлась и мы вышли. Каждому числу шагов зададим цвет на палитре. Цвета плавно меняются вдоль палитры.

from PIL import Image
from tqdm import tqdm
import math

W, H = 1024, 768  # размеры картинки
ITER = 1000  # максимальное число итераций, чтобы убедиться расходися или нет формула в данной точке
LIMIT = 2.0  # предельное значение, выше которого уже расходится

img = Image.new('RGB', (W, H))

# создадим палитру от числа итераций
palette = [
    (
        int(255 * math.sin(i / 50.0 + 1.0) ** 2),
        int(255 * math.sin(i / 50.0 + 0.5) ** 2),
        int(255 * math.sin(i / 50.0 + 1.7) ** 2)
    ) for i in range(ITER - 1)
]
palette.append((0, 0, 0))  # последняя итерация - значит мы внутри - черный

for px in tqdm(range(W)):
    for py in range(H):
        # преобразование координат
        x = px / W * 3 - 2  # x = -2..1
        y = py / H * 2 - 1  # y = -1..1

        c = x + 1j * y  # смещение из координат
        z = 0j  # начальная точка
        for n in range(ITER):
            z = z ** 2 + c
            if abs(z) > LIMIT:  # разошлось
                break
        img.putpixel((px, py), palette[n])


img.save('mand1.png')  # сохраним
img.show()  # покажем

Вот итог:

Цветной фрактал
Цветной фрактал

Вот такая красота получается из простейшей формулы!

Можно пойти дальше и создать GIF анимацию, где мы постепенно приближаемся к деталям фрактала. Перед этим оптимизируем немного код. abs извлекает квадратный корень, что не обязательно. Лучше сделать так, для улучшения производительности:

                if (z * z.conjugate()).real > 4.0:  # вместо abs(z) > 2.0!
                    break

От кадра к кадру будем менять область отображения. Генератор фрактала для заданной области описан функцией:

def mandelbrot(w, h, palette, x1=-2, y1=-1, x2=1, y2=1):
    img = Image.new('RGB', (w, h))

    dx = x2 - x1
    dy = y2 - y1
    iters = len(palette)

    n = 0
    for px in range(w):
        for py in range(h):
            # преобразование координат
            x = px / w * dx + x1
            y = py / h * dy + y1

            c = x + 1j * y  # смещение из координат
            z = 0j  # начальная точка
            for n in range(iters):
                z = z ** 2 + c
                if (z * z.conjugate()).real > 4.0:  # разошлось
                    break
            img.putpixel((px, py), palette[n])
    return img

Остаетя задать закон движения камеры и сохранить пачку кадров в GIF.

x, y, r = 0, 0, 5  # от
x_tar, y_tar, r_tar = -0.74529, 0.113075, 1.5e-6  # до

frames = []
for _ in tqdm(range(FRAMES)):
    frames.append(mandelbrot(W, H, palette, x - r, y - r, x + r, y + r))
    x += (x_tar - x) * 0.1
    y += (y_tar - y) * 0.1
    r += (r_tar - r) * 0.1

frames[0].save('mandel.gif', save_all=True, append_images=frames[1:], duration=60, loop=0)

Полный код генератора анимации здесь.

Анимация погружения в фрактал
Анимация погружения в фрактал

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

globals, locals, vars, dir – инспектируем переменные

Программист на Python может узнать, какие именно переменные определенны в данный момент в интерпретаторе. Переменные можно разделить на локальные и глобальные. Глобальные определены на верхнем уровне кода снаружи функций и классов (грубо говоря без отступов слева). Локальные переменные наоборот определены внутри своих зон видимости, ограниченных классами и функциями.

Функция globals() выдает словарь глобальных переменных (ключ – имя переменной). Функция locals() возвращает словарь только локальных переменных. Пример:

x, y = 5, 10

def test():
    y, z = 33, 44
    print('globals:', globals())
    print('locals:', locals())

test()

"""Вывод:
globals: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': ...>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/.../vars.py', '__cached__': None, 'x': 5, 'y': 10, 'test': <function test at 0x107677280>}
locals: {'y': 33, 'z': 44}"""

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

Еще важно знать, что в список переменных входят не только простые переменные, которые вы определяете через знак присваивания, но и функции, классы и импортированные модули!

Через словари из locals() и globals() переменные можно не только читать, но и создавать, перезаписывать и удалять:

>>> x = 10
>>> globals()['x'] = 5
>>> x
5
>>> globals()['new_var'] = 10
>>> new_var
10
>>> del globals()['new_var']
>>> new_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'new_var' is not defined

vars()

vars() ведет себя как locals(), если вызвана без аргумента, а если с аргументом, то она просто получает __dict__ от аргумента. Если его нет у аргумента, то будет TypeError.

class Foo:
    def __init__(self):
        self.x = 5

f = Foo()
print(vars(f))  # {'x': 5}
print(vars(f) == f.__dict__)  # True

В глобальном контексте все три функции возвращают одно и тоже – глобальные переменные. Проверьте:

print(globals())
print(locals())
print(vars())
print(globals() == locals() == vars()) # True

dir()

Без параметров dir() возвращает список имен переменных. В глобальном контексте – глобальных переменных, в локальном – список имен локальных переменных.

def test():
    x = 10
    print(dir())  # ['x']

y = 10
test()
print(dir())  # ['__annotations__', ..., '__spec__', 'test', 'y']

Все рассмотренные выше функции являются встроенными и не требуют импортов.

P. S.

В отличие он некоторых других языков в Python блоки типа for, if, while, with не создают областей видимости (scope) для переменных, то есть переменная внутри и снаружи блока будет одна и та же:

x = 1
if True:
    x = 2
print(x)  # 2

Частая ошибка – затирание внешней переменной в цикле for:

i = 10
for i in range(5):  # затирает i
    ...
print(i)  # 4

Зоны видимости отделяются только функциями, классами и модулями. Здесь все переменные x – разные:

x = 1
class Foo:
    x = 2
    def method(self):
        x = 3
        return x
print(x, Foo.x, Foo().method())  # все 3 разные

Самая широкая зона видимости называется builtin. В нее попадают все имена, известные интерпретатору в данный момент, включая вещи импортированные из других модулей.

>>> from math import pi
>>> pi, id(pi)
(3.141592653589793, 4465320624)
>>> pi = 3
>>> pi, id(pi)
(3, 4462262880)
>>> from math import pi
>>> pi, id(pi)
(3.141592653589793, 4465320624)

Казалось бы мы затерли pi, но мы затерли его лишь в глобальной области видимости. Повторно импортируя pi, мы получаем старую переменную с тем же адресом, иными словами мы достаем ее из builtin области в global.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

​​Разбор URL

Функция urlparse из модуля urllib.parse разбирает URL на составные части: протокол, имя хоста, порт, путь, запрос и прочие.

Части URL
>>> u1 = urlparse('https://tirinox:1234@www.site.com:8080/some/page/index.html?page=2&action=login')
>>> u1
ParseResult(scheme='https', netloc='tirinox:1234@www.site.com:8080', path='/some/page/index.html', params='', query='page=2&action=login', fragment='')
>>> u1.scheme, u1.query, u1.netloc
('https', 'page=2&action=login', 'tirinox:1234@www.site.com:8080')

Имя хоста, порт, логин и пароль объединены в поле netloc, но их компоненты доступны для чтения по этим атрибутам:

>>> u1.username, u1.password, u1.hostname, u1.port
('tirinox', '1234', 'www.site.com', 8080)

Собрать обратно ParseResult в URL:

>>> u1.geturl()
'https://www.site.com:8080/some/page/index.html?page=2&action=login'

Если мы хотим в URL поменять какие-то части, удобно делать вот так:

>>> u1._replace(scheme='http').geturl()
'http://www.site.com:8080/some/page/index.html?page=2&action=login'

Однако таким способом нельзя поменять компоненты netloc, например, отдельно порт. netloc нужно менять целиком:

>>> u1._replace(netloc="user:password@www.site.com:8090").geturl()
'https://user:password@www.site.com:8090/some/page/index.html?page=2&action=login'

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈