FortranからPythonを使いたい！:forpy

FortranからPython、使いたいですよね？
Fortranで何かファイル出力とかディレクトリ操作とか大変ですので、そのようなFortranが苦手とすることはPythonに渡してしまいましょう。
例えば、二つのFortranコードを合体させたようなFortranコードの場合、Fortranのコードが何かのインプットファイルをテキストファイルとして吐き出して、それをインプットとするFortranサブルーチンを動かす、などがあります。インプットファイルをテキストファイルにするところがFortranは苦手だったりします。
あるいは、Fortranで数値計算をして、その結果を機械学習したいときにPythonを呼び出してTensorFlowとか使ったりしたいこともあるかと思います。

というわけで、forpyというものについて解説します。

https://github.com/ylikx/forpy

これはFortranからPythonを呼び出せるようになるライブラリです。

# 環境
- macOS Catalina 10.15.7
- gcc version 10.2.0 (Homebrew GCC 10.2.0_4) 

Pythonは3.7を使いました。すでにPythonは入っているとします。gfortranを使いましたが、ifortでも動くらしいです。

# インストールと動作確認

```
git clone https://github.com/ylikx/forpy.git
```
でforpyを持ってきます。
自分の作業用ディレクトリを適当に作成して、その中にforpy_mod.F90をコピーしましょう。
以後はその作業用ディレクトリで作業します。
まず、ちゃんと動くかどうか確認するために、サンプルコードを動かします。

```fortran
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
```
をintro_to_forpy.F90として保存します。

Python 3.7以前の場合は

```
gfortran -c forpy_mod.F90
gfortran intro_to_forpy.F90 forpy_mod.o `python3-config --ldflags`
```

Python 3.8以降であれば

```
# Python 3.8 and higher
gfortran -c forpy_mod.F90
gfortran intro_to_forpy.F90 forpy_mod.o `python3-config --ldflags --embed`
```
だそうです。

ただ、手元のMacではこれだと

```
gfortran test.f90 forpy_mod.o `python3-config --ldflags`                                     
ld: library not found for -lintl
collect2: error: ld returned 1 exit status
```
となりました。これはgettextというものが入っていないか適切にリンクされていないと起きるようです。解決策はbrewで

```
brew install gettext
```
をしてから、

```
gfortran test.f90 forpy_mod.o `python3-config --ldflags` -L/usr/local/Cellar/gettext/0.21/lib
```
としてください。0.21の部分はご自分のgettextのバージョンに合わせてください。
コンパイルができたら、

```
./a.out
```
で実行すれば、

```
[19, 'Hello world!', 3.14]
```
とアウトプットが出ます。これで無事Pythonを呼べました。

# 自分で作ったPythonモジュールの使用
Hello worldまでできましたが、次は、自分で作ったPythonモジュールを呼び出す方法についてです。ここでは一番単純なものを紹介します。他の方法はリンク先にありますので見てください。

まず、Pythonのファイルを```hellomodule.py```として

```python:hellomodule.py
def helloworld():
    print("hello world from Python!")
```
保存しておきます。

そして、

```fortran
subroutine test2()
    use forpy_mod
    implicit none
    type(module_py) :: hellomodule
    type(list) :: paths
    integer::ierror

    ierror = forpy_initialize()
    ierror = get_sys_path(paths)
    ierror = paths%append(".")
    
    ierror = import_py(hellomodule, "hellomodule")

    ierror = call_py_noret(hellomodule,"helloworld")

    call forpy_finalize
end

program intro_to_forpy
    integer :: ierror 
    call test2()  
end program
```
とすれば、Pythonのモジュールを呼べます。他にもPythonのリストとかタプルとかを扱う方法がリンク先には書かれてあります。
しかし、FortranからPythonを呼びたいときは、大抵Fortranだと書きにくいものを使いたいときですので、このように自分で作ったPythonのコードを呼ぶことができさえすれば、あとはPythonのコードの中身を適当に書けば良い、ということになります。

もし返り値が必要な場合は、

```python:mymodule.py
def print_args(*args, **kwargs):
    print("Arguments: ", args)
    print("Keyword arguments: ", kwargs)
    
    return "Returned from mymodule.print_args"
```

```fortran
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
```
みたいに```call_py```を使えば良いようです。