C# + IronPython: вызов методов (original) (raw)
IronPython – реализация python на платформе .Net обычно имеено тем, что это именно .Net реализация на основе DLR. Это дает возможность простого сочетания производительности и строгости статических языков .Net (c#, например) и гибкости python. В текущем проекте потребовалась как раз такая связка для обеспечения производительности и гибкости одного из компонентов сервисной архитектуры.
В решаемой задаче необходимо было в коде C# использовать объекты python, находящиеся в динамически подгружаемых скриптах. Для решения задачи использованы .Net 3.5 (C# 3.0), DLR 0.91, IronPython 2.6 RC1. На прямую работать с интерпретатором python и dlr классами не очень удобно, т.к. надо постоянно тащить за собой такие объекты как ScriptEngine, ScriptScope. Так как способы динамической типизации появятся только в версии C# 4.0, то на данный момент приходится пользоваться небольшими и простыми обертками.
Пусть имеется скрипт:
class PythonCalc ( object ) :
def add ( self, val1, val2 ) :
return val1 + val2
def minus ( self, val1, val2 ) :
return val1 - val2
Для того чтобы начать использовать экземпляр класса PythonCalc необходимо создать экземпляр интерпретатора IronPython:
// вычитываем конфигурацию из app.config
var scriptLang = ScriptRuntimeSetup.ReadConfiguration();
var scriptRuntime = new ScriptRuntime(scriptLang);
var scriptEngine = scriptRuntime.GetEngine("IronPython");
Для успешного исполнения данного кода app.config должен иметь примерно следующий вид:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="microsoft.scripting"
type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting"
requirePermission="false" />
</configSections>
<microsoft.scripting>
<languages>
<language names="IronPython;Python;py" extensions=".py"
displayName="IronPython 2.6"
type="IronPython.Runtime.PythonContext, IronPython" />
</languages>
</microsoft.scripting>
</configuration>
Далее создаем класс обертку. Из-за особенностей динамической типизации методы add и minus могут принимать в качестве фактических параметров любые объекты. В шарпе можно добится того же, однако придется либо постоянно указывать тип сигнатуры методов, либо пользоваться приведением. Как один из вариантов решения проблемы дополнить реализацию ограничением идентичности типа первого и второго операнда:
public class PythonCalc<T>
{
private readonly object pythonCalc;
public PythonCalc(ScriptEngine scriptEngine)
{
var operations = scriptEngine.CreateOperations();
var script = Encoding.GetEncoding(1251).GetString(
Assembly.GetCallingAssembly().GetManifestResourceStream("TestDLRApplication.script.py").ReadToEnd());
var scriptSource = scriptEngine.CreateScriptSourceFromString(script, SourceCodeKind.Statements);
var scriptCode = scriptSource.Compile();
scriptCode.Execute();
var scriptScope = scriptCode.DefaultScope;
pythonCalc = scriptScope.GetVariable<Func<object>>("PythonCalc")();
Add = operations.GetMember<Func<T, T, T>>(pythonCalc, "add");
Minus = operations.GetMember<Func<T, T, T>>(pythonCalc, "minus");
}
public Func<T, T, T> Add { get; private set; }
public Func<T, T, T> Minus { get; private set; }
}
Можно немного расширить гибкость данной обертки по средством замены делегатов на методы с обобщенной типизацией:
public class PythonCalc1
{
private object pythonCalc;
private Func<object, object, object> add;
private Func<object, object, object> minus;
public PythonCalc1(ScriptEngine scriptEngine)
{
var operations = scriptEngine.CreateOperations();
var script = Encoding.GetEncoding(1251).GetString(
Assembly.GetCallingAssembly().GetManifestResourceStream("TestDLRApplication.script.py").ReadToEnd());
var scriptSource = scriptEngine.CreateScriptSourceFromString(script, SourceCodeKind.Statements);
var scriptCode = scriptSource.Compile();
scriptCode.Execute();
var scriptScope = scriptCode.DefaultScope;
pythonCalc = scriptScope.GetVariable<Func<object>>("PythonCalc")();
add = operations.GetMember<Func<object, object, object>>(pythonCalc, "add");
minus = operations.GetMember<Func<object, object, object>>(pythonCalc, "minus");
}
public TResult Add<TResult>(TResult val1, TResult val2)
{
return (TResult) add(val1, val2);
}
public TResult Minus<TResult>(TResult val1, TResult val2)
{
return (TResult) minus(val1, val2);
}
}
Пример использования первого и второго варианта очевиден:
var pythonCalc = new PythonCalc<int>(scriptEngine);
var result = pythonCalc.Add(12, 3) + pythonCalc.Minus(30, 5);
var pythonCalc1 = new PythonCalc1(scriptEngine);
var result = pythonCalc1.Add(12, 3) + pythonCalc1.Minus(30, 5);
Таким образом, мы получаем относительно простые обертки, позволяющие нам прозрачно использовать DLR объекты в статическом коде.
Весь python основан на двух концепциях, из которых следуют практически все остальные нюансы – все есть объект и некоторые объекты могут обладать callable свойством, т.е. их можно вызвать. Таким образом все классы, экземпляры классов, свойства, методы и т.п. – является объектом. Кроме того если углублятся в структуру классов python, то можно заметить, что экземпляр класса – фактически словарь, в котором хранятся имена членов класса и ссылки на них. Это позволяет использовать операцию GetMember для любых переменных, классов, свойств и методов, рассматривая их как члены класса. Так в этом примере:
add = operations.GetMember<Func<object, object, object>>(pythonCalc, "add");видно, что мы методом GetMember берем указатель на член экземпляра DLR класса PythonCalc, который обладает callable свойством, приводя его к определенной сигнатуре делегата.
Кроме того, каждый класс python является объектом, наследующимся от метакласса type. Этот стандартный метакласс реализует метод __call__, делающий каждый класс callable сущностью – конструктор. Поэтому появляется возможность применить метод GetMember для классов и привести к сингатуре делегата.
В ObjectOperations имеются методы CreateInstance и InvokeMember для аналогичных действий, однако они не всегда работают. В ironpython 2.0-2.6 имеется проблема в связи с вызовом методов и функций более чем с двумя параметрами из CLR (c# и т.д.). Если уж говорить по честному, то это проблема не IronPython, а DLR. Проблема заключается в том, что CreateInstance в любом случае, а InvokeMember при вызове метода класса более чем с двумя параметрами – генерирует исключение NotImplementedException. Например:
class Broker ( object ) :
def validation( self, info, provider_manager ):
pass
def check( self, payment, payment_manager, provider_manager ):
pass
operations = scriptEngine.CreateOperations();
var scriptSource = scriptEngine.CreateScriptSourceFromString(script,
SourceCodeKind.Statements);
scriptCode = scriptSource.Compile();
scriptCode.Execute();
scriptScope = scriptCode.DefaultScope;
broker = scriptScope.GetVariable<Func<object>>("Broker")();
operations.InvokeMember(broker, "validation", paymentInfo, providerManager);
operations.InvokeMember(broker, "check", payment, paymentManager, providerManager);
Вызов метода validation проходит успешно, а вот check вызывает ошибку NotImplementedException
System.NotImplementedException: Method or operation not implemented.
в Microsoft.Scripting.Runtime.DynamicOperations.InvokeMember(Object obj, String memberName, Boolean ignoreCase, Object[] parameters)
в Microsoft.Scripting.Hosting.ObjectOperations.InvokeMember(Object obj, String memberName, Object[] parameters)
We haven't implemented support for more than 3 parameters :)In 2.6 I just fixed > 3 params for the Invoke case (not InvokeMember)and I can fix > 3 for InvokeMember/CreateInstance as well before2.6 ships.The preferred way to do this though is to do a GetMember whereT is a delegate type with > 3 paraemters. Then you can just invokethe delegate.
Таким образом можно обойти указанную проблему не только для вызова методов объекта, но и для создания экземпляров класса.
В результате, создается впечаление, что в архитектуре DLR далеко не все так гладко - как будто присутствует две ветки реализации (InvokeMember и GetMember). Возможно это будет как пофиксено в конце концов.
Запись опубликована Roinet.Net.Вы можете оставить комментарии здесь или тут