воскресенье, 18 марта 2012 г.

Мемоизация в Python: пробуем на зуб.

Как-то мне посчастливилось натолкнуться на хабрапост, посвященный мемоизации. На тот момент это слово было мне ещё не знакомым и я даже слегка растерялся.
Однако уже после первых строк я сразу понял, что скрывается под этим мудрёным словом - это же старое, доброе, классическое ("тёплое ламповое" ;-) ) табулирование функций, но подведённое под паттерн проектирования и с более удобным (в контексте языка Python) интерфейсом.

Почитав ещё несколько постов на эту же тему я увидел в представленных реализациях "фатальный недостаток" тут же кинулся переписывать самостоятельно, на примере расчёта факториала:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from functools import wraps
import cPickle
import math
import time

mem = {}

def save():
    f = open('memoization.dat', 'wb')
    cPickle.dump(mem, f)
    f.close()

def load():
    global mem
    try:
        f = open("memoization.dat", 'rb')
        mem = cPickle.load(f)
        f.close()
    except:
        pass

def make_key(name, *args, **kw):
    # создание строкового ключа на основе имени функции и её входных параметров
    key = name + '|'
    key += "|".join([str(p) for p in args]) + "|"
    key += "|".join(["%s:%s" % (str(k),str(kw[k])) for k in sorted(kw.keys())])
    return key

def memoization(func):
    # специальный декоратор для кеширования
    @wraps(func)
    def wrap(*args, **kwargs):
        # формируем ключ
        key = make_key(func.__name__, *args,**kwargs)
        # поиск в кеше
        if key not in mem:
            mem[key] = func(*args,**kwargs)
        return mem[key]

    return wrap

def tracetime(func):
    # специальный декоратор для оценки времени работы функций
    @wraps(func)
    def wrap(*args, **kwargs):
        t1 = time.time()
        res = func(*args, **kwargs)
        t2 = time.time()
        print func.__name__, '/ dt=', t2-t1
        return res

    return wrap

@memoization
def fact(x):
    # мемоизированный факториал
    return math.factorial(x)

# простой тест: сумма длин строковых представлений факториалов
# из заданного диапазона

@tracetime
def test1(a,b):    
    L = 0
    for x in range(a,b):
        L += len(str(fact(x)))

    return L

@tracetime
def test2(a,b):
    L = 0
    for x in range(a,b):
        L += len(str(math.factorial(x)))

    return L

a = 2000
b = 3000

# первый вызов - данных в словаре нет
print test1(a,b)
# обычный расчёт
print test2(a,b)
# при повторном вызове - мемоизация будет уже задействована
print test1(a,b)

Однако отличия от увиденного ранее - минимальны. Такова уж специфика задачи и особенности Python (да, досадно вышло - хочется изобрести свой велосипед, а даже код получается почти такой же, как у всех!). Отличие от прочих вариантов - словарь описан снаружи и пригоден к сериализации с дальнейшим сохранением в файл посредством pickle.
Описанный выше подход - красив и лаконичен, но слишком обобщён и идеален, годится лишь как демонстрация красоты Python и наглядности подхода.
Теперь добавим несколько смачных ложек дёгтя.

1) В приведенном примере для хранения значений функций используется простой словарь, который хранится в памяти. Его размер определяется ограничениями используемой операционной системы. То есть запуская скрипт с мемоизацией на разных конфигурациях железа и ОС мы не можем расчитывать, что нам всегда будет хватать памяти на одних и тех же объёмах вычислений.

2) Может так получиться, что расчёты будут производиться внутри параллельно выполняющихся процессов - в этом случае, приведенный метод потребует доработки: словарь mem придётся сделать разделяемой переменной, что может несколько усложнить приложение.

3) Области определения функций и действительно ли надо хранить так много значений?
Например, простые числа или числа Фибоначчи. Куда удобнее хранить их не в словаре, а в упорядоченном списке - и использовать двоичный поиск для проверки наличия значений. Из этого следует очевидный вывод, что способ хранения и доступа определяется особенностями функции.

Всё-таки, если бы вдруг встала острая необходимость мемоизации - действительно важная функция (или их некоторое множество) считалась долго, нужна была часто и вероятность повторного вызова с таким же набором входных параметров была бы заметно отлична от нуля - пожалуй я выбрал бы для этого какую-нибудь базу данных, MySQL или PostgreSQL - не суть важно. Храни сколько хочешь, доставай хоть из треда, хоть из процесса. А обращение также обернул бы в декоратор.

пятница, 16 декабря 2011 г.

Python для параллельных вычислений

XXI век давно уже наступил и параллельные вычисления перестали быть чем-то сложным и недостижимым. Многие платформы и языки имеют средства для реализации распараллеливания и замечательный язык Python не является исключением.
Python предоставляет разнообразные инструменты для реализации многозадачности: многопоточную, многопроцессную и т.н. сопрограммы. Не хотелось бы вдаваться в глубокую теорию и описывать отличия и особенности многопоточной и многопроцессной многозадачностей - всё это есть в книгах, которые без труда находятся в гугле или яндексе. Вкратце стоит лишь отметить, что для математических вычислений лучше всего подходит именно многопроцессная многозадачность, которая реализована в Python благодаря модулю multiprocessing.

Перейдём к практике. Допустим есть такая нехитрая задача:

Найти минимальное натуральное число с суммой цифр 80, которое делится на 1237.

(Это несколько упрощённый аналог задачи с замечательного сайта Диофант)

Наверняка, такую задачу можно решить аналитически, обладая хорошим математическим образованием. Но нас интересует, как решить её с помощью компьютера и Python.
Если применить простой последовательный перебор в одном цикле, решаться она будет долго. Имея многоядерный процессор такую задачу можно легко распараллелить и решать тоже перебором, но параллельно ;).
Поскольку предугадать на каком интервале находится искомое число трудно - поступим следующим образом: создадим функцию поиска, принимающую на входе интервал чисел и выдающую результат. Будем запускать параллельно несколько процессов с этой функцией с последовательными и относительно небольшими интервалами и проверять, найдено ли решение.

Итак, исходный код (использовался Python 2.7.2):

#!/usr/bin/env python/
# -*- coding: utf-8 -*-

import multiprocessing
import time

# очередь для сбора результатов
result_queue = multiprocessing.Queue()

# блокировка
output_lock = multiprocessing.Lock()

def sumdig(v):
    """ Возвращает сумму цифр числа v.
    """
    Sum = 0
    remain = v
    while remain > 10:
        Sum += remain % 10
        remain = remain // 10
    Sum += remain
    return Sum

def worker(num, n1,n2, qres):
    """
    Поиск числа с суммой цифр =80 в заданном диапазоне [n1, n2].
   
    num - номер воркера. Сугубо для того, чтобы отличать их друг от друга
    n1, n2 - диапазон значений
    qres - объект Queue, очередь для накопления результатов
    """
    t1 = time.time()
    x = n1
    res = 0
    while x <= n2:
        sm = sumdig(x)
        if sm == 80:
            res = x
            break
        x += 1237
    t2 = time.time()   
   
    # запросим блокировку для вывода на экран результатов работы воркера
    output_lock.acquire()
   
    print 'worker #%d Res=%d  time: %0.4f ' % (num, res, float(t2-t1)),
    print ' n1=%d, n2=%d' % (n1,n2)

    # отпустим блокировку
    output_lock.release()
   
    if res > 0:
        qres.put_nowait(res)


if __name__ == '__main__':

    # число воркеров принимаем равным числу ядер
    worker_count = multiprocessing.cpu_count()

    start_value = 0
    M = 100000
    while True:
        jobs = []
        for i in xrange(worker_count):
            p = multiprocessing.Process(target=worker,
                                        args=(i, start_value + 1237*M*i + 1237,
                                                 start_value + 1237*M*(i+1),
                                                 result_queue))
            jobs.append(p)
            p.start()

        # ожидаем завершения работы всех воркеров
        for w in jobs:
            w.join()

        # начальное значение для следующей итерации
        start_value = start_value + 1237*M*(worker_count)
        print "start_value=", start_value


        if result_queue.empty():
            print "next iteration..."
        else:
            print "--- THE END ---"
            break

    # из очереди выберем все ответы и найдём самый минимальный
    res = []
    while not result_queue.empty():
        res.append(result_queue.get())

    print 'RESULT=', min(res) 

Некоторые замечания по исходному коду:

1) Каким выбрать число воркеров - заранее предсказать сложно. При малом его количестве - задача будет решаться медленнее. При большом - можно здорово подгрузить свою систему, что повлечет замедление работы не только воркеров, но и прочих процессов - так, например, при тестировании этого примера на 6 и более воркерах (четырёхядерный процессор i3, Windows7 и было ещё запущено много всякого типа Aptana Studio, Bat! и 4 окна Firefox c кучей вкладок) я наблюдал такие эффекты - с заметными задержками открывались программы и перетаскивались окна. Диспетчер задач показывал почти 100% загрузку всех ядер процессора.
    Практика показала, что макс. число воркеров должно выбираться эмпирически, с учётом загрузки процессора и среднего времени работы воркеров (в нашем случае, интервалы поиска чисел не следует задавать слишком длинными) и относительно оптимальным значением явлется число равное числу ядер процессора.

2) Для нормального отображения хода работы, а именно чтобы сообщения не накладывались друг на друга, искажая смысл - нужно блокировать вывод на консоль. Пожалуй, тема блокировок стоит того, чтобы рассмотреть её отдельно.

3) В данном примере, из соображений упрощения, для хранения воркеров задействован обычный список (jobs). Хотя на самом деле в модуле multiprocessing есть реализация пула процессов, обладающая широким набором функций: multiprocessing.Pool().

Как видно из примера - многозадачность, основанная на механизме многопроцессности, не такая уж сложная и страшная штука, а Python вполне себе годен для решения математических задач путём распараллеливания.

среда, 23 ноября 2011 г.

Работаем с оракловыми курсорами в Python с помощью библиотеки cx_Oracle

Курсорная переменная может возвращаться в качестве результата хранимой процедуры, написанной на PL/SQL. Например, вот такой:

create or replace procedure test_cur(
  a number,
  b number,
  p_ret out sys_refcursor)
is
begin
  open p_ret for select a + level-1 v from dual
          connect by level <= b;
end;
Для работы с СУБД Oracle из Python есть весьма хорошая библиотека cx_Oracle . С курсорами работа построена там очень просто - они объявляются как обычные типизированные переменные библиотеки, в данном случае с типом курсора. Всё очень просто, смотрим на примере:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cx_Oracle as db

try:
    conn = db.connect('user', 'password', 'server')
except  db.DatabaseError, err:
    print 'DB Error: ', err
    exit()

cur = conn.cursor()

# так объявляется курсорная переменная
ret = cur.var(db.CURSOR)

# Не забываем, что есть dir(), с чьей помощью можно узнать очень
# много полезного об инстансе курсорной переменной
print 'ret: ', dir(ret)
print 'ret.getvalue(): ', dir(ret.getvalue())
   
cur.execute('''begin test_cur(1, 20, :ret); end; ''', ret=ret)

# описание полей запроса
print ret.getvalue().description

# А вот по этому можно уже пройтись через for ;-)
print ret.getvalue().fetchall()

# не забываем закрывать за собой соединение с Ораклом
conn.close()
Кстати, вот тут лежит неплохой мануал по cx_Oracle на русском языке.

среда, 13 апреля 2011 г.

Ещё один способ "заточки пилы"

Не все из нас работают в стартапах или научных проектах, в творческих коллективах и по SCRUM-методикам. У многих программистов профессиональная деятельность связана с довольно скучными вещами типа сопровождения отчётности и всякой бухгалтерии-логистики.
В этом нет ничего противоестественного или фатального, но иногда могут появляться ощущения однообразия и затягивания в "профессиональную яму".

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

А открыл я для себя Diofant.ru

Это совместный проект компаний "Интернет-Университет Информационных Технологий (INTUIT.ru)" и издательства "Открытые Системы".
Чтобы решать задачи, регистрироваться не обязательно. Но чтобы узнать правильность ответа и инкрементировать пузомерку рейтинг - зарегистрироваться нужно.

Теперь о том, чем он полезен. Я увидел в нём следующие фишки, которые мне очень понравились.

А. Логические задачки, заставляющие мозг скрипеть, а руки - терзать карандаш и переводить бумагу. Много задач. На логику и математику. Регулярное их решение продлевает жизнь и нефигово "затачивает пилу" логического мышления.

Б. Информатика. Более трех сотен программистских задач. На арифметику, на умение составлять алгоритмы или реализовывать их на языке программирования. Много задач взято с проекта "Эйлер". Что касается выбора средств - тут полная свобода (лично я пользуюсь Python). Нужен только ответ.
Часть задачек решается тупо перебором. Но для некоторых перебор - слишком дорогое удовольствие: задача будет считаться очень, очень долго. Вот тут приходится включать мозг, оптимизировать алгоритм, внимательнее читать условия задачи, применять хитрости, присущие языку, на котором эта задача решается.
Собственно, ради этого всё и затевается. Хочешь - жди неделями, когда найдётся ответ, а хочешь - задумайся, пересмотри алгоритм и может так получиться, что ждать придется не больше минуты. Если есть математическое образование - то да, ждать вообще не придётся ;-)
Ещё я заметил, что с помощью "Диофанта" я воспитываю в себе очень хорошую привычку. А именно, получая некий расчётный ответ в какой-либо задачке, я должен быть уверен на 100% в его правильности. Диофант ошибки не прощает, что сказывается на рейтинге. Поэтому приходится внимательно читать условия и проводить дополнительные проверки.

Резюме такое. Ресурс очень полезный. В качестве источника задач при изучении нового языка программирования и просто для разминки мозгов.

Отчёты отчётами, но "пила" всегда должна быть острой!