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
struct MyIterator
_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
を使わずに
struct MyIterator
_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
struct MyIterator
_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 type Test end
function (test::Type{<:Test})(num)
println("初期化")
return test(num)
end
function print_num(self::Test)
println("引数で渡された数字は$(self.num)です。")
end
struct Test2 <: 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()
であるとします。このコードでは、Test2
のa
を100として初期化しています。
このように、他の値はデフォルトのままにしたい、という需要はあるかと思います。l
Julia
上記のPythonコードをJuliaで書くと、
abstract type Test 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
struct Test2 <: 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を使って書けることがわかりました。