28 мая 2009 г.

Мысли по поводу list comprehension

List comprehension — приятная штука, позволяющая во многих ситуациях выразить мысль не только коротко, но и понятно. И всё же иногда кажется, что там чего-то не хватает. Вот несколько вариантов записи одного выражения:
[target for target in [long_expr_of(source) for source in sources]
 if test_expr_of(target)]

filter(lambda x: test_expr_of(x),
       [long_expr_of(source) for source in sources])

[long_expr_of(source) for source in sources
 if test_expr_of(long_expr_of(source))]
Первый способ громоздок, да и читаются вложенные списки не так хорошо. Второй способ выглядит неплохо, только если есть готовая функция test_expr_of и нет необходимости писать lambda-выражение. Третий способ неприятен с точки зрения эффективности. В таких случаях я предпочитаю разбивать вычисление на два выражения:
targets = [long_expr_of(source) for source in sources]
targets = [target for target in targets if test_expr_of(target)]
Вроде всё замечательно и ничего больше не нужно. И всё-таки, вспоминая SQL, хочется записать что-то вроде:
[long_expr_of(source) as target for source in sources
 if test_expr_of(target)]
Выглядит очень приятно. Может стоит написать PEP?

20 мая 2009 г.

Точность определения страны и региона с помощью ip2cc

При использовании таких продуктов, как ip2cc всегда возникает вопрос, насколько точно он позволяет определить местонахождения пользователя. К сожалению, точный ответ на этот вопрос получить невозможно, но можно попытаться оценить её по косвенным признакам. И мне представилась такая возможность. Около полугода назад (свежесть здесь тоже имеет значение, так как блоки IP иногда перерегистрируются) мне посчастливилось участвовать в одном проекте, в котором пользователь при регистрации может указать страну и регион (для России). К настоящему времени таких пользователей набралось около 20000 — достаточное количество для проведения оценки. Проект ориентирован на русскоязычную аудиторию, поэтому 93% пользователей из России. Благодаря ориентиру на широкую аудиторию, представленными оказались 77 стран и все 83 региона России.
Вот что получилось при сравнении страны, определённой по IP с помощью базы ip2cc, с введённой пользователем:
Правильно — 96.1%, Не правильно — 3.7%, Нет данных — 0.2%
Картинка представляется очень приятной, однако следует понимать, что она несколько искажается из-за того, что большинство пользователей из России — такова специфика проекта. Если брать только пользователей, выбравших при регистрации другую страну, то всё становится не таким радужным:
Правильно — 82.7%, Не правильно — 16.5%, Нет данных — 0.8%
Подавляющее большинство пользователей, попавших здесь в категорию "нет данных" имеют IP, зарегистрированный на Европу без указания конкретной страны.
Ну и, наконец, регионы России. Здесь я выделил в отдельную категорию пользователей из Москвы, Московской области, Санкт-Перербурга и Лениградской области (это всё разные субъекты федерации), для которых удалось определить регион с точностью путаницы город–область. Дело в том, что значительное количество пользователей живя в области посещают проект с работы, находящейся в областном центре, и наоборот, выбирают при регистрации город, вынесенный вверх списка, не утруждая себя поисками области. Следует их считать определёнными неправильно или нет, зависит от задачи.
Правильно — 79.1%, Город/область — 4.9%, Не правильно — 13.8%, Нет данных — 2.2%
Мне сложно судить, насколько качественная такая оценка. Далеко не всегда пользователи заходят из того региона, к которому они себя относят. Например, мне сложно поверить, чтобы IP, зарегистрированный на европейский университет, в реальности находился бы в России (хотя и такое бывает — в своё время пул IP-адресов одной из сетей МГУ им. Ломоносова был зарегистрирован на Германию). Скорее всего, человек из России просто проходит там сейчас обучение. Поэтому указанные здесь цифры следует воспринимать только как нижнюю оценку точности, фактически гарантирующие, что определение будет не хуже.
P.S. Для тех кто не в курсе, приведённые здесь диаграммы сделаны с помощью API Google Chart. И чтобы как-то разбавить скучные статистические данные этого поста, приведу здесь использованную мной функцию для их получения:
def pie_slice(items):
    values = []
    labels = []
    colors = []
    for label, value, color in items:
        values.append('%.1f' % value)
        label = u'%s \u2014 %.1f%%' % (label, value)
        labels.append(label.encode('utf-8'))
        colors.append(color)
    data = dict(
        cht='p3',
        chd='t:'+','.join(values),
        chs='500x100',
        chl='|'.join(labels),
        chco=','.join(colors)
    )
    url = 'http://chart.apis.google.com/chart?'+urlencode(data)
    alt = ', '.join(labels)
    return '<img src="%s" alt="%s">' % (url, alt)

7 мая 2009 г.

ip2cc 0.5: теперь и регионы России

Как-то в далёком 2002 году я написал небольшой модуль ip2cc, позволяющий определять страну по IP и, что более важно, самостоятельно обновлять локальную базу на основании данных из официальных источников. И даже написал по этому поводу статью. После этого появились два новых регистратора LACNIC и AfriNIC, обнаружился баг в модуле bsddb, который сподвиг меня на написание своего узкоспециализированного B-Tree хранилища для IP-адресов. Модуль bsddb с тех пор починили, однако своё хранилище оказалось лучше по производительности, проще, стабильнее — возращаться назад к bsddb я уже не вижу смысла. С тех пор много воды утекло, определять страну как-то не требовалось, потому я о нём забыл. И вот на днях мне снова потребовалось определять местонахождения пользователя по IP, но не только страну, а ещё и регион России. И вот результат — ip2cc 0.5, в котором добавлена возможность создания базы IP по регионам России на основе данных проекта IpGeoBase. Данные IpGeoBase позволяют определить и город, но у меня такой необходимости пока нет, поэтому я ограничился субъектами федерации.

4 мая 2009 г.

Несколько баз данных в SQLAlchemy и наследование

Как я писал ранее, в SQLAlchemy есть средства для работы с нескольким базами. Первая проблема, с которой я столкнулся — автоматический выбор соединения перестаётся работать для моделей с наследованием. Дело в том, что метод get_bind() сессии делает поиск соединения с нашем словаре на основе атрибута mapped_table маппера объекта. Для обычных моделей это объект класса Table, на основе которого я и строил словарь с соединениями. Но для производных классов при использовании joined table inheritance это объект класса Join для нескольких таблиц. На самом деле выбор соединения для запросов (каковым объект Join и является) в SQLAlchemy реализован, но почему-то не используется для моделей. Во избежание переписывания всего метода get_bind() я просто передал mapped_table вторым аргументом:
class DBSession(sqlalchemy.orm.session.Session):

    def get_bind(self, mapper, clause=None):
        if mapper is not None and clause is None:
            c_mapper = sqlalchemy.orm.util._class_to_mapper(mapper)
            if hasattr(c_mapper, 'mapped_table'):
                clause = mapper.mapped_table
        return sqlalchemy.orm.session.Session.get_bind(self, mapper, clause)