Метка: python 3

Циклы и замыкания Python

Когда вы определяете функцию внутри другой функции и используете локальные переменные внешней функции во вложенной, вы создаете замыкание. Время жизни этих переменных «продляется» в особой области видимости enclosing даже после завершения работы внешней функции. Пример: make_adder возвращает функцию-прибавлятор. Объект из переменной a будет жить и работать даже после выхода из make_adder:

def make_adder(a):
    def adder(x):
        return a + x
    return adder

plus_5 = make_adder(5)
print(plus_5(3))  # 8

Здесь я хочу коснуться одной популярной проблемы. Дело в том, что если мы создадим несколько функций внутри одного контекста, то они будут разделять одну область видимости enclosing. Рассмотрим пример создания трех функций в цикле:

def make_adders():
    adders = []
    for a in range(3):
        def adder(x):
            return a + x
        adders.append(adder)
    return adders

adders = make_adders()
for adder in adders:
    print(adder(2))  # 4 4 4

Вместо функций прибавляющих разные числа от 0 до 2, мы получили 3 одинаковых функции, потому что внутри себя они поддерживают ссылку на одну и ту же переменную a, значение которой останется равным 2 после выполнения всего цикла целиком.

Есть простой прием, помогающий «зафиксировать» значения переменной в моменте: достаточно добавить во вложенную функцию дополнительный аргумент со значением по умолчанию, равным нужной переменной a=a:

def make_adders():
    adders = []
    for a in range(3):
        def adder(x, a=a):  # FIX!
            return a + x
        adders.append(adder)
    return adders

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

Еще лучше переименовать аргумент, чтобы избежать конфликтов имен и замечаний IDE, например, так:

def adder(x, that_a=a):  # FIX!
    return that_a + x

yield

Пока писал код для этого поста, я наткнулся на одну обманку. Люблю оформлять функции, возвращающие коллекции, как генераторы с ключевым словом yield. Вот так:

def make_adders():
    for a in range(3):
        def adder(x):
            return a + x
        yield adder

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

Видите, тут нет фикса a=a! Казалось бы, что код должен также содержать в себе баг и выводить «4 4 4», но он работает, как задумано изначально.

Однако, если мы применим list к генератору, извлекая все значения разом, то баг вернется:

adders = list(make_adders())
for adder in adders:
    print(adder(2))  # 4 4 4

Разгадка. В первом случае происходят следующие действия:

  • a = 0
  • yield функцию (a + x), make_adders становится на паузу
  • печать adder(2) = 0 + 2 = 2
  • make_adders запускается
  • a = 1
  • yield функцию (a + x), пауза
  • печать adder(2) = 1 + 2 = 2
  • … и так далее…

То есть мы запускаем adder только один раз в тот момент, пока переменная a еще равна нужному значению. 

Во втором код list прокручивает make_adders до конца, оставляя a = 2, и все функции выдают одинаковый результат.

Вывод мы должны сделать такой: yield не создает нового замыкания с отдельной переменной a и не освобождает нас от ответственности следить за переменными.

Еще кое-что. 

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

next(adders)  # StopIteration

После исполнения цикла в коде выше, генератор adders будет исчерпан. В нем больше не останется значений, и если еще раз запустить цикл по adders, то он пройдет ровно 0 итераций. 

Генератор – вещь одноразовая.

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

Найти первый элемент списка по условию

Пускай имеется такая задача: дан список с численными элементами. Требуется найти и вернуть первый отрицательный элемент. Казалось бы, должна быть какая-нибудь встроенная функция для этого, но нет. Придется писать ее самим. Решение в лоб:

items = [1, 3, 5, -17, 20, 3, -6]

for x in items:
    if x < 0:
        print(x)
        break
else:
    print('not found')

Такое решение работает, но выглядит скорее по-бейсиковски, нежели чем по-питоновски. Пытаясь проявить смекалку, некоторые извращаются и пишут так:

result = list(filter(lambda x: x < 0, items))[0]
print(result)

По-моему, стало гораздо сложнее, хоть и в одну строку. А может лучше так:

result = [x for x in items if x < 0][0]

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

Правильное решение

Лучше использовать встроенную функцию next – она возвращает следующий элемент из итератора, а в качестве итератора мы напишем генераторное выражение с if. Вот так:

result = next(x for x in items if x < 0)

Вот это коротко, экономно и очень по-питоновски (in a pythonic way). Остается одна проблемка: если элемент не найден, что будет брошено исключение StopIteration. Чтобы подавить его, достаточно вторым аргументом в next передать значение по-умолчанию. Если оно задано, то оно будет возвращено вместо возбуждения исключения, если в итераторе нет элементов, то есть не найдено удовлетворяющих условию элементов в исходной коллекции. И не забыть обернуть генераторное выражение в скобки:

items = [1, 2, 4]
result = next((x for x in items if x < 0), 'not found')
print(result)  # not found

С произвольной функцией, задающей критерий поиска (ее еще называют предикат – predicate) это выглядит так:

def is_odd(x):
    return x % 2 != 0

result = next(x for x in items if is_odd(x))
# или еще лучше
result = next(filter(is_odd, items))

Так как в Python 3 filter работает лениво, как и генератор, она не «обналичивает» весь исходный список через фильтр, а лишь идет до первого удачно-выполненного условия. Любите итераторы! ✌️ 

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

Python: все про del

КДПВ

Инструкция del (от англ. delete), как можно понять из названия, нужна чтобы что-то удалять, а именно имена переменных, атрибуты объектов, элементы списков и ключи словарей.

1. Удаление элемента из списка по индексу:

>>> x = [1, 2, 3, 4, 5]
>>> del x[2]
>>> x
[1, 2, 4, 5]

Также можно удалять по срезам. Пример: удаление первых двух элементов:

>>> x = [1, 2, 3, 4, 5]
>>> del x[:2]
>>> x
[3, 4, 5]

Удаление последних n элементов: del x[n:].

Удаление элементов с четными индексами: del x[::2], нечетными: del x[1::2].

Удаление произвольного среза: del x[i:j:k].

Не путайте del x[2] и x.remove(2). Первый удаляет по индексу (нумерация с 0), а второй по значению, то есть находит в списке первую двойку и удаляет ее.

2. Удаление ключа из словаря. Просто:

>>> d = {"foo": 5, "bar": 8}
>>> del d["foo"]
>>> d
{'bar': 8}

А вот строки, байты и сеты del не поддерживают.

3. Удаление атрибута объекта.

class Foo:
    def __init__(self):
        self.var = 10

f = Foo()
del f.var
print(f.var)  # ошибка! 

Примечание: можно через del удалить метод у самого класса del Foo.method, но нельзя удалить метод у экземпляра класса del Foo().methodAttributeError.

4. Что значит удалить имя переменной? Это просто значит, что надо отвязать имя от объекта (при этом если на объект никто более не ссылается, то он будет освобожден сборщиком мусора), а само имя станет свободно. При попытке доступа к этому имени после удаления будет NameError, пока ему снова не будет что-то присвоено.

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

Здесь кроется один нюанс. Если переменная была внутри функции помечена, как global, то после ее удаления глобальная переменная никуда не денется, а имя освободится лишь в зоне видимости функции. Причем если мы снова присвоим ей значение, то она опять окажется глобальной, т.е. del не очищает информацию о global!

g = 100
def f():
    global g
    g = 200
    del g  # g останется вне функции
    g = 300  # та же самая глобальная g

f()
print(g) # 300

Чтобы реально удалить глобальную переменную, можно сделать так: del globals()['g'].

В пунктах 1, 2, 3 в качестве имен могут фигурировать выражения и ссылки, так как операции идут над содержимым объектов, а в пункте 4 должно быть строго формальное имя удаляемого объекта.

>>> x = [1, 2, 3]
>>> y = x
>>> del y  # удаляет именно y, но x остается

Еще одна особенность del – она может удалить несколько вещей за раз, если передать в нее кортеж или список объектов на удаление.

x, y, z = 10, 20, [1, 2, 3]
del x, y, z[2]

Пусть задан список из 5 элементов:

x = [1, 2, 3, 4, 5]
del x[2], x[4]

Казалось бы, что будут удалены 2-й и 4-й элементы списка, но это не так! Удаления происходят один за одним, и сначала будет удален 2-й элемент, размер списка уменьшится на 1, а потом будет попытка удалить 4-й элемент, но она будет неудачна – вылетит исключение IndexError, потому что элемента с индексом 4 больше нет, а сам список будет равен [1, 2, 4, 5]. Так что будьте внимательны в подобных ситуациях!

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @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 👈