Edited at

PythonっぽいJulia:PythonコードのclassをJuliaコードへ移植する

PythonのコードをJuliaのコードに移植したい!ということがあるかと思います。

その際、最初に困りそうなものはPythonにおけるclassの扱いです。

Pythonにclassがあったときに、どのようにJuliaに移植するか、ということを書きます。

なお、Pythonのclassについてはあまり詳しくないので、

"【Python入門】Pythonにおけるclassの使い方とは?"

https://qiita.com/Morio/items/0fe3abb58fcaff229f3d



"Pythonのイテレータとジェネレータ"

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

を参考にさせていただきました。


バージョン

Julia 1.1

Python 3系


class


Python

まず初めに、Python3系で以下のようなclassがあったとします。

class MyIterator(object):

def __init__(self, *numbers):
self._numbers = numbers
self._i = 0

my_iterator = MyIterator(10, 20, 30)
print(my_iterator._numbers)

これを実行すると、

(10, 20, 30)

となります。Pythonでのclassの初期化は、__init__が使われます。


Julia

次に、このコードと等価なJuliaコードは、

module myiterator

export MyIterator

structMyIterator
_numbers
end
function MyIterator(numbers...)
_numbers = numbers
return MyIterator(_numbers)
end
end

using .myiterator
my_iterator = MyIterator(10,20,30)
println(my_iterator._numbers)

となります。ここで、上のコードはmoduleを使わずに

structMyIterator 

_numbers
end
function MyIterator(numbers...)
_numbers = numbers
return MyIterator(_numbers)
end
my_iterator = MyIterator(10,20,30)
println(my_iterator._numbers)

としても構いません。Pythonのclassと同じように動作をひとまとめにするために、あえてmoduleを使っています。

ここで、MyIterator型の_numbersは変更されないものと仮定していますが、もし値が変更されるようなものであれば、structのかわりにmutable structを使います。

さて、MyIterator(numbers...)numbers......は同じような引数が複数来る場合を想定しています。この関数の中でMyIterator型を返り値とすることで、Pythonでの__init__と同等の機能が得られます。


イテレータ

次に、forループをinで回すことを考えます。


Python

"Pythonのイテレータとジェネレータ"

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

を参考にして、classをinを使ってfor文で回すことを考えます。コードは

class MyIterator(object):

def __init__(self, *numbers):
self._numbers = numbers
self._i = 0
def __iter__(self):
# __next__()はselfが実装してるのでそのままselfを返す
return self
def __next__(self): # Python2だと next(self) で定義
if self._i == len(self._numbers):
raise StopIteration()
value = self._numbers[self._i]
self._i += 1
return value

my_iterator = MyIterator(10, 20, 30)
for num in my_iterator:
print('hello %d' % num)

とします。

この出力結果は、

hello 10

hello 20
hello 30

となります。for文で回すためにイテレータというものを設定しており、このclassはiterableなオブジェクトとなっています。


Julia

Juliaでのイテレータは

"Julia 0.7-DEV の新しい Iteration に触れてみた。"

https://qiita.com/antimon2/items/8b1a96d1370bb6252757

を参考にします。

コードは

module myiterator

export MyIterator

structMyIterator
_numbers
end
function MyIterator(numbers...)
_numbers = numbers
return MyIterator(_numbers)
end
function Base.iterate(self::MyIterator,i::Int=0)
i == length(self._numbers) && return nothing
value = self._numbers[i+1]
i +=1
return (value,i)
end
end

using .myiterator
my_iterator = MyIterator(10,20,30)
for num in my_iterator
println("hello \t",num)
end

となります。Base.iterateを型MyIteratorに対して多重定義しています。

Base.iterateは返り値としては二つの要素を持ち、なんらかの値と状態を表す値(この場合は変数i)となっています。これによって、for num in my_iteratorのように、for文でinとして呼べるようになります。


継承

次は、classの継承についてです。


Python

Pythonのクラスは他のクラスを継承することができます。

今回は、

"【Python入門】Pythonにおけるclassの使い方とは?"

https://qiita.com/Morio/items/0fe3abb58fcaff229f3d

を参考にします。コードを

class Test:

def __init__(self, num):
self.num = num;

def print_num(self):
print('引数で渡された数字は{}です。'.format(self.num))

class Test2(Test): #Testクラスを継承
def print_test2_info(self):
print('このクラスはTestクラスを継承しています。')
super().print_num() #親クラスのprint_num()を呼び出す

test = Test2(10)
test.print_test2_info()

とします。実行すると、

このクラスはTestクラスを継承しています。

引数で渡された数字は10です。

と出力されます。

クラスTestはメソッドとしてprint_numを持っており、初期化は__init__で行われます。クラスTest2はクラスTestを継承しているので、print_numを使うことができますし、初期化はTest__init__を使うことができます。


Julia


以下は間違いです。正しいものは追記に後述しました

Juliaでは、Pythonの継承に相当する機能を使うためには、抽象型Abstract typeを使います。

これは、Abstract type型は子のtypeを持つことができ、子のtypeは親のtypeが使用条件である関数を使うことができます。

したがって、コードは

#=

abstract type Test end
function Test(self::Test,num)
return self(num)
end
function print_num(self::Test)
println("引数で渡された数字は$(self.num)です。")
end
struct Test2 <: Test
num
end
function Test2(self::Test2,num)
return Test(self,num)
end
function print_test2_info(self::Test2)
println("この型はTest型を継承しています。")
print_num(self)
end
test = Test2(10)
print_test2_info(test)
=#

となります。ここで、abstract typeとしてTestを定義し、型がTestである引数がきた場合に使われる関数print_numが定義されています。ここでselfと引数をしていますが、これはPythonと似せるためにそうしているだけで、どんなものでも構いません。

Test2<: Testとなっており、Testをsupertypeとして持ちます。そのため、Testを引数とする関数を使うことができます。

Pythonのように__init__に相当するものを親と子で共通化する方法は存在するかわかりませんでした。上のコードでは、Testの初期化としてTest(self::Test,num)を、Test2の初期化としてTest2(self::Test2,num)を呼んでいまして、Test2(self::Test2,num)の中ではTest(self::Test,num)を呼ぶことで共通の初期化となるようにしています。

ここで注意しなければならないのは、抽象型abstract typeはフィールドを持てない、ということです。ここでは、numです。

そのため、その型がどんなフィールドを持っているかは、型の葉であるmutable structやstructを見なければなりません。

しかしこれはわかりやすさの意味では良いかもしれません。というのは、実際に操作するのはTest2ですので、その定義を見ればどのようなフィールドがあるかを親を見ずにわかりますので。


追記

上記のコードは間違っていますので、下に新しいコードを記します。

Juliaでは、Pythonの継承に相当する機能を使うためには、抽象型Abstract typeを使います。

これは、Abstract type型は子のtypeを持つことができ、子のtypeは親のtypeが使用条件である関数を使うことができます。

abstract typeTest end

function (test::Type{<:Test})(num)
println("初期化")
return test(num)
end
function print_num(self::Test)
println("引数で渡された数字は$(self.num)です。")
end
structTest2 <: Test
num
end

function print_test2_info(self::Test2)
println("この型はTest型を継承しています。")
print_num(self)
end
test = Test2(10)
print_test2_info(test)

となります。ここで、abstract typeとしてTestを定義し、型がTestである引数がきた場合に使われる関数print_numが定義されています。ここでselfと引数をしていますが、これはPythonと似せるためにそうしているだけで、どんなものでも構いません。

Test2<: Testとなっており、Testをsupertypeとして持ちます。そのため、Testを引数とする関数を使うことができます。

Pythonのように__init__に相当するものを親と子で共通化する方法は存在するかわかりませんでした。

@antimon2 さんのコメントにもありますように、抽象型abstract typeはフィールドを持てないために、これはできないようです。フィールドとは、ここではnumです。

しかし、親が同じ子が初期化するための方法を記述することはできるようです。上記コードでは

function (test::Type{<:Test})(num)

println("初期化")
return test(num)
end

の部分のことです。

これは型がTestに属するようなものをまとめて初期化するようにしています。これではあまり恩恵を感じられませんので、以下の節を見てください。


継承と初期化


Python

Pythonコードが

class Test:

def __init__(self, num):
self.a = 100
self.num = num;

def print_num(self):
print('引数で渡された数字は{}です。'.format(self.num))
print('aの値は{}です。'.format(self.a))

class Test2(Test): #Testクラスを継承
def print_test2_info(self):
print('このクラスはTestクラスを継承しています。')
super().print_num() #親クラスのprint_num()を呼び出す

test = Test2(10)
test.print_test2_info()

であるとします。このコードでは、Test2aを100として初期化しています。

このように、他の値はデフォルトのままにしたい、という需要はあるかと思います。l


Julia

上記のPythonコードをJuliaで書くと、

abstract typeTest end

function (test::Type{<:Test})(num)
println("初期化")
a = 100
return test(num,a)
end
function print_num(self::Test)
println("引数で渡された数字は$(self.num)です。")
println("aの値は$(self.a)です。")
end
structTest2 <: Test
num
a
end

function print_test2_info(self::Test2)
println("この型はTest型を継承しています。")
print_num(self)
end
test = Test2(10)
print_test2_info(test)

となります。

function (test::Type{<:Test})(num)

println("初期化")
a = 100
return test(num,a)
end

では、Test型を親にもつ型に対して動作を指定しています。

ただし、子のフィールドが二つでなければエラーとなります。

Test2はフィールドを二つ持っているので、エラーなしで動きました。

フィールドが二つである、とはっきりさせておきたい場合には、

function (test::Type{<:Test})(num)

println("初期化")
if length(test.types) == 2
a = 100
return test(num,a)
elseif length(test.types) == 1
return test(num)
else
println("Error! num. of fields should be 2")
end
end

としても良いかと思います。


まとめ

以上のように、PythonのclassはJuliaのtypeを使って書けることがわかりました。