LoginSignup
34
20

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-18

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()

であるとします。このコードでは、Test2aを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を使って書けることがわかりました。

34
20
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
20