Blog Trac Lyrics for music

All articles, tagged with “python”

Global Interpreter Lock: факты и мифы

«И вы ещё говорите о параллельном выполнении!..»
Erlang BEAM

Что такое GIL?

Global Interpreter Lock используется для того, чтобы только один поток мог выполнять питоновский код и манипулировать данными в виртуальной машине CPython. Эта блокировка отпускается и захватывается каждые 100 инструкций (по умолчанию, но можно изменить при помощи sys.setcheckinterval()), а также перед участками кода, которые могут выполняться длительное время и не модифицируют данные. Например, I/O.

 continue reading

Отчёт о Exception Masterclass #08

Итак, состоялся восьмой Exception, за что организторам и докладчикам отправляются всевозможные благодарности. Как уже всем известно (а особенно тем, кто присутствовал :)), этот эксепшен проходил в форме мастер-класса. Мастер-класс уже был опробован на седьмом Эксепшене, когда Андрей Светлов рассказывал про метаклассы и дескрипторы в Питоне, на что было получено некоторое количество положительных отзывов.

 continue reading

Видео с Exception #07

Оказывается, некоторое видео с седьмого Эксепшена уже появилось неделю-две как, а никто о том и не знает. Пока больше ничего нет, насколько я понимаю, поэтому смотрим то, что есть. К сожалению, пока не выложили самого нужного видео — с докладом Дмитрия Кожевина :). Да, из-за того, что выложено на ютубе, картинка качеством не блещет и бывает сложно разобрать, что же там на экране изображено.

Доклады

PickledManyToManyField в Django

Однажды в студёную зимнюю пору понадобилось срочно подняться нам в гору сохранять ManyToMany в истории изменений. Само приложение для истории изменений обитает здесь, но там пока что старая версия, которая даже ForeignKey не умеет сохранять (когда доделаю всё - выложу). Два слова о том, как работает эта самая история работает.

Хранение истории

Для каждой модели, которая будет архивироваться создаётся своя собственная модель-история. Из нужной модели через TargetModel._meta.fields вытягиваются все поля и пересоздаются для новой модели. Ненужные поля отсекаются (например, AutoField), ненужные настройки - тоже (таких целая куча). Также в модель-историю добавляются несколько своих полей: для сохранения IP-адреса, номера ревизии, времени изменения, ссылки на оригинальную модель.

Потом через dispatcher к сигналу post_save присоединяется функция, которая определяет изменилась ли модель, и если да - сохраняется новая ревизия, в которую копируются все поля.

Хранение ManyToMany

Если хранить m2m таким же способом, как все остальные поля, то в таблице в которой будут храниться эти отношения достаточно несложно достичь огромного количества записей. Если в среднем около пятидесяти связей и двухсот ревизий на объект, то нужно всего 10‘000 объектов для ста миллионов отношений в m2m-таблице. Значит, хранить нужно каким-то альтернативным образом. Выход - сохранять отношения в той же таблице.

Задача - написать своё поле, которое внешне будет выглядеть как обычное ManyToManyField, но хранить данные «по месту прописки». Единственное - у него не может быть такой же обратной связи, по понятным причинам. Проведённые исследования показали, что проще всего унаследоваться от базового Field, а не от ManyToManyField, так как переписывать нужно практически все значимые методы, и дополнительные классы (дескриптор, менеджер, rel-класс). А также, информацию про отношение (rel-класс) нельзя хранить в field.rel, как это делается во всех остальных полях с отношениями, потому что в таком случае Django пытается создать обратное отношение (от целевой модели к стартовой), а в случае с Pickled организовать такую связь на уровне таблицы невозможно.

Так как сигналы для ManyToMany пока ещё не включены в Django, то это поле ещё немного бесполезно. А пока кода в django-history нет, на него можно посмотреть на dumpz.org, а покритиковать здесь.

Miranda + Python (MirPy)

Как-то так случилось, что Miranda потеряла в своей базе список контактов (дважды). Восстановить-то она его восстановила - список хранится на сервере, но вся история сообщений потерялась, так как в базе данных все события привязаны к внутренним айдишникам контактов. Я уже думал писать программу, которая будет разбирать вручную кривую мирандовскую базу данных, но тут я наткнулся на замечательный плагин - MirPy (miranda forums), который предоставляет питоновскую консоль.

Я использовал последнюю на данный момент версию - 0.1.3.1 (static), которая скомпилирована с поддержкой Python 2.5. Подключаем плагин к миранде, запускаем - MirPy показывает формочку, с полем для ввода и полем для вывода, куда перенаправляется весь обычный вывод. В нашем распоряжении - весь питон со всеми установленными расширениями, плюс модули MirPy, clist, contact, database, messaging, mirandamisc, popups, status. Какой-либо внешней документации я не нашёл, только три примера от автора плагина на miranda forum‘ах. Впрочем, я документацию особо и не искал, а воспользовался таким кусочком кода:

import MirPy
import database
from pprint import pprint
MirPy.ConsoleClear()

pprint(database.__dict__)
for k, v in database.__dict__.items():
    if v.__doc__:
        print k, '::\n', v.__doc__, '\n\n'

Он вывел всю информацию, которая мне впоследствии понадобилась для поставленной задачи.

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

import MirPy
import database
MirPy.ConsoleClear()

# first -> next
contacts = []
contact = database.ContactFindFirst()
while contact:
    contacts.append(contact)
    contact = database.ContactFindNext(contacts)

print contacts

# brute-force
# needs several tens of seconds to execute
contacts = []
min_cont, max_cont = 0, 25000000
for contact in xrange(min_cont, max_cont):
    if database.ContactIs(contact):
        contacts_all.append(contact)

print contacts

Первым способом достало 141 контакт, вторым - 467. Дальше, собственно, можно получить всю историю, а значит, нужно разобраться с форматом событий. У события есть такие параметры: timestamp, flags, blob, module, eventType. Лезть в исходники Миранды было лень, и потому я просто поглядел на дамп сырых данных. Результаты:

  • timestamp - ясное дело, проблем не вызвал; это время события от Эпохи
  • blob - тело сообщения
  • module - строка, которая идентифицирует модуль (протокол). В моём случае - ‘ICQ’, ‘JABBER’.
  • eventType - тип события. 0 - сообщение, 1001 - запрос на авторизацию, 1002 - файл. Других не нашёл
  • flags - разные битовые флаги. Первый бит говорит о том, что это первое сообщение в истории, второй бит - исходящее сообщение, третий бит - исходящее, пятый - юникодное.
В результате исследований написался вот такой скриптик (он даже чуть-чуть конфигурируется!):

import database
from datetime import datetime

DIRECTORY = 'd:\\history\\'
DIRECTORY_PROCESSED = 'd:\\history\\processed\\'
FORMAT_TIME = '%Y-%m-%d %H:%M:%S'

contacts = [9301095]

for contact in contacts:
    event = database.EventFindFirst(contact)
    if not event:
        continue
    f = open('%s%d.txt' % (DIRECTORY, contact), 'w')
    f_p = open('%s%d.txt' % (DIRECTORY_PROCESSED, contact), 'w')
    event_obj = database.EventGet(event)
    f_p.write('Module: %s\n\n' % event_obj.module)
    while event:
        event_obj = database.EventGet(event)
        f.write(str(event_obj.__dict__))
        f.write('\n')
        event_time = datetime.fromtimestamp(event_obj.timestamp).strftime(FORMAT_TIME)
        is_utf8 = event_obj.flags & 16
        direction = 'IN' if event_obj.flags & 4 else 'OUT'
        # first_message = event_obj.flags & 1

        try:
            event_body = event_obj.blob.decode('utf8' if is_utf8 else 'cp1251')
            processed = u'%s %s\n%s\n\n' % (direction, event_time, event_body)
            f_p.write(processed.encode('utf8'))
        except:
            f_p.write('%s %s\n### ERROR ###\n%s\n\n' % (direction, event_time, event_obj.blob))
        event = database.EventFindNext(event)
    f.close()
    f_p.close()

Список контактов надо положить в contacts.

P.S.На всю мою историю нашлось три вхождения ### ERROR ###. Не знаю в чём причина, может вуглускр покоцал три сообщения :)
P.P.S.Версия миранды - 0.7.1.

Guards в Python

В момент чтения книги по Эрлангу мне пришла в голову мысль - а почему бы не сделать guard’ы в Питоне? Инструмент, который можно для этого приспособить, существует - это декораторы.

Недолго думая, я написал первый вариант, потом ещё раза три его переделал, и вот что получилось в итоге:

def _main_guard(main_func):
    def inner(*args, **kwargs):
        for guard, func in inner.guards:
            if guard(*args, **kwargs):
                return func(*args, **kwargs)
        return main_func(*args, **kwargs)

    inner.guards = []
    return inner

def guard(guard_clause=None):
    if not guard_clause:
        return _main_guard

    def decor(func):
        main_func = func.func_globals[func.__name__]
        main_func.guards.append((guard_clause, func))
        return main_func
    return decor

Вот пример использования:

>>> @guard()
... def x0(x, y, z=5):
...     print "Standard: x = %d, y = %d, z = %d" % (x, y, z)
...
>>> @guard(lambda x, y, z=0: x == 1)
... def x0(x, y, z=87):
...     print "First: x = %d, y = %d, z = %d" % (x, y, z)
...
>>> @guard(lambda x, y: x == 2 and y == 4)
... def x0(x, y, z=7):
...     print "Second: x = %d, y = %d, z = %d" % (x, y, z)
...
>>> x0(0, 1)
Standard: x = 0, y = 1, z = 5
>>> x0(1, y=2)
First: x = 1, y = 2, z = 87
>>> x0(x=2, y=4)
Second: x = 2, y = 4, z = 7
>>> x0(2, 21)
Standard: x = 2, y = 21, z = 5

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

Сейчас в планах сделать так, чтоб можно было писать вот так:

@guard(lambda: x == 2 and y == 4)
def x0(x, y, z=7):
    print "First: x = %d, y = %d, z = %d" % (x, y, z)

а guard сам будет подставлять аргументы в проверочную лямбду соответственно тому, как они написаны в самой функции - так будет лучше выглядеть, меньше ошибок и вообще хардкорнее. Эксперименты показали, что такое однозначно возможно и, в принципе, не так уж и сложно сделать. К сожалению, без слова lambda сделать ну никак не получится - всё-таки это Python, а не Lisp :-)

Интересно, насколько это медленнее, чем просто очередь условий и вызовов функций. Может руки дойдут, потестирую производительность.

Какие есть замечания, предложения?

Забытые возможности Python 2

Как я и обещал, пишу продолжение.

Где используется else?


   if test1:
       statement1
   elif test2:
       statement2
   else:
       statement3
   


   try:
       statement1
   except:
       statement2
   else:
       statement3
   finally:
       statement4
   

Всё? Казалось бы - а что ещё может быть? Оказывается, else ещё можно использовать в циклах. Действует он после того, как закончился собственно цикл:


   >>> for a in xrange(3):
   ...     print a
   ... else:
   ...     print 'else'
   ...
   0
   1
   2
   else
   


   >>> a = 0
   >>> while a < 3:
   ...     print a
   ...     a += 1
   ... else:
   ...     print 'else', a
   ...
   0
   1
   2
   else 3
   


   >>> a = 0
   >>> while a > 3:
   ...     print a
   ... else:
   ...     print 'else'
   ...
   else
   

В чём же, в таком случае, состоит его полезность? Некоторая полезность состоит в том, что если в цикле сработал break, то else не выполняется:


   >>> for a in xrange(3):
   ...     print a
   ...     if a == 1:
   ...         print 'break'
   ...         break
   ... else:
   ...     print 'else'
   ...
   0
   1
   break
   

В принципе, иногда такое нужно, но редко :)

Ну и последнее - строки считаются одной строкой, если между ними только пробельные символы:


   >>> def f(s):
   ...     print s
   ...
   >>> f("first " # first
   ... "second " # second
   ... 'third ' # third
   ... '''fourth
   ...  fifth''') # end
   first second third fourth
    fifth
   >>> "first " 'second'
   'first second'
   

Сделано это для того, чтобы уменьшить количество обратных слэшей в строках - комбинировать можно raw-string и обычные, а можно юникодные и ASCII-строки - результат юникодный, если хотя бы один кусочек юникодный:


   >>> r'begin n ' """end"""
   'begin \n end'
   >>> u'unicode ' 'non-unicode'
   u'unicode non-unicode'
   >>> 'non-unicode ' u'unicode'
   u'non-unicode unicode'
   

Ещё одна приятная особенность состоит в том, что складываются они в момент компиляции, а не выполнения.

Интроспекция в Python

При создании своей history к Django возник вопрос: “А как же получать юзера при сохранении очередной ревизии?”. Первым делом это было решено при помощи грязных хаков - с помощью функции-хелпера во view к объекту формы добавлялись юзер, айпишник, а потом в форме руками приходилось передавать дополнительные параметры в save(), которые хватаются декоратором, ну а он уже выполняет всю работу. Очень некрасиво, много лишних телодвижений, и можно что-нибудь забыть. Хотелось бы переделать это дело на сигналы… Но в сигналах нет информации о том, кто же это выполняет действие - а значит, надо каким-то образом их получить. Интроспекция - вот выход из ситуации!

Внимательный осмотр бранча full-history (который работает на сигналах) из репозитория Django ни к чему не привёл - у них хоть и есть user = models.ForeignKey(User, default="1"), но он нигде не используется и не заполняется. Потом был осмотрен проект django-modelhistory, который фактически является улучшенным и дополненным бранчем full-history, и там был найден код, который, используя интроспекцию, возвращал юзера. Выглядит этот код как Smart Fortwo, попросту говоря - ужасно. Вот он:

def snoop_the_call_chain():
    """
    Currently, a hackish (and a bit naive) way of walking up the call chain
    to determine the user (if any) that initiated the change in the system.
    """

    from django.contrib.auth.models         import User

    cur = inspect.currentframe()
    desiredFrame = None
    desiredFrameCount = 0
    f = None

    try:
        ancestors = inspect.getouterframes(cur)
        count = 0
        for frame in ancestors:
            if frame[3] == "save":
                desiredFrameCount = count
            count = count+1

        # Ensure that we have a callee for the save methods
        # which we're auditing.
        if debug_mode: print "Desired Frame: (%d)\n" % (desiredFrameCount+1)
        if desiredFrameCount >= len(ancestors):
            if debug_mode: print "No callee in call chain for save method. Bailing ..."
            return None

        desiredFrame = ancestors[desiredFrameCount+1][0]
        if inspect.isframe(desiredFrame):
            for name,value in inspect.getmembers(desiredFrame):
                if name == "f_locals":

                    dictionary = dict(value)
                    count = 1
                    if debug_mode: print "=========== Begin Frame Objects ==============="
                    for key,val in dictionary.items():
                        if debug_mode: print "   (%d) %s: %s" % (count,key,val)
                        count = count + 1
                    if debug_mode: print "============ End Frame Objects ================\n"

                    if 'request' in dictionary.keys() and\
                           dictionary['request'].user:

                        if debug_mode: print "*** Found Calling User ***"
                        return dictionary['request'].user

    finally:
        del cur

Гммм… Не самый очевидный код, плюс в нём есть несколько багов. При помощи мануала по Python и дополнительного мозга код был понят. А потом написан заново, адекватный и рабочий:

def obtain_request():
    """
    True hackish way of walking up the call chain to get the HttpRequest object
    that initiated the change in the system.
    """
    ancestors = inspect.getouterframes(inspect.currentframe())
    for frame_record in ancestors:
        frame = frame_record[0]
        if 'request' in frame.f_locals:
            req = frame.f_locals['request']
            if isinstance(req, HttpRequest):
                return req

Работает отлично. Какие есть замечания, предложения, пожелания, может вопросы?

Забытые возможности Python

У всех на слуху и часто используются такие возможности Python, как генераторы, декораторы, list comprehensions, оператор with… Но есть в Питоне и некоторые возможности, которые достаточно мало кому известны.

Например, то что в list comprehensions можно использовать “вложенные” циклы:


   >>> [a + b for a in 'abc' for b in 'de']
   ['ad', 'ae', 'bd', 'be', 'cd', 'ce']
   

Или вот другая возможность - сравнения цепочкой Питон трактует в математическом смысле, а не в программистском. То есть, x < y <= z интерпретируется как x < y and y <= z, не учитывая того, что в первом варианте y вычисляется единожды, а во втором - дважды. Примеры использования:


   >>> 'a' in 'ab' in 'abc'
   True
   >>> 5 in [5] in [[5]]
   True
   >>> 'a' in 'abcd' in ['abcd', 'adf']
   True
   >>> a = 5
   >>> a == 5 != 234
   True
   

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


   >>> 5 < 20 in (20,30)
   True
   >>> 5 > 1 < 5
   True
   >>> 5 is not None < 20
   True
   >>> 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8
   True
   

P.S.Есть ещё пару вещей, про них я чуть позже расскажу.