Size: a a a

2018 November 28
Oh My Py
Oh My Py — канал про тайные возможности стандартной библиотеки Питона. Тайные не потому, что кто-то их скрывает, конечно ツ

Просто стандартная библиотека огромная! А разработчики часто не копают глубоко и изобретают велосипед вместо того, чтобы использовать готовое.

Заодно обсудим полезные и не самые известные приёмы в работе с языком и структурами данных. А ещё особенности дизайна и плохой код в стандартной библиотеке (да, встречается и такое).

Поехали!
источник
Oh My Py
Сделать превьюшку длинного текста

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

article = "Около двух месяцев назад породистый голубь по имени Френк постучался в стеклянные двери омской ветеринарной клиники"
article[:30]

'Около двух месяцев назад пород'


Фраза оборвана посреди слова — это неуважение к читателю и к Френку.

А можно воспользоваться функцией textwrap.shorten():

import textwrap
textwrap.shorten(article, 30, placeholder="...")

'Около двух месяцев назад...'


Намного лучше!
источник
2018 November 29
Oh My Py
Отформатировать текст для консоли

Если любите делать CLI-утилиты, модуль textwrap наверняка вам понравится.

Он умеет перформатировать многострочный текст, чтобы длина строки не превышала N символов:

text = "Около двух месяцев назад породистый голубь по имени Френк постучался в стеклянные двери омской ветеринарной клиники"
formatted = textwrap.fill(text, width=20)
print(formatted)

Около двух месяцев
назад породистый
голубь по имени
Френк постучался в
стеклянные двери


Или добавить отступ, например для цитаты:

import textwrap
inspirational = "Цитаты простых людей:"
quote = "Откройте окно вообще дышать невозможно"
quote = textwrap.indent(quote, prefix="> ")
print(inspirational, quote, sep="\n")

Цитаты простых людей:
> Откройте окно вообще дышать невозможно


Френк одобряет.
источник
2018 November 30
Oh My Py
Все слова с прописной буквы

Допустим, запустили вы стартап. В автоматическом режиме собираете самые упоротые новости русскоязычных СМИ, вот такие:

Кот из Новокузнецка признан виновным в потопе

Автоматически же переводите их на английский, вот так:

Cat from Novokuznetsk found guilty in the flood

И ежедневно рассылаете подписчикам по всему миру.

Всё хорошо, но знакомый эксперт из МГИМО подсказывает: в английском принято каждое слово в заголовке начинать с заглавной буквы. А у вас-то не так!

Можно, конечно, бить заголовок по пробелам через .split(), исправлять регистр через .capitalize() и склеивать обратно через .join(). Но есть способ лучше:

import string
header = "Cat from Novokuznetsk found guilty in the flood"
string.capwords(header)

'Cat From Novokuznetsk Found Guilty In The Flood'


Соу мач беттер.
источник
2018 December 02
Oh My Py
Простое сравнение с шаблоном

Для проверки строки по шаблону обычно используют регулярные выражения и модуль re. Но иногда хочется что-нибудь попроще, пусть и не такое мощное — вроде like в SQL.

Сравнить строку или список с шаблоном поможет модуль fnmatch:

import fnmatch
journal = [
 "10:00 Начался обычный день в омской ветклинике",
 "10:30 Голубь Френк постучался в стеклянные двери",
 "10:50 Лисица Клер поскреблась в окно",
 "11:10 Попугай Питер проник через вентиляцию",
 "11:11 Клер попыталась сожрать Френка и Питера",
 "11:25 Осьминог Пауль всплыл в мужском туалете",
]

fnmatch.filter(journal, "*Френк*")
[ '10:30 Голубь Френк постучался в стеклянные двери',
 '11:11 Клер попыталась сожрать Френка и Питера' ]

fnmatch.fnmatch("frank", "f???k")
True


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

fnmatch.translate("*Френк*")
'(?s:.*Френк.*)\\Z'


Курлык.
источник
2018 December 03
Oh My Py
Сравнить строки на похожесть

Помните ваш стартап с самыми актуальными новостями дня? Кажется, у него появился конкурент — он нагло крадёт ваши аутентичные новости, рерайтит их, и рассылает ничего не подозревающим клиентам, подрывая вашу репутацию.

Судите сами, вот ваши новости:

genuine = [
 "«Братец-хлеб» из Китая носит плащ и корону из булочек, чтобы кормить чаек",
 "Мясо гигантских тараканов станет вкусной и недорогой альтернативой говядине",
 "Скандал в ботаническом саду: 10 миллионов рублей ушло на зарплату кактусам",
]


А вот новости жалкого подражателя:

plagiary = [
 "Китайский хлебный братец кормит чаек плащом и короной из булочек",
 "Гигантское мясо тараканов станет говядине недорогой и вкусной альтернативой",
 "Зарплата кактусов в ботаническом саду составила 10 скандальных миллионов рублей",
]


Нужны какие-то основания для судебного иска, и нужны быстро. Хорошо, что в стандартной библиотеке Питона есть модуль difflib. Сделаем на нём функцию сравнения:

import difflib

def similarity(s1, s2):
 normalized1 = s1.lower()
 normalized2 = s2.lower()
 matcher = difflib.SequenceMatcher(None, normalized1, normalized2)
 return matcher.ratio()


И сравним:
similarity(genuine[0], plagiary[0])
0.51

similarity(genuine[1], plagiary[1])
0.69

similarity(genuine[2], plagiary[2])
0.55


АГА! 51%, 69% и 55% похожести! Всё ясно, какие ещё нужны доказательства.
источник
2018 December 04
Oh My Py
Напечатать развесистую структуру данных в кратком виде

Наверняка вы знаете про функции pprint.pprint и pprint.pformat, который красиво форматируют разные коллекции и словари.

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

Например, запросили вы апишечку и получили в ответ развесистый словарь:

rating = requests.get("https://www.cia.gov/the-world-factbook/top-dumbest-animals").json()


Заглянем в него, не погружаясь в детали:

import pprint
pprint.pprint(rating, depth=3)

{'leaderbord': [
 {'details': {...}, 'name': 'Голубь Френк', 'position': 1},
 {'details': {...}, 'name': 'Лисица Клер', 'position': 2},
 {'details': {...}, 'name': 'Попугай Питер', 'position': 3},
 {'details': {...}, 'name': 'Свинка Зои', 'position': 4},
 {'details': {...}, 'name': 'Макака Лукас', 'position': 5}],
'name': 'Самые тупые животные'}


Ненужные подробности автоматически скрыты за «...», и мы видим самую суть. Френк, я в тебе не сомневался.
источник
2018 December 06
Oh My Py
Чистый код: ratio и его упоротые друзья

На днях мы использовали метод SequenceMatcher.ratio() из модуля difflib, чтобы оценить сходство двух строк.

А что бы вы сказали, если узнали, что у того же SequenceMatcher есть ещё методы quick_ratio() и real_quick_ratio()? С описанием «возвращает верхнюю границу ratio() довольно быстро» и «возвращает верхнюю границу ratio() очень быстро»?

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

Конкретно в данном случае я бы сделал «быстрый» и «очень быстрый» методы приватными, потому что они нужны только для оптимизации работы других публичных методов difflib. Используются примерно так:

if matcher.real_quick_ratio() >= cutoff and \
   matcher.quick_ratio() >= cutoff and \
   matcher.ratio() >= cutoff:
   ...


Как вспомогательные методы — ладно. Но точно не в публичный интерфейс.
источник
2018 December 07
Oh My Py
Разбить строку на слова с учётом кавычек

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

Например, такая статья:

text = """Голубь Френк прибыл в отель "Четыре сезона" с дружеским визитом. По сообщениям очевидцев, он сожрал в ресторане киноа прямо из тарелки гостя, а затем клюнул в глаз прибежавшего на шум официанта.

Френк прилетает в "Четыре сезона" каждый год. В прошлый раз мерзкая птица нагадила в ванну с шампанским в королевском люксе, лишив кого-то романтического вечера."""


Вы чистите текст от пунктуации, бьёте по пробелам и считаете слова. Вот топ-3:

[('френк', 2),
('четыре', 2),
('сезона', 2)]


Но погодите, разве правильно считать «четыре» и «сезона» разными тегами? Это ведь название отеля, лучше учитывать их как одно словосочетание. Тут-то и пригодится функция shlex.split() — она трактует словосочетания в кавычках как один токен:

# слегка чистим text, для краткости опускаю
import shlex
from collections import Counter

words = shlex.split(text)
words = [word for word in words if len(word) > 3]
Counter(words).most_common(3)

[('френк', 2),
('четыре сезона', 2),
('голубь', 1)]


Вот теперь теги что надо!

P.S. Вообще, shlex предназначен для разбора shell-подобных строк, так что если злая судьба заставит вас парсить bash-скрипты — вы знаете, куда смотреть.
источник
2018 December 12
Oh My Py
Шаблонизатор для бедных

Мантра «There should be one — and preferably only one — obvious way to do it» из Zen of Python далека от реальности.

Все мы знаем, что в Питоне за долгие годы собрали аж три способа подстановки переменных в строку:

who = "Голубь Френк"
"%s постучался в стеклянные двери" % who
"{} постучался в стеклянные двери".format(who)
f"{who} постучался в стеклянные двери"


Но не все знают, что есть ещё и четвёртый способ — string.Template. Больше того, он ещё и может быть полезен иногда.

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

CHANGEME:who постучался в стеклянные двери


Тут и пригодится string.Template:

import string
class OmskTemplate(string.Template):
   delimiter = "CHANGEME:"

template = OmskTemplate("CHANGEME:who постучался в стеклянные двери")
template.substitute({ "who": "Кот Джарвис"})

'Кот Джарвис постучался в стеклянные двери'


Если нужен ещё более извращённый синтаксис — например, ==!who!== — достаточно перекрыть атрибут класса pattern, указав в нём подходящее регулярное выражение.
источник
2018 December 13
Oh My Py
Чистый код: единообразие в именах

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

Посмотрим на питоновский модуль difflib, который помогал нам сравнивать строки:

find_longest_match() находит самый длинный совпадающий кусок между двуми последовательностями и возвращает match — объект с совпадением и дополнительной информацией.

get_matching_blocks() находит все совпадения между двумя последовательностями и возвращает список из match.

get_close_matches() находит слова, сильнее всего похожие на переданное слово, возвращает список строк.

По отдельности вроде все названия хороши и понятны. Но я утверждаю, что это — говнокод:

— find_longest_match вовращает объект-match, как и следует из названия; и get_matching_blocks возвращает такие же объекты, хотя название намекает, что должны возвращаться какие-то blocks

— get_close_matches, судя по названию, должен возвращать match, как find_longest_match — но возвращает строки

— одна и та же по сути операция (поиск совпадений) в одном случае называется find, а в двух других — get

В результате уже на следующий день не вспомнить, кто как называется, без обращения к документации.

Уж на уровне одного модуля можно напрячься и сохранить единообразие? Я предложил бы такие имена:

— find_longest_match()
— find_all_matches()
— find_similar_words() или просто find_similar()
источник
2019 January 03
Oh My Py
Исходники стандартной библиотеки

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

Core-разработчик Питона Реймонд Хеттингер тоже это заметил, и поэтому в документации к каждому модулю стандартной библиотеки первым делом идёт ссылка на исходники этого самого модуля на гитхабе.

Если вы прочитали описание функции или класса, а вопросы остались — не стесняйтесь пойти в исходный код и посмотреть, как оно там устроено. Большинство модулей отлично написаны, код понятный, в меру откомментирован.
источник
2019 January 04
Oh My Py
Enum здорового человека

Если программист привык писать код, как это делали наши пращуры со времён аграрной революции, то перечисления у него выглядят как-то так:

class PigeonState:
 eating = 0
 sleeping = 1
 flying = 2

PigeonState.sleeping
1


Конечно, у наших современников есть способ получше — enum.Enum:

import enum
class PigeonState(enum.Enum):
 eating = 0
 sleeping = 1
 flying = 2

PigeonState.sleeping.value
1


Это не просто более многословный способ сделать то же самое. У енумов есть вагон плюшек, недоступных староверам. Например, можно делать синонимы состояний:

class PigeonState(enum.Enum):
 eating = 0
 sleeping = 1
 flying = 2
 
 # There is no way Frank
 # is really doing that
 thinking = 1

PigeonState.thinking
<PigeonState.sleeping: 1>


Или добавлять свои атрибуты:

class PigeonState(enum.Enum):
 eating = (0, "Ест")
 sleeping = (1, "Спит")
 flying = (2, "Парит в небесах")
 
 def __init__(self, id, title):
   self.id = id
   self.title = title

PigeonState.flying.id
2

PigeonState.flying.title
'Парит в небесах'


А ещё можно: сортировать, сравнивать по is вместо ==, итерировать по значениям и создавать динамически. В общем, енумы — однозначное добро.
источник
2019 January 08
Oh My Py
Умолчательные значения настроек

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

Допустим, настройки по умолчанию мы сложили в словарь:

DEFAULTS = {
 "name": "Frank",
 "species": "pigeon",
 "age": 42,
}


А пользовательские настройки лежат в settings.ini. Их можно считать функцией load_settings(), которая тоже возвращает словарь.

Вопрос: как получить актуальное значение того или иного свойства?

Так себе способ:

custom = load_settings()

def get_setting_value(name):
 if name in custom:
   return custom[name]
 else:
   return DEFAULTS[name]


Способ лучше — воспользоваться collections.ChainMap:

from collections import ChainMap

# пусть custom ==
# { "species": "human" }
custom = load_settings()
settings = ChainMap(custom, DEFAULTS)

def get_setting_value(name):
 return settings[name]

get_setting_value("name")
'Frank'

get_setting_value("species")
'human'


В ChainMap можно запихать сколько угодно словарей, поиск по ним производится последовательно. Присваивание тоже работает:

settings["age"] = 33

custom
{'species': 'human', 'age': 33}

DEFAULTS
{'name': 'Frank', 'species': 'pigeon', 'age': 42}
источник
2019 January 09
Oh My Py
Посчитать количество объектов каждого типа

Допустим, вы пишете программу, которая обрабатывает заявки разных типов — идеи, вопросы и проблемы:

from collections import namedtuple
Request = namedtuple("Request", ("type", "text"))

requests = [
 Request(type="question", text="Как пасти котов?"),
 Request(type="problem", text="Бакланы портят стадион"),
 Request(type="idea", text="Переводчик с лисьего на русский"),
 Request(type="problem", text="Кот крадёт электричество"),
 Request(type="problem", text="Мыши похитили 540 кг марихуаны"),
 Request(type="idea", text="Холодильник с таймером"),
]


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

stats = {}
for req in requests:
 if req.type in stats:
   stats[req.type] += 1
 else:
   stats[req.type] = 1

stats
{'question': 1, 'problem': 3, 'idea': 2}


Прямо больно смотреть на этот if, верно? Лучше воспользоваться методом dict.setdefault(). Но как по мне, он тоже уродливый, поэтому ещё лучше — collections.defaultdict:

from collections import defaultdict
stats = defaultdict(lambda: 0)
for req in requests:
   stats[req.type] += 1

dict(stats)
{'question': 1, 'problem': 3, 'idea': 2}


А совсем хорошо — collections.Counter:

from collections import Counter
stats = Counter(req.type for req in requests)

dict(stats)
{'question': 1, 'problem': 3, 'idea': 2}


У счётчиков есть ещё пара полезных особенностей, но о них в другой раз.
источник
2019 January 19
Oh My Py
Операции со статистикой

Вернёмся к нашему примеру со статистикой по заявкам разных типов. Вот данные за три дня:

monday = {"question": 1, "problem": 3, "idea": 2}
tuesday = {"problem": 5, "idea": 1}
wednesday = {"question": 2, "problem": 2}


Как бы посчитать агрегированную статистику? Можно так, конечно:

def add_day(day_stats, stats):
 for key, value in day_stats.items():
   stats[key] += value
 return stats

stats = {"question": 0, "problem": 0, "idea": 0}
stats = add_day(monday, stats)
stats = add_day(tuesday, stats)
stats = add_day(wednesday, stats)

stats
{'question': 3, 'problem': 10, 'idea': 3}


Но вы наверняка догадываетесь, что это не наш метод. Поможет арифметика со счётчиками:

from collections import Counter
monday = Counter(monday)
tuesday = Counter(tuesday)
wednesday = Counter(wednesday)

stats = monday + tuesday + wednesday

stats
Counter({'problem': 10, 'question': 3, 'idea': 3})


Что насчёт самого популярного типа заявок?

stats.most_common(1)
[('problem', 10)]


А какие типы заявок встречались во вторник и в среду?

(tuesday | wednesday).keys()
dict_keys(['problem', 'idea', 'question'])


А сколько проблем было за все дни, кроме понедельника?

(stats - monday)["problem"]
7


Думаю, вы уловили идею ツ

P.S. Хотите реально злую штуку? Вот как посчитать агрегированную статистику в одну строчку:

sum(map(Counter, [monday, tuesday, wednesday]), Counter())


Ставьте 😮, если хотите подробный разбор этого однострочника (а также узнать, почему от него вздыхает Гвидо). Или ставьте 😎, если и так всё понятно.
источник
2019 January 21
Oh My Py
Подвох в функции sum()

Однострочник из предыдущей заметки заработал у меня не с первого раза. Причина — особенности работы функции sum() в питоне.

Разобрал все нюансы, пост получился великоват для телеграма, поэтому вынес его в блог: https://antonz.ru/sum-gotcha/
источник
2019 January 24
Oh My Py
Хранить последние N объектов

Допустим, вы пишете систему учёта посетителей для музея изящных искусств в Хиросиме (не спрашивайте). Одно из требований безопасников — команда tail, которая показывает трёх последних визитёров. Как её реализовать?

Конечно, можно складывать всех прибывших в список и по запросу выдавать из него последние 3 элемента:

TAIL_COUNT = 3
visitors = []

def handle(visitor):
 visitors.append(visitor)

def tail():
 return visitors[-TAIL_COUNT:]

handle("Питер")
handle("Клер")
handle("Френк")
handle("Кен Чан")
handle("Гоу Чан")

visitors
['Питер', 'Клер', 'Френк', 'Кен Чан', 'Гоу Чан']

tail()
['Френк', 'Кен Чан', 'Гоу Чан']


Но как-то не очень правильно хранить всех посетителей только ради того, чтобы показывать последних трёх, верно? Нам поможет collections.deque:

from collections import deque

visitors = deque(maxlen=3)

def handle(visitor):
 visitors.append(visitor)

def tail():
 return list(visitors)

handle("Питер")
handle("Клер")
handle("Френк")
handle("Кен Чан")
handle("Гоу Чан")

visitors
deque(['Френк', 'Кен Чан', 'Гоу Чан'], maxlen=3)

tail()
['Френк', 'Кен Чан', 'Гоу Чан']


deque (double-ended queue) хранит не более maxlen элементов, автоматически «выпихивая» старые при добавлении новых.

А ещё она добавляет элементы в начало и в конец за O(1), в отличие от списка, у которого это O(n). Идеально подходит, если коллекция часто модифицируется, а выбирать элементы по индексу не надо.
источник
2019 January 25
Oh My Py
Кортеж здорового человека

Наверняка вы сталкивались с ситуацией, когда нужно передать несколько свойств объекта одним куском. Например, информацию о домашнем питомце: тип, кличка и возраст.

Часто создавать отдельный класс под это дело лень, и используют кортежи:

("pigeon", "Френк", 3)
("fox", "Клер", 7)
("parrot", "Питер", 1)


Для большей наглядности подойдёт collections.namedtuple:

from collections import namedtuple

Pet = namedtuple("Pet", "type name age")
frank = Pet(type="pigeon", name="Френк", age=3)

>>> frank.age
3


Но что делать, если одно из свойств надо изменить? Френк стареет, а кортеж-то неизменяемый. Чтобы не пересоздавать его целиком, придумали метод _replace():

>>> frank._replace(age=4)
Pet(type='pigeon', name='Френк', age=4)


А если хотите сделать всю структуру изменяемой — _asdict():

>>> dict(frank._asdict())
{'type': 'pigeon', 'name': 'Френк', 'age': 3}


Удобно!
источник
2019 January 26
Oh My Py
Из десятичной дроби — в обычную

Субботний совет от капитана Очевидность. У класса float есть прекрасный метод as_integer_ratio(), который представляет десятичную дробь в виде обычной — пары «числитель, знаменатель»:

>>> (0.25).as_integer_ratio()
(1, 4)

>>> (0.5).as_integer_ratio()
(1, 2)

>>> (0.75).as_integer_ratio()
(3, 4)


Так вот. Никогда им не пользуйтесь ツ Потому что:

>>> (0.2).as_integer_ratio()
(3602879701896397, 18014398509481984)


Виной всему стандарт представления дробных чисел IEEE 754, который реализует float.

Используйте Decimal:

>>> from decimal import Decimal
>>> Decimal("0.2").as_integer_ratio()
(1, 5)


Уверен, вы и так это знаете. Просто на всякий случай ツ
источник