-
Notifications
You must be signed in to change notification settings - Fork 296
Interop with .NET
One of the main purposes of IronPython is as a .NET programming language. This article briefly describes a few, perhaps not so obvious cases where IronPython is used to access the .NET library (or any other managed code). For more in-depth coverage of programming .NET with IronPython, refer to an excellent book "IronPython in Action" by Michael J. Foord and Christian Muirhead (ISBN 9781933988337). That book covers IronPython 2, but is still largely accurate for IronPython 3. For main differences between IronPython 3 and 2, refer to Upgrading from IronPython2.
Some Python types are implemented using corresponding .NET types. For instance bool
is implemented with System.Boolean
. By default, only Python-specific members are exposed. However, importing module System
brings all .NET members into the scope.
>>> dir(bool)
['__abs__', '__add__', '__and__', '__bool__', '__class__', '__delattr__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> import System
>>> dir(bool)
['Abs', 'Add', 'AsDecimal', 'AsInt32', 'AsInt64', 'AsUInt32', 'AsUInt64', 'BitwiseAnd', 'BitwiseOr', 'Compare', 'CompareTo', 'Create', 'DivRem', 'Divide', 'Equals', 'ExclusiveOr', 'FalseString', 'GetBitCount', 'GetByteCount', 'GetHashCode', 'GetType', 'GetTypeCode', 'GetWord', 'GetWordCount', 'GetWords', 'GreatestCommonDivisor', 'IsEven', 'IsNegative', 'IsOne', 'IsPositive', 'IsPowerOfTwo', 'IsZero', 'LeftShift', 'Log', 'Log10', 'Max', 'MemberwiseClone', 'Min', 'MinusOne', 'Mod', 'ModPow', 'Multiply', 'Negate', 'One', 'OnesComplement', 'Parse', 'Pow', 'ReferenceEquals', 'Remainder', 'RightShift', 'Sign', 'Square', 'Subtract', 'ToBigInteger', 'ToBoolean', 'ToByte', 'ToByteArray', 'ToChar', 'ToDecimal', 'ToDouble', 'ToFloat', 'ToInt16', 'ToInt32', 'ToInt64', 'ToSByte', 'ToSingle', 'ToString', 'ToType', 'ToUInt16', 'ToUInt32', 'ToUInt64', 'TrueString', 'TryParse', 'Xor', 'Zero', '__abs__', '__add__', '__and__', '__bool__', '__class__', '__complex__', '__delattr__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> bool is System.Boolean
True
The Python int
type in IronPython 3 is implemented with System.Numerics.BigInteger
(and not as System.Int32
as it was in IronPython 2). It can contain in theory an arbitrarily large integer (only limited by the 2 GByte memory boundary).
>>> import clr
>>> clr.AddReference("System.Numerics")
>>> import System
>>> int is System.Numerics.BigInteger
True
>>> int is System.Int32
False
>>> clr.GetClrType(int).Name
'BigInteger'
However, mostly for performance reasons, IronPython will use instances of System.Int32
to hold smaller integers. This is largely transparent from the Python side, but the distinction may become relevant for interop cases. Examples:
i = 1 # instance of Int32
j = 1 << 31 # instance of BigInteger
k = j - 1 # still BigInteger, as one of the arguments makes the result type BigInteger
This means that the type of Int32
objects is reported as int
(which is the same as BigInteger
). If it is important to check what is the actual type of a given integer object, test for the presence of MaxValue
or MinValue
. For those properties to be visible, System
has to be imported first.
>>> import System
>>> type(i)
<class 'int'>
>>> hasattr(i, 'MaxValue') # Int32
True
>>> hex(i.MaxValue)
'0x7fffffff'
>>> type(j)
<class 'int'>
>>> hasattr(j, 'MaxValue') # BigInteger
False
The creation of either Int32
or BigInteger
instances happens automatically by the int
constructor. If for interop purposes it is important to create a BigInteger
(despite the value fitting in 32 bits), use method ToBigInteger
. It converts Int32
values to BigInteger
and leaves BigInteger
values unaffected.
>>> bi = i.ToBigInteger()
>>> hasattr(bi, 'MaxValue')
False
In the opposite direction, if it is essential to create Int32
objects, either use constructors for int
or Int32
. The former converts an integer to Int32
if the value fits in 32 bits, otherwise it leaves it as BigInteger
. The latter throws an exception is the conversion is not possible.
For more examples see Upgrading from Python2 — int Type .
Specialized types based on generic types in .NET can be constructed using the indexed-type notation. The indexer is the type to be used in place of a generic type.
Example.
>>> from System.Collections.Generic import List
>>> python_list = list(range(10))
>>> cli_list = List[int](python_list)
>>> cli_list
List[int]([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> cli_list.Add(10)
>>> list(cli_list)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
CLR arrays are usually created using the generic class construction syntax. Example:
from System import Array, Int32
arr1 = Array[string](10)
arr2 = Array[Int32](2, 3)
The above is equivalent to the following C# code:
string[] arr1 = new string[10];
int[,] arr2 = new int[2, 3];
One-dimensional arrays can be created and initialized in one step, by providing an iterable object with a defined length to the constructor:
>>> vec = Array[Int32](range(10))
>>> vec
Array[Int32]((0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
A less common way of creating arrays is by using static method CreateInstance
. This allows for greater flexibility in some corner cases. For instance, let's create a 1-based 2x2 matrix (2-dimensional array):
matrix = Array.CreateInstance(str, (2,2), (1,1))
matrix[1, 1] = "x_1,1"
matrix[1, 2] = "x_1,2"
matrix[2, 1] = "x_2,1"
matrix[2, 2] = "x_2,2"
Nonzero-based arrays are fully supported, including negative indices counting from the end.
>>> matrix[1, 1] == matrix[-2, -2]
True
This is the Pythonic way of interpreting negative indices as relative from the end, but it breaks down if the array base is negative too. In such case negative indices are treated as absolute, to allow access to the part of the array that uses negative indices. For instance, consider a 3-element array indexed from -1 to 1.
>>> symmetricArr = Array.CreateInstance(int, (3,), (-1,))
>>> symmetricArr
Array[int]((0, 0, 0), base=-1)
>>> symmetricArr[-1] = 10
>>> symmetricArr[1] = -10
>>> symmetricArr
Array[int]((10, 0, -10), base=-1)
Base | Index >= 0 | Index < 0 |
---|---|---|
> 0 | absolute | relative from end |
0 | absolute == relative from beginning | relative from end |
< 0 | absolute | absolute |
Comparison to indexing in C# and CPython:
- Index >= 0, any base is C# compliant.
- Base 0, any index is CPython compliant.
- Base 0, index < 0 is not supported by C# but can be achieved by
System.Index
with 1-dim arrays only; then IronPython indexing is C# compliant (as well as CPython compliant) in principle (support forSystem.Index
is not implemented). - Base > 0, index < 0 is not supported by C#; IronPython follows the CPython convention as more practical.
- Base < 0, index < 0 is C# compliant.
- Base != 0 is not supported by CPython for any builtin structures.
Base | Index >= 0 | Index < 0 |
---|---|---|
> 0 | absolute | N/A |
0 | absolute | N/A |
< 0 | absolute | absolute |
Base | Index >= 0 | Index < 0 |
---|---|---|
> 0 | N/A | N/A |
0 | relative from beginning | relative from end |
< 0 | N/A | N/A |
The table for IronPython is basically a merge of the tables for C# and CPython.
For more detailed explanation of indexing see https://github.com/IronLanguages/ironpython3/pull/1828#discussion_r1870572832
To call methods that use an out
or ref
parameter, a strongly-typed box can be created and provided as the argument.
>>> import clr, System
>>> box = clr.StrongBox[float]() # initialized to 0.0
>>> # Calling public static bool TryParse(string? value, out double result);
>>> System.Double.TryParse("3.1416", box)
True
>>> box.Value
3.1415999999999999
The input value for the box used as a ref
argument can be provided as argument to the box constructor or by assigning its property Value
. An alternative to clr.StrongBox
is clr.Reference
, which is just a forward to StrongBox
.
For out
arguments, a simplified call convention is supported, where the out
argument is omitted and instead a tuple is returned containing the actual return value as the first element (if the method return type is not void
), with subsequent elements being values for the missing out
arguments. If the tuple would be of length 1, the single element is returned instead.
>>> import System
>>> # Calling public static bool TryParse(string? value, out double result);
>>> System.Double.TryParse("3.1416")
(True, 3.1415999999999999)
For ref
arguments a similar convention holds, except that the arguments cannot be omitted. The output values for those arguments are still included in the return.
>>> import System
>>> arr = System.Array[bool](4)
>>> arr
Array[bool]((False, False, False, False))
>>> System.Array.Resize(arr, 6)
Array[bool]((False, False, False, False, False, False))
Extension methods are by default not visible and must be explicitly imported before used. Also the DLL containing the extensions has to be referenced first.
import clr, System
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
query = range(10).Where(lambda x: x % 2 == 0).Select(lambda x: 2 ** x)
list(query) # executes query, result: [1, 4, 16, 64, 256]
.NET interface types are modelled as abstract Python classes. This means it is possible to make an unbounded method call on an interface-derived class by providing self
as the extra first argument.
For instance, the following C# expressions (using redundant casts for clarity)
((System.IComparable<int>)1).CompareTo(2)
((System.IComparable<System.Numerics.BigInteger>)new System.Numerics.BigInteger(1)).CompareTo(2)
have equivalent IronPython expressions
System.IComparable[System.Int32].CompareTo(1, 2)
System.IComparable[int].CompareTo(1, 2)
.NET Span<T>
and ReadOnlySpan<T>
type are ref-structs and instances of such types cannot be boxed. Therefore it is not possible to have a Python variable referring to an instance of Span<T>
or ReadOnlySpan<T>
. Moreover, it is not possible to make calls from IronPython to methods that take spans as arguments (since existing reflection invoke APIs require boxing).
On the other hand, Memory<T>
and ReadOnlyMemory<T>
is fully supported.
import System
arr = System.Array[System.Byte](10)
dest = System.Memory[System.Byte](arr)
For more info why spans are not supported, see the docs.
There is a proposal to implement this support in .NET
Python does not have a special casting syntax like C# or C++. It is usually not necessary in a dynamic language and if it is, usually the type constructor is sufficient, e.g. float(i)
converts an object referred by i into float (if possible). Type constructors in IronPython do that as well, including for CLR types. For convenience, those constructors are often augmented to accept arguments beyond what the CLR defines. For instance, System.UInt64(1)
creates an object of type System.UInt64
and initializes it to hold value 1
. Or System.Char(65)
creates a System.Char
object with value "A". In cases where such support is not (yet) provided, one can invoke a conversion operator (explicit or implicit) defined for the given type by using clr.Convert
. Beyond engaging conversion operators, this function will also apply typical Python conversions in order to succeed.
>>> import System, clr
>>> System.Char(65) # using constructor
A
>>> clr.Convert(65, System.Char) # using cast, same result
A
>>> clr.Convert(1.23456789, System.Half) # using cast
<System.Half object at 0x000000000000002B [1.234]>
>>> clr.Convert("abc", System.Boolean) # in Python, everything is truthy or falsy
True
Still looking for more? Browse the Discussions tab, where you can ask questions to the IronPython community.
🐍 IronPython