?

Log in

No account? Create an account
Записки теоретика
Использование Python в Delphi. 
6th-Jul-2010 01:01 pm
Хочу черкнуть пару строк про использование интерпретатора Python в Delphi.


Вступление



Delphi - язык, мягко говоря, не мейнстримовый, поэтому всевозможные наработки и API часто обходят его стороной. Python имеет API для C, на основе которого были сделаны добрыми людьми нужные компоненты. Я очень вскользь пробегусь по вещам, которые задокументированы и более подробно коснусь некоторых проблем, с которыми можно столкнуться, но они не описаны. Заранее прошу прощения за то, что мало времеди уделил оформлению очерка.

Итак.

Почему именно Python?



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

Почему же именно Python? В процессе поиска скриптовых расширения я пробовал различные варианты, остановлюсь на некоторых.

- Использование ActiveScript control. Встроен в Windows, работает с COM-объектами, значит, вам надо их программировать. Это процесс сам по себе не простой. У меня не получилось заставить этот компонент работать как полагается.

- Fast Script control (http://www.fast-report.com/ru/products/fast-script.html) Позволяет писать скрипты на четырех языках, в том числе Pascal, что дает возможность написанные скрипты потом встраивать в программу в виде компилируемого кода. Из delphi-специфичных компонентов понравился мне больше всего. Платный.

- Язык Lua. (www.lua.org) Довольно простой язык, заточеный специально под скриптование нативного кода. Библиотека интерпретатора под windows весит всего 54 kb. Попробовал под Delphi, работает хорошо, но конкуренции с Python не выдерживает.

- И, наконец, Python. О самом языке писать не буду, вы все сами наверняка знаете. Позводяет делать практически настоящую объектность, кроме того, под него имеется куча наработок.

О компонентах.



Скачиваем тут: http://www.atug.com/andypatterns/pythonDelphiTalk.htm .
Большое предупреждение: работа над компонентами давно прекращена , поэтому установку на новые версии и некоторые плюшечки придется делать самому.

Для Delphi 2007 и ранее ставится без проблем, для 2010 придется сделать небольшие правки в связи с переходом на Unicode. Я их не делал, так что готовый рецепт не предоставлю.

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

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

01-07 - совсем элементарные примеры по подключению и вызовам.
08 - базовая работа с объектом
09 - вызов из dll-ки
10 - работа с БД
11 - сортировка
12 - Atom, работа через ole и позднее связывание
13 - БД
14 - БД
15 - БД
16 - работа со словарем
17 - variant array
18 - c++ builder
19 - c++ builder
20 - c++ builder
21 - объект, с событиями
22 - threading
23 - то же
24 - variants в параметрах, извлечение объектов
25 - тесты
26 - объекты, наследование
27 - тривиальный sequence
28 - итератор
29 - работа с растровой картинкой
30 - позднее связывание
31 - работа с готовыми обертками VCL
32 - видимо, обертка над объектом с помощью RTTI, не работает.


Кратко о Python



Как вы уже наверняка знаете, в Python все является объектом. На практике это значит, что любая переменная Python может поддерживать (а может и не поддерживать) какие-то методы общего предка всех питоновских объектов, поддерживаемые на уровне языка.

Основа связки Delphi-Python



Релизация вышеуказанных методов в ваших дельфийских объектах, наследниках TPyObject, и будет основой для ваших переменных, видимых одновременно из Delphi и Python. Пример элементарного объекта описан в примере 8. Далее я буду исходить из предположения, что вы с ним ознакомились.

Весь API Python'а работает с указателем на PyObject, в Delphi он называется PPyObject.

Вызов delphi-функций.



Python API работает с С-функциями определенного вида, (какого именно - можно посмотреть в примерах) поэтому не забывайте указывать в конце cdecl. На выходе функция должна обязательно возвращать PPyObject, иначе будет Access violation. Если логически функция ничего возвращать не должна, верните TPythonEngine.ReturnNone.


Как получить из PPyObject - TPyObject и наоборот?


Не надо использовать "@" и "^"!

Для первого - использовать функцию PythonToDelphi.

var
	myobj: TmyPyObject; // наследние TPyObject;
	p: PpyObject;
... begin ...
	myobj := TmyPyObject(PythonToDelphi(p));
...

Для второго - использовать метод TPyObject.GetSelf.

О python-овских ссылках на объекты.



Если вы просто возвращаете в python объект в виде указателя, то вполне возможна ситуация, когда этот объект изчезнет, а вместо него потом автоматически будет создан новый, но уже с другими данными. Например, для такого вызова (python):

myobj = mymodule.GetMainStorage().GetObject

после этого вызова то, что было возвращено GetMainStorage() - изчезнет, а вместо myobj будет создан новый объект. Чтобы не было такой путаницы, используйте повышение счетчика питоновских ссылок:
function GetMainStorage( self, args : PPyObject ) : PPyObject; cdecl;
var
    p: PPyObject;
begin
    with GetPythonEngine do
    begin
        p := FFrame.pytDocument.CreateInstance; // FFrame.pytDocument = TpythonType
        FMainStorage := TPyDoc(PythonToDelphi(p));
        if Assigned(FdelphiMainStorage) then
            FMainStorage.FObject := FdelphiMainStorage;
        Result := p;
        Py_INCREF(p);
    end;
end;

Здесь не так существенно что, что ссылка на объект FMainStorage живет в Delphi долго, а то, что на него делается ссылка в python, вызовом Py_INCREF(p).

function TMainStorage.GetAttr(key: PChar): PPyObject;
var
    p: PPyObject;
    pyobj: TPySomeObject;
    pycube: TPyCube;
//    pyhist: TPyHistoryStorage;
    ck: Integer;
begin
    with GetPythonEngine do
    begin
        if UpperCase(key) = 'GETOBJECT' then
        begin
            p := FFrame.pytSomeObject.CreateInstance; // FFrame.pytSomeObject = TpythonType
            pyobj := TPySomeObject( PythonToDelphi(p) );
				// какие-то другие присвоения
		      Result := p;
            Py_INCREF(p);
			end;
	...


Утечки памяти



После работы скрипта все объекты типа TPyObject уничтожаюся сами. Если объект представлял собой обертку для вашего рабочего объекта и держал на него ссылку, уничтожение ссылки коректно не делается и вы получите утечки памяти. Я достаточно долго ломал голову, как разрулить этот вопрос, в итоге получил рецепт: сделать у каждого такого TPyObject метод очистки ссылок, вызвать его вручную после работы скрипта, причем - это важно - этот метод должен быть не в деструкторе! Может, есть способ элегантнее, но я его не знаю.

Могу ли я использовать библиотеку NumPy?



Да, можете. Numpy - питоновская библиотека для быстрой работы с числовыми массивами. Эти массивы находятся в памяти подряд, как обычные массивы нативного кода. Python API предоставляет функции для работы с такими участками памяти, но в наших компонентах их выховы отсутствуют, поэтому придется добавить это самостоятельно.

Идем в юнит PythonEngine, в класс TPythonInterface добавляем методы:

    PyBuffer_FromMemory:  function ( prt: Pointer; Size: Py_ssize_t): PPyObject; cdecl;
    PyBuffer_FromReadWriteMemory: function ( prt: Pointer; Size: Py_ssize_t): PPyObject; cdecl;
    PyBuffer_New:         function ( Size: Py_ssize_t): PPyObject; cdecl;


(PyBuffer_FromMemory выдает участок памяти только для чтения, для возможности записи используйте PyBuffer_FromReadWriteMemory.)

Далее, идем в процедуру TPythonInterface.MapDll и в конец добавляем:

    @PyBuffer_FromMemory      := Import('PyBuffer_FromMemory');
    @PyBuffer_FromReadWriteMemory := Import('PyBuffer_FromReadWriteMemory');
    @PyBuffer_New := Import('PyBuffer_New');


Готово, теперь можно пользоваться.

Передаем массив из Delphi так:

function GetFloatArray( self, args : PPyObject ) : PPyObject; cdecl;
begin
	result := GetPythonEngine.PyBuffer_FromMemory(@(MyTestArray[0]), 100 * SizeOf(single));
end;

где 100 - это размер вашего массива, single - его тип.

На стороне Python с использованием NumPy принимаем так:

import numpy
v1 = mymodule.GetFloatArray()
#print v1
dt = numpy.dtype(numpy.float32)
na = numpy.frombuffer(v1, dt, 100)
print na

nympy.float64 используется для double, float32 для single, int32 для integer.

Как работать с датой и временем



Я опишу работу со стандартным типом datetime из библиотеки datetime.

Из Python в Delphi:

При распарсивании пришедшего datetime с помощью функции PyArg_ParseTuple в строке Format используйте буковку O, заглавную:

function TPyMyObject.GetSomeData(args: PPyObject): PPyObject;
var
    ck: Integer;
    source: PChar;
    pdt: PPyObject;
...
begin
...
	tmp := PyArg_ParseTuple( args, 'isO:MyObject.GetSomeData', [@ck, @source, @pdt] );
...

Затем преобразуйте pdt в дельфийский TdateTime примерно так:

...
var
    pdt: PPyDateTime_DateTime;
    pd: PPyDateTime_Date;
...

    with GetPythonEngine do
    begin
        if PyDateTime_Check(p) then
        begin
            pdt := PPyDateTime_DateTime(p);
            result := EncodeDate(pdt^.data[0]*256+ pdt^.data[1], pdt^.data[2], pdt^.data[3]);
        end
        else if PyDate_Check(p) then
        begin
           // здесь - писать преобразование, если нужна только дата без времени
        end
	 end;

Данный код помещает в DateTime только время без даты, потому что это черновик. В общем, возможны вариации на ваше усмотрение.
Распределение байт в структуре можно посмотреть в юните PythonEngine, в комментарии, начинающимся словами "Fields are packed into successive bytes".

Из Delphi в Python

... это делается значительно проще. Прежде всего, в вашем компоненте TpythonEngine переключите свойство DatetimeConversionMode на dcmToDatetime.
Затем используйте функцию TpythonEngine.VariantToPyObject, с параметром TDateTime, она сама разберется.

Элементарный итератор



Цикл for в Python в общем случае работает так: сначала получаем у объекта итератор, а уж он работает на итерации. Пример итератора есть в демках, в примере 28, но в общем случае усложнять программу дополнительным объектом не стоит и можно совместить, примерно так:
    TPyList = class(TPyObject)
    private
        FCursor: Integer;
        FDelphiList: TObjectList;
...

function TPyList.Iter: PPyObject;
begin
    FCursor := 0;
    result := Self.GetSelf;
    GetPythonEngine.Py_INCREF(Result);
end;

function TPyList.IterNext: PPyObject;
var
    s: string;
begin
    with GetPythonEngine do
    begin
        if FCursor < FDelphiList.Count then
            result := PyList_GetItem(FPyList, FCursor);
        else
            result := nil; // не ReturnNone!
    end;
    inc(FCursor);
end;

Когда кончается итерация - это, наверно, единственный случай, когда в функции не надо возвразать PPyObject. Обращаю ваше внимание: для окончания итерации надо просто вернуть nil, создавать python exception StopIteration вам не нужно.

Вызов по индексу


Для вызова из объекта какого-то значения по произвольному индексу используйте функцию (переопределение, разумеется)
function  MpSubscript( obj : PPyObject) : PPyObject;

Для присваивания по индексу -
function  MpAssSubscript( obj1, obj2 : PPyObject) : Integer;

Вернуть надо 0 в случае успеха, иное число в случае неудачи. В случае неудачи надо также сделать питоновский exception, но я до этого вопроса еще не дошел.

***


На этом все, спасибо за внимание.
Если этот текст оказался вам интересен, прошу не полениться и написать коммент. Так я буду знать, что, в случае, если найдутся еще какие-то достойные внимания приемы, то их неплохо бы выложить публично.
Comments 
4th-Oct-2010 01:36 pm (UTC) - укажите пожалуйста источники
В тексте присутствуют ссылки на какие-то примеры (8, 28). Наверно фрагменты текста брались откуда-то. Откуда?
4th-Oct-2010 04:11 pm (UTC) - Re: укажите пожалуйста источники
к компонентам прилагаются примеры, 30 штук.
по умолчанию все ставится в c:\program files\PythonForDelphi, там каталог Demos.
2nd-May-2011 12:44 pm (UTC)
фантастика! спасибо за полезную информацию, ваш пост на 2 месте в гугле по запросу "delphi python", сразу в след за ссылкой на компонент :)

есть желание подключить python к одной программе написанной на delphi, пока прощупываю брод :)
2nd-May-2011 01:45 pm (UTC) - спасибо
Кстати, работа над компонентами, как оказалось, продолжена. http://code.google.com/p/python4delphi/

Там компоненты с поддержкой и 3-го питона, и юникодного delphi. Сам не пробовал.
19th-May-2011 09:22 am (UTC) - Re: спасибо
не подскажете как решить проблему? использую delphi 2007. скомпилил Python_2010.dpk, установил в делфю компонент, установил питон 3.2.1rc1. программа компилируется, получилось выполнить несколько уроков из туториала. поставил на форму 2 TMemo. первый для инструкций, второй для вывода. но вот попробовал выполнить инструкцию print('ф') и на ней питон кидает исключение, что utf8 codec cant decode 0x0ff. Как побороть проблему с русским языком там?
19th-May-2011 12:07 pm (UTC) - Re: спасибо
Ну для начала - а почему вы для 2007-го делфи взяли пакет для 2010-го?
Ожидаемо, получается ошибка с юникодом, потому что 2007 - до-юникодная версия, а в 2010 уже юникод.
19th-May-2011 10:35 pm (UTC)
я его взял потому что не увидел пакета для 2007, да и взял как самый свежий, Python_d7.bpl - похоже на delphi7. но я его тоже вот попробовал, проблема та же осталась. вернулся на Python_2010.bpl. заметил вот такую штуку:
скомпилил программу(обычный первый урок из туториала)
поставил питон 3.2
запускаю программу - обычные инструкции работают, но сабжевая ошибка
снес 3.2, поставил 2.7
запускаю программу(без перекомпиляции)
запускаю программу - работае и print 'ф'

вот как ошибка полностью выглядит для print('ф')
File "", line 1
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xf4 in position 0: unexpected end of data
20th-May-2011 04:33 am (UTC)
Ну если нет пакета для 2007, надо было более ранний взять.
Насчет третьего питона - я его бегло посмотрел (попытался прикрутить), увидел, насколько сильно там переработан API по сравнению со вторым, ужаснулся и отказался от этой затеи :)
Тем более что особой нужды в третьем не было. Так что тут помочь не смогу.

В частности, в третьем питоне тоже тотальный переход на юникод. Так что для 2007-го настоятельно советую брать второй. Или переходить на юникодный дельфи.
22nd-May-2011 02:20 pm (UTC)
11й пример(Using Threads inside Python), - описыват 2 варианта
1. запуск 3 сриптов в одном интерпретаторе
2. запуск трех интерпретаторов

запускаю тестовое приложение из примера.
первая кнопка "one interpreter" - работает
вторая кнопка "three interpreters" - крешит программу

вторая крешится в TPythonThread.Execute на строчке
PyEval_AcquireLock
не понимаю почему так.

как я понял обе кнопки судя по вызовам PyEval_AcquireLock и PyEval_AcquireThread(fThreadState) работают в рамках GIL.

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

PythonForDelphi не поддерживает сейчас запуска нескольких TPythonEngine(наверно потому что их неможет быть несколько в одном потоке), придется менять код в PythonEngine.pas что бы убрать их проверки на несколько инстансов TPythonEngine

посоветуете что-то по этому поводу? почему может крешится PyEval_AcquireLock? на сколько больше оверхед в третем варианте? может стоит держатся второго варианта?

на сколько я понял GIL является проблемой в большей степени если использовать блокирующие вызовы, такие как чтение файла например, и вероятно может быть помехой если "параллельно" запустить 20-30 скриптов
23rd-May-2011 02:47 pm (UTC)
Тут не знаю, многопоточностью в питоне вообще не баловался.
25th-May-2011 08:47 am (UTC)
не подскажите, как с помощью AddDelphiMethod добавить функций в __builtins__ модуль? нашол как его достать - BuiltinModule() ф-ия, но она возвращает тип Variant
25th-May-2011 09:21 am (UTC)
хе, я даже и не заметил что там такой метод есть, всегда пользовал просто AddMethod, принимающий сишные функции.

А в чем проблема? Я просто тупо скопировал то что было в демках. Получилось примерно следующее:
procedure TScriptFrame.pymModelInitialization(Sender: TObject);
begin
    ;
    with Sender as TPythonModule do
    begin
        // model
        AddMethod( 'GetCubeValue', @GetCubeValue, 'GetCubeValue(ck,i,j,k)'+LF+'ck is CubeKind;'+LF+'i,j,k is cell indices, 1-based' );
        AddMethod( 'SetCubeValue', @SetCubeValue, 'SetCubeValue(ck,i,j,k, value)'+LF+'ck is CubeKind;'+LF+'i,j,k is cell indices, 1-based' );
.... ну и таки далее.

Заменяете с-function на TDelphiMethod и вперед.
В восьмом примере есть похожее, см. class procedure TPyPoint.RegisterMethods( PythonType : TPythonType ).
25th-May-2011 10:12 am (UTC)
нашол еще одну функцию как из Variant`а достать ссылку PPyObject - ExtractPythonObjectFrom

procedure TForm1.test();
var pm : PPyObject;
begin
pm := ExtractPythonObjectFrom(BuiltinModule());

with pm as TPythonModule do
begin
addDelphiMethod('test', test2, '');
end;
end;

вот такой код у меня не работает, подскажите пожалуйста, что я не так делаю
и надо ли тут чтото делать со счетчиком Py_INCREF
25th-May-2011 10:28 am (UTC)
По-моему, вы идете каким-то не тем путём.

1. Что вообще за функция BuiltinModule? Я ее не нашел в PythonEngine.pas и сопутствующих модулях. Вас не устраивает получение TPythonModule обычным путём?

2. "Код не работает" - это, знаете ли, ОЧЕНЬ неопределенно :)

3. Нет, со счетчиком ссылок тут ничего делать не надо. Это может быть нужно, когда вы получаете объекты, передаете их на сторону Питона.
25th-May-2011 11:00 am (UTC)
1. эта функция лежит в VarPyth.pas
меня устроит обьект типа TPythonModule, но как его достать? ищу именно инициализированный встроенный __builtins__ модуль, потому что хочу добавить функции так, что бы их можно было везде вызывать ничего не импортируя, создать свой модуль не проблема, ищу как расширить встроенный модуль __builtins__

2. хех, точно :) сейчас просто нет делфи под рукой, вечером покажу что говорит компилятор на такой код
25th-May-2011 12:00 pm (UTC)
Я вас понял.
Думаю, это не самое лучшее решение.

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

Чтобы пользователю не приходилось импортировать самостоятельно, можно где-нибудь при инициализации приложения сделать
FPythonEngine.ExecString('import mymodule');      

и все будет пучком.

Я поступил именно так, и, думаю, правильно.
26th-Aug-2011 03:24 am (UTC)
Добрый день.
Увидел явно ваш комментарий на Хабре тут http://habrahabr.ru/blogs/python/111306/ .
У вас есть хорошая возможность отблагодарить меня за помощь - дать мне инвайт на Хабр :)
28th-Aug-2011 01:12 pm (UTC)
добрый, я б с удовольствием, но у меня нету кармы для этого :\
28th-Aug-2011 05:15 pm (UTC)
печально. ну ладно.
This page was loaded Sep 24th 2017, 1:42 pm GMT.