Архив рубрики: Python

Почему я не люблю Flask

Есть такой популярный microframework: Flask.

Многим нравится: легкий и простой для изучения, то да сё.

А мне — категорически нет.

Нелюбовь началась с элементарного: request — это thread local variable:

import flask
from myapp import app

@app.route('/')
def handler():
req = flask.request
if 'arg' in req.args:
process_arg(req.args['arg'])
###

Т.е. для для того чтобы узнать с какими GET или POST параметрами вызвали мой код — я должен обращаться к глобальной переменной!

Я знаю разницу между global variable и thread local variable если что — но это не избавляет от неприятного послевкусия.

Ага, есть еще и flask.g!

Если уж мне потребуются context local variables — я их буду использовать по моему выбору, морщась от осознания собственного несовершенства. Зачем flask их мне навязывает?

Дальше — больше.

Смотрим еще раз:

from myapp import app

@app.route('/')
def handler():
###

Имеем наполовину сконфигурированный импортированный откуда-то app, к которому добавляем обработчик.

Мне это не нравится. Я хочу сделать app и добавить в него route table.

Flask это позволяет, но документация провоцирует делать ровно наоборот.

Исполнять код на этапе импорта модуля не выглядит хорошей идеей, сейчас в этом я полностью уверен.

Идем дальше.

Параметры в route:

@app.route('/user/')
def handler(username):
pass

Весной это казалось мне удачным. Даже сделал что-то похожее в aiorest.

Потом понял, что штука абсолютно бесполезная: нам всегдатребовалось что-то из HTTP HEADERS, COOKIES и GET/POST parameres в обработчике запроста.

Чтобы проверить — авторизирован ли пользователь, например.

Выпилил.

С другой стороны проблема правильных параметров для обработчика не имеет красивого решения.

route args, GET, POST, COOKIES — каждый dict может иметь перекрывающиеся имена-названия.

Паша Коломиец в zorro попытался решить проблему через аннотации:

def handler(self, request: Request):
pass

Т.е. handler имеет параметр с аннотацией Request — он получит в него request object.

В zorro можно регистрировать свои аннотации для получения дополнительной информации.

Симпатично и элегантно — но слишком сложно для библиотеки для чайников.

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

Заключение

Я не призываю не использовать flask, у меня нет такой цели. Хотите граблей — получайте.

Просто сейчас я занялся добавлением в aiohttp WEB-сервера, пригодного для использования простым программистом.

И я точно знаю, чего не будет в aiohttpконтекстных переменныхи зависимостей на этапе импорта.

aiohttp.web должен быть прост насколько это возможно, но не проще.

Желающие выстрелить себе в ногу пусть делают это в библиотеках, построенных на основе aiohttp.web — мы дадим им такую возможность.

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

Автор: Andrew Svetlov

Поиск и ранжирование

Сегодня мы рассмотрим систему полнотекстового поиска, она позволяют искать слова в большом наборе документов и сортируют результаты поиска по релевантности найденных документов запросу.  Алгоритмы полнотекстового поиска относятся к числу важнейших среди алгоритмов коллективного разума. Новые идеи в этой области помогли сколотить целые состояния. Широко распространено мнение, что своей быстрой эволюцией от академического проекта к самой популярной поисковой машине в мире система Google обязана прежде всего алгоритму ранжирования страниц PageRank.

Что такое поисковая машина

Итак, давайте же создадим здоровую конкуренцию мировым поисковикам!) Статья так же будет полезна начинающим SEO специалистам, т.к. покажет некоторые величины, которые могут влиять на позиции вашего сайта в поиске.

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

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

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

Итак, для данной задачи нам потребуется DB (PostgreSQL) и Python (библиотека Grab). А за источник индексирования, новостную ленту rambler’а.
Все функции, описаные ниже, у меня являются методами классов, с такой инициализацией:

class Indexer:

def __init__(self):

self.conn = psycopg2.connect("dbname='postgres' user='postgres' password='120789' host='localhost'")

self.cur = self.conn.cursor()
self.cur.execute("set search_path to 'rambler_search'")

self.grab = Grab()

def __del__(self):
self.conn.close()

def dbcommit(self):
self.conn.commit()

Код паука

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

Эта функция в цикле обходит список страниц, вызывая для каждой функцию addtoindex. Далее она получает все ссылки на данной странице и добавляет их URL в список newpages. В конце цикла newpages присваивается pages, и процесс повторяется.

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

  
# Начинаем со списка страниц для индексации
# опускаемся в глубину на 2 уровня
def crawl(self, pages, depth=2):

rambler_news_href_pattern = re.compile(r'(^http://news.rambler.ru/[d]+)')

for i in range(depth):
newpages={}

for page in pages:

try:
self.grab.go(page)

except:
print "Could not open %s" % page
continue

try:
article_text = '' # текст статьи
for paragraph in self.grab.tree.xpath("//p[contains(@class, 'b-article__paragraph')]"):
article_text += paragraph.text_content()

self.addtoindex(page, article_text) # записываем в базу текст статьи с индексацией

links = self.grab.tree.xpath("//a")
for link in links:

if ('href' in link.attrib):
url = urljoin(page, link.attrib['href']).split('#')[0]# делаем полную ссылку и удаляем часть за # если она есть

match = rambler_news_href_pattern.findall(url)
if match:
url = match[0]

if url[0:4] == 'http' and not self.isindexed(url):
newpages[url] = 1

linkText = link.text_content() # текст ссылки
self.addlinkref(page, url, linkText) # записываем в базу текст ссылки с индексацией

self.dbcommit()

except Exception, e:
print "Could not parse page %s" % page, e

pages = newpages

Построение индекса

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

Создание схемы

Подождите запускать программу – нужно сначала подготовить базу данных. Схема простого индекса состоит из пяти таблиц. Первая (url_list) – это список проиндексированных URL. Вторая (word_list) – список слов, а третья (word_location) – список мест вхождения слов в документы. В оставшихся двух таблицах хранятся ссылки между документами. В таблице link хранятся идентификаторы двух URL, связанных ссылкой, а в таблице link_words есть два столбца – word_id и link_id – для хранения слов, составляющих ссылку. Вся схема изображена на рисунке ниже. Код запросов приводить не буду, должны ведь и вы поработать)))

Выделение слов на странице

Для решения данной задачи, воспользуемся простейшей функцией. Мы не будем вводить стоп слова, которые не нужно индексировать, а просто введем ограничение на длину слова в 3 символа, тем самым убрав предлоги и другой мусор (хоть и пострадают всякие мелкие слова, типа «мир», «тир», «пир»).
def getwords(html):
words = []
for split_str in html.split():
t = re.split("[s;:-_*".,?!()'&#«»]", split_str)
words += [a.lower() for a in t if a != '' and len(a) > 3]
return words

Добавление в индекс

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

  
def addtoindex(self, url, text):

if self.isindexed(url): return # если уже индексирована - пропускаем
print 'Indexing %s' % url

# Получаем слова из текста
words = getwords(text)

# Получаем id url'а
url_id = self.getentryid('url_list', 'url', url)

# Связываем слова с этим урлом
for i, word in enumerate(words): # пронумеруем слова
word_id = self.getentryid('word_list', 'word', word)
self.cur.execute("insert into word_location(url_id, word_id, location) values (%d, %d, %d)" % (url_id, word_id, i))
self.dbcommit()
Хорошо, теперь нужно описать функцию getentryid — возвращающую id записи из бд и isindexed — проверяющую, не индексировали ли мы эту страницу раньше.

  
# Узнаем id записи в БД, если нет
# иначе записываем и возвращаем новый
def getentryid(self, table, field, value, createnew=True):

self.cur.execute("select id from %s where %s = '%s'" % (table, field, value))
cur = self.cur.fetchone()

if not cur:
# print (table, field, value)
self.cur.execute("insert into %s (%s) values ('%s') returning %s.id" % (table, field, value, table))
cur = self.cur.fetchone()
self.dbcommit()

return cur[0]

else:
return cur[0]

# Возвращаем True, если посещали страницу
def isindexed(self, url):
self.cur.execute("select id from url_list where url = '%s'" % url)
u = self.cur.fetchone()

if u:
# Проверяем, что паук посещал страницу
self.cur.execute("select count(1) from word_location where url_id = %d" % u[0])
v = self.cur.fetchone()

if v[0]:
return True

return False

Последняя функция, необходимая нам для начала индексирования — это addlinkref. Как следует из ее названия, она заносит ссылки и слова из которых они состоят к нам в БД.

 def addlinkref(self, urlFrom, urlTo, linkText):

words = getwords(linkText)
from_id = self.getentryid('url_list', 'url', urlFrom)
to_id = self.getentryid('url_list', 'url', urlTo)

if from_id == to_id: return

self.cur.execute("insert into link(from_id, to_id) values (%d, %d) returning link.id" % (from_id, to_id))
link_id = self.cur.fetchone()[0]

for word in words:

word_id = self.getentryid('word_list', 'word', word)
self.cur.execute("insert into link_words(link_id, word_id) values (%d, %d)" % (link_id, word_id))

Ну что же, на этом этапе мы можем запустить паука и создать базу данных)

Запросы

Теперь, когда у нас есть материал, с которым можно работать, необходимо написать сам поисковик. Начнем мы с мал
ого, грубого поиска, по абсолютному вхождению фразы в новость.

Таблица word_location обеспечивает простой способ связать слова с документами, так что мы можем легко найти, какие страницы содержат данное слово. Однако поисковая машина была бы довольно слабой, если бы не позволяла задавать запросы с несколькими словами. Чтобы исправить это упущение, нам понадобится функция, которая принимает строку запроса, разбивает ее на слова и строит SQL-запрос для поиска URL тех документов, в которые входят все указанные слова.

 def get_match_rows(self, query):

select_query_add = []
join_query_add = []
where_query_add = []

main_search_query = """
SELECT wl0.url_id, %s
FROM word_location wl0
%s
WHERE %s
"""

query_words = getwords(query)

query_word_ids = []
for query_word in query_words:

self.cur.execute("select id from word_list where word = '%s'" % query_word)
query_word_id = self.cur.fetchone()

if query_word_id:
query_word_ids.append(query_word_id[0])

if query_word_ids:
for position, query_word_id in enumerate(query_word_ids):

if position:
join_query_add.append('JOIN word_location wl%d ON wl%d.url_id = wl%d.url_id' % (position, position-1, position))

select_query_add.append('wl%d.location' % position)
where_query_add.append('wl%d.word_id = %d' % (position, query_word_id))

main_search_query = main_search_query % (', '.join(select_query_add), ' '.join(join_query_add), ' and '.join(where_query_add))

self.cur.execute(main_search_query)
search_results = self.cur.fetchall()

return search_results, query_word_ids
Данная функция всего лишь создает строгий JOIN, типа такого:

 
SELECT wl0.url_id, wl0.location, wl1.location
FROM word_location wl0
JOIN word_location wl1 ON wl0.url_id = wl1.url_id
WHERE wl0.word_id = 2734 and wl1.word_id = 2698

В результате, получаем список кортежей и список с id слов. Каждый кортеж — это сто список вида (id новости, позиция 1го слова, позиция 2го слова…)

Ранжирование по содержимому

Итак, худо-бедно, но мы научились искать по индексу. Однако возвращаем мы их в том порядке, в котором они посещались пауком. Чтобы решить эту проблему, необходимо как-то присвоить страницам ранг относительно данного запроса и уметь возвращать их в порядке убывания рангов.

В этом разделе мы рассмотрим несколько способов вычисления ранга
на основе самого запроса и содержимого страницы, а именно:

  • Частота слов 

Количество вхождений в документ слова, указанного в запросе, помогает определить степень релевантности документа.

  • Расположение в документе 

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

  • Расстояние между словами 

Если в запросе несколько слов, то они должны встречаться в документе рядом.

Самые первые поисковые машины (кстати rambler.ru) часто работали только с подобными метриками и тем не менее давали пристойные результаты. Давайте добавим следующие методы:

 # Функция, которая взвешивает результаты
def get_scored_list(self, rows, word_ids):

total_scores = {row[0]: 0 for row in rows}

if rows:
# Список весовых функций
weight_functions = [
]

for (weight, scores) in weight_functions:
for url in total_scores:
total_scores[url] += weight * scores[url]

return total_scores

# Возвращает полный урл по id
def get_url_name(self, url_id):

self.cur.execute("select url from url_list where id = %d" % url_id)

return self.cur.fetchone()[0]

# Основная функция поиска
def search(self, search_sentence):

search_results, word_ids = self.get_match_rows(search_sentence)
scores = self.get_scored_list(search_results, word_ids)

ranked_scores = [(score, url) for (url, score) in scores.items()]
ranked_scores.sort()
ranked_scores.reverse()

for (score, url_id) in ranked_scores[0:10]:
print '%ft%s' % (score, self.get_url_name(url_id))

return word_ids, [r[1] for r in ranked_scores[0:10]]

Наиболее важна здесь функция get_scored_list, код которой мы будем постепенно уточнять. По мере добавления функций ранжирования мы будем вводить их в список weight_functions и начнем взвешивать полученные результаты.

Функция нормализации

Все рассматриваемые ниже функции ранжирования возвращают словарь, в котором ключом является идентификатор URL, а значением – числовой ранг. Иногда лучшим считается больший ранг, иногда – меньший. Чтобы сравнивать результаты, получаемые разными методами, необход
имо как-то нормализовать их, то есть привести к одному и тому же диапазону и направлению.
Функция нормализации принимает на входе словарь идентификаторов и рангов и возвращает новый словарь, в котором идентификаторы те же самые, а ранг находится в диапазоне от 0 до 1. Ранги масштабируются по близости к наилучшему результату, которому всегда припи- сывается ранг 1. От вас требуется лишь передать функции список рангов и указать, какой ранг лучше – меньший или больший:
 def normalize_scores(self, scores, smallIsBetter=0):

vsmall = 0.00001 # Avoid division by zero errors

if smallIsBetter:
minscore = min(scores.values())
return {u: float(minscore)/max(vsmall, l) for (u,l) in scores.items()}

else:
maxscore = max(scores.values())
if maxscore == 0: maxscore = vsmall
return {u: float(c)/maxscore for (u,c) in scores.items()}

return scores
Отлично! Пора заняться весовыми функциями, для которых мы и придумали эту нормализацию.

Весовые функции

Частота слов

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

def frequency_score(self, rows):

counts = {row[0]:0 for row in rows}
for row in rows:
counts[row[0]] += 1

return self.normalize_scores(counts)

Чтобы активировать ранжирование документов по частоте слов, измените строку функции get_scored_list, где определяется список weight_functions, следующим образом:

weight_functions = [(1.0,self.frequency_score(rows))]
Запустите поиск снова и радуйтесь!)

Расположение в документе

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

def location_score(self, rows):

locations = {}
for row in rows:
loc = sum(row[1:])
if locations.has_key(row[0]):
if loc < locations[row[0]]:
locations[row[0]] = loc
else:
locations[row[0]] = loc

return self.normalizescores(locations, smallIsBetter=1)

Расстояние между словами

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

 def distance_score(self, rows):

mindistance = {}

# Если только 1 слово, любой документ выигрывает
if len(rows[0]) <= 2:
return {row[0]: 1.0 for row in rows}

mindistance = {}

for row in rows:
dist = sum([abs(row[i]-row[i-1]) for i in xrange(2, len(row))])

if mindistance.has_key(row[0]):
if dist < mindistance[row[0]]:
mindistance[row[0]] = dist
else:
mindistance[row[0]] = dist

return self.normalize_scores(mindistance, smallIsBetter=1)

Использование внешних ссылок на сайт

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

Простой подсчет ссылок

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

def inbound_link_score(self, rows):

unique_urls = {row[0]: 1 for row in rows}
inbound_count = {}

for url_id in unique_urls:
self.cur.execute('select count(*) from link where to_id = %d' % url_id)
inbound_count[url_id] = self.cur.fetchone()[0]

return self.normalize_scores(inbound_count)

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

pep8 и 80 символов в строке

На самом деле 79 если внимательно читать pep 8, если что…

Посетил на днях Python Party, организованной компанией Yandex.
Мероприятие понравилось, а самый интерес был потом, на «поболталках» в кабаке.

Был на party доклад от Кирилла Борисова «Контроль за стилем кода». Интересный и толковый.

Только я возражаю против с высказывания вроде:
  — pep8 рекомендует ограничивать длину строк в 79 символов максимум. Мы с этим несогласны — сейчас мониторы большие, можно писать 120 символов и это великолепно помещается на экране.

Я везде строго пишу с ограничением на 79 символов.

Попробую объяснить почему.

1. Во первых сам код Python так написан и patch вылезающий за границы просто не будет принят. OK, я committer — значит тем более обязан следовать соглашениям.
2. Во вторых мой редактор (emacs если что) настроен на то чтобы подсвечивать длинные строки. И когда я открываю код библиотеки, наплевавшей на ограничение по длине строки — у меня половина экрана «красная». Это огорчает.
3. В третьих и главное: если у вас широкий монитор — это прекрасная возможность разбить его по вертикали и видеть одновременно несколько редактируемых файлов. У меня даже на 14'' ноутбуке Full HD — это значит что при размере шрифта в 13pt у меня помещается два буфера. Коллега на 24'' привык работать в vim с шестью буферами: 3х2. Это очень удобно — гораздо лучше видеть сразу несколько файлов чем один, но с длинными строками.

Что до «невозможности» уместить код в 79 символов — это распространенное заблуждение.
При некотором навыке всё легко получается.

К тому же такой подход провоцирует сохранение промежуточных вычислений в локальные переменные — что хорошо само по себе, так как улучшает читабельность кода (вы же даете переменным не слишком длинные, но «говорящие» имена, верно?)

Коротко говоря, 79 символов заставляют лучше писать код и помогают его читать. Что вам всем и рекомендую.

Автор: Andrew Svetlov

Перегрузка операций

В питоне буквально все используют магические методы. Когда пишем конструктор класса — называем его __init__ и т.д.

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

Поговорим о правильной перегрузке математических операций.

Создаем класс-точку

Итак, имеем точку в двухмерном пространстве:

class Point(object):

def __init__(self, x, y):
self.x = x
self.y = y

def __repr__(self):
return 'Point({}, {})'.format(self.x, self.y)

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

Обновленная версия:

class Point(object):

def __init__(self, x, y):
self._x = x
self._y = y

@property
def x(self):
return self._x

@property
def y(self):
return self._y

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

Над точками нужно производить какие-то операции. Самая, наверное, распространенная — это сравнение.

Сравнение

class Point:

# ...

def __eq__(self, other):
return self._x == other._x and self._y == other._y

Что плохо? То, что попытка сравнить точку с не-точкой (Point(1, 2) == 1) выбросит исключение AttributeError:

>>> Point(1, 2) == 1
AttributeError: 'int' object has no attribute '_x'

в то время как стандартные питоновские типы ведут себя иначе:

>>> 1 == 'a'
False

Меняем сравнение:

def __eq__(self, other):
if not isinstance(other, Point):
return False
return self._x == other._x and self._y == other._y

Теперь сравнивание работает почти правильно:

>>> Point(1, 2) == Point(1, 2)
True

>>> Point(1, 2) == 1
False

Слово почти я употребил потому, что Питон работает так:

  • сначала пытается сделать сравнение a == b
  • если сравнение не дает результата — делается вторая попытка с перестановкой операторов b == a

Чтобы сказать, что операция сравнения не дает результата — нужно вернуть константу NotImplemented (не путать с исключением NotImplementedError):

def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self._x == other._x and self._y == other._y

В паре с == всегда идет оператор !=, не нужно про него забывать:

def __ne__(self, other):
return not (self == other)

На самом деле Питон будет сам использовать метод __eq__ если __ne__ не определен, но я считаю что лучше и понятней написать __ne__ самому, тем более что это не трудно.

hash

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

>>> {Point(1, 2): 0}
TypeError: unhashable type: 'Point'

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

def __hash__(self):
return hash((self._x, self._y))

Результат:

>>> {Point(1, 2): 0}
{Point(1, 2): 0}

Определять только __hash__ без __eq__/__ne__ неправильно: в случае коллизии задействуются операторы сравнения. Если они не определены — можно получить некорректный результат.

Упорядочивание

Как говорил один преподаватель, не используйте слово «сортировка» — оно очень созвучно слову «сортир».

Точки на плоскости не имеют естественного порядка. Поэтому реализовывать операторы упорядочивания (<, >, <=, >=) не нужно.

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

Если для какой-то цели вы придумали принцип упорядочивания для точек на плоскости -- сделайте это нормальным методом класса со своим именем, не нужно вводить в изумление пользователей.

Арифметика

Точки можно складывать и вычитать.

def __add__(self, other):
if not isinstance(other, Point):
return NotImplemented
return Point(self._x + other._x, self._y + other._y)

def __sub__(self, other):
if not isinstance(other, Point):
return NotImplemented
return Point(self._x - other._x, self._y - other._y)

Пример:

>>> Point(1, 2) + Point(2, 3)
Point(3, 5)

>>> Point(1, 2) - Point(2, 3)
Point(-1, -1)

Так как точки неизменяемые, то возвращается новый объект.

Вообще оператор + подразумевает, что мы создаем в качестве результата что-то новое, а не меняем какой-то из аргументов.

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

res = a.__add__(b)
if res is NotImplemented:
res = b.__radd__(a)

Реализуем и эти методы:

def __radd__(self, other):
if not isinstance(other, Point):
return NotImplemented
return other + self

def __rsub__(self, other):
if not isinstance(other, Point):
return NotImplemented
return other - self

Зачем это нужно? Допустим, мы хотим складывать наши точки с QPointиз библиотеки PyQt, полуая в результате опять объекты класса Point.

Тогда нужно расширить наши __add__ и __radd__:

def __add__(self, other):
if isinstance(other, Point):
return Point(self._x + other._x, self._y + other._y)
elif isinstance(other, QPoint):
return Point(self._x + other.x(), self._y + other.y())
return NotImplemented

def __radd__(self, other):
if isinstance(other, Point):
return Point(self._x + other._x, self._y + other._y)
elif isinstance(other, QPoint):
return Point(self._x + other.x(), self._y + other.y())
return NotImplemented

Реализацию __iadd__/__isub__ рассматривать не буду, там всё очевидно. К тому же Питон сам способен сделать как надо, сконструировав нужный код на основе вызовов __add__/__sub__.

Умножение и деление для точек не имеют смысла, поэтому их просто не делаем. Если бы делали, скажем, операции над векторами -- ввели бы скалярное и векторное произведения. "Просто точкам" эти излишества не нужны.

Заключение

Вот и всё, набросок для класса точки готов.

Надеюсь, хотя бы некоторым читателям написанное окажется полезным.

Автор: Andrew Svetlov

Пространства имен модулей

Модули будут, вероятно, более понятны, если представлять их, как простые пакеты имен, – то есть место, где определяются переменные, которые должны быть доступны остальной системе.
С технической точки зрения каждому модулю соответствует отдельный файл, и интерпретатор создает объект модуля, содержащий все имена, которым присвоены какие-либо значения в файле модуля. Проще говоря, модули – это всего лишь пространства имен (места, где создаются имена), и имена, находящиеся в модуле, называются его атрибутами.В данной тематике мы разберем как работает этот механизм.

Файлы создают пространства имен
Итак, как же файлы трансформируются в пространства имен? Суть в том, что каждое имя, которому присваивается некоторое значение на верхнем уровне файла модуля (то есть не вложенное в функции или в классы), превращается в атрибут этого модуля.
Например, операция присваивания, такая как X = 1, на верхнем уровне модуля M.py превращает имя X в атрибут модуля M, обратиться к которому из-за пределов модуля можно как M.X. Кроме того, имя X становится глобальной переменной для программного кода внутри M.py, но нам необходимо более формально объяснить понятия загрузки модуля и областей видимости, чтобы понять, почему:
•  Инструкции  модуля  выполняются  во  время  первой  попытки  импорта. Когда модуль импортируется в первый раз, интерпретатор Python создает пустой объект модуля и выполняет инструкции в модуле одну за другой, от начала файла до конца.
•  Операции присваивания, выполняемые на верхнем уровне, создают атрибуты  модуля.  Во  время  импортирования  инструкции  присваивания,  выполняемые на верхнем уровне файла и не вложенные в инструкции def или class (например, =, def), создают атрибуты объекта модуля – при присваивании имена сохраняются в пространстве имен модуля.
•  Доступ  к  пространствам  имен  модулей  можно  получить  через  атрибут __dict__  или  dir(M).  Пространства  имен  модулей,  создаваемые  операцией импортирования, представляют собой словари – доступ к ним можно получить через встроенный атрибут __dict__, ассоциированный с модулем, и с помощью функции dir. Функция dir – это примерный эквивалент отсортированного списка ключей атрибута __dict__, но она включает унаследованные имена классов, может возвращать не полный список и часто изменяется от версии к версии.
 Модуль  –  это  единая  область  видимости  (локальная  является  глобальной). Как мы видели в главе 17, имена на верхнем уровне модуля подчиняются тем же правилам обращения/присваивания, что и имена в функциях, только в этом случае локальная область видимости совпадает с глобальной (точнее, они следуют тому же правилу LEGB поиска в областях видимости, с которым мы познакомились в главе 17, только без уровней поиска L и E). 
Но в модулях область видимости модуля после загрузки модуля превращается в атрибут-словарь объекта модуля. В отличие от функций (где локальное пространство имен существует только во время выполнения функции), область видимости файла модуля превращается в область видимости атрибутов объекта модуля и никуда не исчезает после выполнения операции импортирования.
Ниже эти понятия демонстрируются в программном коде. Предположим, мы 
создаем в текстовом редакторе следующий файл модуля с именем module2.py:
Print(‘starting to load…’)
import sys
name = 42
 
def func(): pass
 
class klass: pass
 
print(‘done loading.’)

Когда  модуль  будет  импортироваться  в  первый  раз  (или  будет  запущен  как программа), интерпретатор выполнит инструкции модуля от начала до конца. В ходе операции импортирования одни инструкции создают имена в пространстве имен модуля, а другие выполняют определенную работу. Например, две инструкции print в этом файле выполняются во время импортирования:
>>> imp
ort module2
starting to load…
done loading.
Но  как  только  модуль  будет  загружен,  его  область  видимости  превратится в пространство имен атрибутов объекта модуля, который возвращает инструкция import. После этого можно обращаться к атрибутам в этом пространстве 
имен, дополняя их именем вмещающего модуля:
>>> module2.sys
 
>>> module2.name
42
 
>>> module2.func
>
 
>>> module2.klass
Здесь именам sys, name, func и klass были присвоены значения во время выполнения инструкций модуля, поэтому они стали атрибутами после завершения операции импортирования. О классах мы будем говорить в шестой части книги, но обратите внимание на атрибут sys – инструкции  import действительно присваивают объекты модулей именам, а любая операция присваивания на 
верхнем уровне файла создает атрибут модуля.
Внутри интерпретатора пространства имен хранятся в  виде объектов словарей. Это самые обычные объекты словарей с обычными методами. Обратиться к словарю пространства имен модуля можно через атрибут __dict__ модуля (не забудьте обернуть вызов этого метода вызовом функции list – в Python 3.0 он возвращает объект представления!):
>>> list(module2.__dict__.keys())
[‘name’, ‘__builtins__’, ‘__file__’, ‘__package__’, ‘sys’, ‘klass’, ‘func’,
‘__name__’, ‘__doc__’]
Имена, которые были определены в файле модуля, становятся ключами внутри словаря, таким образом, большинство имен здесь отражают операции 
присваивания на верхнем уровне в файле. Однако интерпретатор Python  добавляет в пространство имен модуля еще несколько имен, например __file__ содержит имя файла, из которого был загружен модуль, а  __name__ – это имя, под которым модуль известен импортерам (без расширения .py и без пути к каталогу).

Автор: Няшный Человек
Дата публикации: 2014-08-16T04:04:00.000+03:00

exrex генератор данных по регулярным выражениям

Всем привет!

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

API крайне простой
count — возвращет количество возможных вариаций
parse — парсер регулярных выражений
getone — возвращает псевдо-рандомное значение
generate — возвражает генератор со всеми вариантами

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

На всякий замечу, что у вас по некоторым примером вывод может быть другим

>>> print exrex.getone('[A-Z][a-z]{1,10} [A-Z][a-z]{1,10}')
Boris Yeltsin

>>> print exrex.getone('+[0-9]([0-9]{3}) [0-9]{3}-[0-9]{2}-[0-9]{2}')
+5(777) 790-68-69

>>> print [r for r in exrex.generate('|'.join(str(i) for i in range(5)))]
[u'0', u'1', u'2', u'3', u'4']

>>> exrex.count('[0-9]{1}')
10

>>> print exrex.parse('[0-9]{1,100}')
[('max_repeat', (1, 100, [('in', [('range', (48, 57))])]))]

Есть также возможность запуска из консоли

>>> python -m exrex -r [0-9]{2}
31
Возвращает псевдо-рандомное число из 2 цифр

За более подробной информацией в ссылки.


Ссылки

https://github.com/asciimoo/exrex
http://exrex.readthedocs.org/

Автор: Евгений Курочкин