Forpy
Forpy - use Python from Fortran
Install / Use
/learn @ylikx/ForpyREADME
Forpy: A library for Fortran-Python interoperability.
Forpy allows you to use Python features in Fortran ("embedding Python in Fortran")
It provides datastructures such as list, dict, tuple and interoperability of arrays using numpy. With forpy you can even import Python modules in Fortran. Simply use your own or third-party Python modules for tasks that you can easily do in Python. For example: plot with matplotlib or use scientific functions from scipy or numpy.
Forpy also works to other way around: You can write Python modules entirely in Fortran (extending Python with Fortran - "Fortran in Python").
Documentation
- This readme (start with that)
- Wiki
- API reference
Contact
Elias Rabel (ylikx.0 AT gmail.com)
Getting started
A simple example using a Python list:
program intro_to_forpy
use forpy_mod
implicit none
integer :: ierror
type(list) :: my_list
ierror = forpy_initialize()
ierror = list_create(my_list)
ierror = my_list%append(19)
ierror = my_list%append("Hello world!")
ierror = my_list%append(3.14d0)
ierror = print_py(my_list)
call my_list%destroy
call forpy_finalize
end program
Building the example:
To try the examples, copy the file forpy_mod.F90 to your working directory.
Here I assume that you are using Python 3 (version >= 3.3) and
gfortran (ifort also supported).
If you are using Anaconda and have problems when building read Using forpy with Anaconda.
If you are using Windows, read Forpy on Windows.
For use with Python 2 read Python 2 support.
Save the example as intro_to_forpy.F90 and type, depending on your Python version:
# Python 3.7 and earlier
gfortran -c forpy_mod.F90
gfortran intro_to_forpy.F90 forpy_mod.o `python3-config --ldflags`
# Python 3.8 and higher
gfortran -c forpy_mod.F90
gfortran intro_to_forpy.F90 forpy_mod.o `python3-config --ldflags --embed`
Then run the example with
./a.out
You should get the output:
[19, 'Hello world!', 3.14]
If python3-config is not found, you might have to install the package python3-dev (on Ubuntu, Debian).
For simplicity this example and most following examples do not contain error handling code.
Tuples, objects
This example introduces tuples and shows how to check for basic Python types.
It demonstrates the methods getitem and setitem, which also work
with list. These methods are generic for important Fortran types.
The type object can be used for any Python object. Use cast to transform an
object into a Fortran type or to transform into
a more specific Python object, such as list or tuple.
program tuple_example
use forpy_mod
implicit none
integer :: ierror
type(tuple) :: tu
type(object) :: item
integer :: int_value
character(len=:), allocatable :: str_value
integer :: ii
integer :: tu_len
ierror = forpy_initialize()
! Python: tu = (17, "hello", 23, "world")
ierror = tuple_create(tu, 4) ! create tuple with 4 elements
! Must set all tuple elements before using tuple
ierror = tu%setitem(0, 17)
ierror = tu%setitem(1, "hello")
ierror = tu%setitem(2, 23)
ierror = tu%setitem(3, "world")
ierror = tu%len(tu_len)
do ii = 0, tu_len-1 ! Python indices start at 0
ierror = tu%getitem(item, ii)
! Use is_int, is_str, is_float, is_none ...
! to check if an object is of a certain Python type
if (is_int(item)) then
! Use cast to transform 'item' into Fortran type
ierror = cast(int_value, item)
write(*,*) int_value
else if(is_str(item)) then
ierror = cast(str_value, item)
write(*,*) str_value
endif
call item%destroy
enddo
call tu%destroy
call forpy_finalize
end program
Dictionaries, Error handling
The following example shows how to use a Python dict and shows some
error and exception handling.
program dict_example
use forpy_mod
implicit none
integer :: ierror
type(dict) :: di
real :: a_value
ierror = forpy_initialize()
ierror = dict_create(di) ! Python: di = {}
ierror = di%setitem("temperature", 273.0)
ierror = di%setitem("pressure", 1013.0)
ierror = di%getitem(a_value, "pressure")
write(*,*) "pressure = ", a_value
! Show some error handling
ierror = di%getitem(a_value, "does not exist")
if (ierror /= 0) then
if (exception_matches(KeyError)) then
write(*,*) "Key not found..."
! Must clear error after handling exception,
! if we want to continue with program!
call err_clear
else
write(*,*) "Unknown error..."
stop
endif
endif
! alternative to getitem: get - returns given default value if key
! not found, no KeyError exception raised
ierror = di%get(a_value, "volume", 1.0)
write(*,*) "volume = ", a_value
call di%destroy
call forpy_finalize
end program
Import a Python module in Fortran
The following demo, shows how to use a module from Python's standard
library and introduces call_py, which is used to call Python methods and
to instantiate Python objects.
program date_demo
use forpy_mod
implicit none
integer :: ierror
type(module_py) :: datetime
type(object) :: date, today, today_str
character(len=:), allocatable :: today_fortran
! Python:
! import datetime
! date = datetime.date
! today = date.today()
! today_str = today.isoformat()
! print("Today is ", today_str)
ierror = forpy_initialize()
ierror = import_py(datetime, "datetime")
ierror = datetime%getattribute(date, "date")
ierror = call_py(today, date, "today")
ierror = call_py(today_str, today, "isoformat")
ierror = cast(today_fortran, today_str)
write(*,*) "Today is ", today_fortran
call datetime%destroy
call date%destroy
call today%destroy
call today_str%destroy
call forpy_finalize
end program
For Python to import a module that is not in one of the standard search
directories, you can set the environment variable PYTHONPATH:
export PYTHONPATH=$PYTHONPATH:path_to_my_python_module
Alternatively, you can use forpy's get_sys_path function to retrieve and modify the list
of Python module search paths, as shown in the following example.
We want to import the following small Python module:
# File: mymodule.py
def print_args(*args, **kwargs):
print("Arguments: ", args)
print("Keyword arguments: ", kwargs)
return "Returned from mymodule.print_args"
Now we use the module in Fortran, assuming that mymodule.py is in the current
working directory:
program mymodule_example
use forpy_mod
implicit none
integer :: ierror
type(tuple) :: args
type(dict) :: kwargs
type(module_py) :: mymodule
type(object) :: return_value
type(list) :: paths
character(len=:), allocatable :: return_string
ierror = forpy_initialize()
! Instead of setting the environment variable PYTHONPATH,
! we can add the current directory "." to sys.path
ierror = get_sys_path(paths)
ierror = paths%append(".")
ierror = import_py(mymodule, "mymodule")
! Python:
! return_value = mymodule.print_args(12, "Hi", True, message="Hello world!")
ierror = tuple_create(args, 3)
ierror = args%setitem(0, 12)
ierror = args%setitem(1, "Hi")
ierror = args%setitem(2, .true.)
ierror = dict_create(kwargs)
ierror = kwargs%setitem("message", "Hello world!")
ierror = call_py(return_value, mymodule, "print_args", args, kwargs)
ierror = cast(return_string, return_value)
write(*,*) return_string
! For call_py, args and kwargs are optional
! use call_py_noret to ignore the return value
! E. g.:
! ierror = call_py_noret(mymodule, "print_args")
call args%destroy
call kwargs%destroy
call mymodule%destroy
call return_value%destroy
call paths%destroy
call forpy_finalize
end program
Working with arrays
Forpy offers interoperability of Fortran arrays and numpy arrays through
the type ndarray. In the
following examples, you will see various ways to create a numpy array.
Creating a numpy array from a Fortran array
The simplest way to create a numpy array is with ndarray_create. This
function creates a numpy array with the same content as a Fortran array that is
passed to the function. For example:
program ndarray01
use forpy_mod
implicit none
integer, parameter :: NROWS = 2
integer, parameter :: NCOLS = 3
integer :: ierror, ii, jj
real :: matrix(NROWS, NCOLS)
type(ndarray) :: arr
ierror = forpy_initialize()
do jj = 1, NCOLS
do ii = 1, NROWS
matrix(ii, jj) = real(ii) * jj
enddo
enddo
! creates a numpy array with the same content as 'matrix'
ierror = ndarray_create(arr, matrix)
ierror = print_py(arr)
call arr%destroy
call forpy_finalize
end program
When arrays get very large, creating a copy might not be what you want. The next section describes how to wrap a Fortran array with forpy without making a copy.
Creating a numpy wrapper for a Fortran array
When creating a numpy array with ndarray_create_nocopy, no copy of the Fortran
array is made. This is more efficient than ndarray_create, but there are
some things to consider: Changes to the Fortran array affect the numpy array
and vice versa. You have to make sure that the Fortran array is valid
as long as the numpy array is in use.
Since the Fortran array can now be modified not
only directly but also indirectly by the ndarray, it is necessary to
add the asynchronous attribute to the Fortran array declaration, since
without it compiler optimization related bugs
can occur (depending on code, compiler and compiler options).
Alternatively you could also use the volatile attribute.
