Pythonのオブジェクト指向プログラミングを完全理解という記事はオブジェクト指向の歴史などが書いてあり、とても勉強になります。この記事の前半部分で、オブジェクト指向とプロセス指向の書き方について違いを述べていまして、「オブジェクト指向の方が簡潔に記述できる」というようなことが書かれています。
ところで、Juliaはオブジェクト指向を採用しておりませんが、多重ディスパッチによって同等の機能を実現していると言われています。これはつまり、「プロセス指向」であるにもかかわらずこの記事に書かれているようなことができる、はずです。そこで、Pythonのコードと比較をしながらJuliaでの書き方についてみていきましょう。
2-1. インターフェースの統一と管理
「2-1. インターフェースの統一と管理」の部分のPythonコードをこちらに再掲しますと、鳥、犬、魚のクラスを
class Bird:
def __init__(self, name):
self.name = name
def move(self):
print("The bird named {} is flying".format(self.name))
class Dog:
def __init__(self, name):
self.name = name
def move(self):
print("The dog named {} is running".format(self.name))
class Fish:
def __init__(self, name):
self.name = name
def move(self):
print("The fish named {} is swimming".format(self.name))
と作成し、
bob = Bird("Bob")
john = Bird("John")
david = Dog("David")
fabian = Fish("Fabian")
とインスタンスを作成しています。そしてmoveメソッドを呼び出していました:
bob.move()
john.move()
david.move()
fabian.move()
記事では、プロセス指向の場合は
bobというオブジェクトが来たら、それが「鳥」なのか「犬」なのかをまず明確にしないと、move_birdとmove_dogのどれにするかが決められません。実際のプログラムではmoveだけではなく、数十種類の処理関数を実装するのが普通です。関数が多くなると、変数との対応関係を明確にするのが極めて難しくなります。また、これらの関数は内部で他の関数を呼び出している可能性もあり、この関数を他のプログラムで再利用する時に、内部で使われている関数も全部見つけ出して、移行する必要があります。
オブジェクト指向は変数を使って、classからインスタンスを作成し、どのメソッドが使えるかはclassを見れば分かります。そして、classとして抽象化することで、同じ文脈の関数が固まり、管理しやすくなります。
とあります。これをJuliaでやってみます。鳥と犬と魚の構造体を
struct Bird
name
end
function move(self::Bird)
println("The bird named $(self.name) is flying")
end
struct Dog
name
end
function move(self::Dog)
println("The dog named $(self.name) is running")
end
struct Fish
name
end
function move(self::Fish)
println("The fish named $(self.name) is swimming")
end
と定義します。インスタンスは
bob = Bird("Bob")
john = Bird("John")
david = Dog("David")
fabian = Fish("Fabian")
で作成されます。そして、moveは
move(bob)
move(john)
move(david)
move(fabian)
となります。出力結果はPythonのコードと同じく、
The bird named Bob is flying
The bird named John is flying
The dog named David is running
The fish named Fabian is swimming
となります。全て同じmoveで動作を決めており、
bobというオブジェクトが来たら、それが「鳥」なのか「犬」なのかをまず明確にしないと、move_birdとmove_dogのどれにするかが決められません。
の部分をJuliaは多重ディスパッチによって解消していることがわかります。型が異なれば同じ名前でも異なるメソッドが呼ばれており、これが多重ディスパッチの機能です。
なお、Juliaで書く場合には
struct Dog{T}
name::T
end
とした方がパフォーマンスとしてはよいですが、わかりやすさのため、この記事では基本的には型情報は書いていません。
2-2. カプセル化
「2-2. カプセル化」についてみていきます。元の記事のPythonコードは
class Person:
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
def describe(self):
print("name: {}; age: {}; height: {}".format(self.name, self.age, self.height))
def introduce(self):
print("My name is {}, and height is {}, and age is {}. ".format(self.name, self.height, self.age))
bob = Person("Bob", 24, 170)
mary = Person("Mary", 10, 160)
bob.describe()
bob.introduce()
mary.describe()
mary.introduce()
です。
オブジェクト指向は関数とデータを一緒に束ねてくれるので、同じ変数(データ)をたくさんの関数で処理したい時はとても便利です。
とのことでした。
これをJuliaでやってみます。
struct Person
name
age
height
end
describe(self::Person) = println("name: $(self.name); age: $(self.age); height: $(self.height)")
introduce(self::Person) = println("My name is $(self.name), and height is $(self.height), and age is $(self.age). ")
bob = Person("Bob", 24, 170)
mary = Person("Mary", 10, 160)
describe(bob)
introduce(bob)
describe(mary)
introduce(mary)
となります。出力結果は
name: Bob; age: 24; height: 170
My name is Bob, and height is 170, and age is 24.
name: Mary; age: 10; height: 160
My name is Mary, and height is 160, and age is 10.
です。
ほとんどPythonと同じような書き方ができており、オブジェクト指向ではなくてもJuliaならコンパクトに書けていることがわかります。ですので、
上記の処理をプロセス指向で実装すると、以下の2通りの方法があります。1つはそのまま引数として渡す方法です。
というのはJuliaでは当てはまりません。
2-3. オブジェクトの動的操作
「2-3. オブジェクトの動的操作」についてです。まず、元の記事のPythonコードは
class Individual:
def __init__(self, energy=10):
self.energy = energy
def eat_fruit(self):
self.energy += 1
return self
def eat_meat(self):
self.energy += 2
return self
def run(self):
self.energy -= 3
return self
anyone = Individual()
print("energy: {}".format(anyone.energy))
anyone.eat_meat()
print("energy after eat_meat: {}".format(anyone.energy))
anyone.eat_fruit()
print("energy after eat_fruit: {}".format(anyone.energy))
anyone.run()
print("energy after run: {}".format(anyone.energy))
anyone.eat_meat().run()
print("energy after eat_meat and run: {}".format(anyone.energy))
です。anyone.eat_meat().run()のように繋がっているのが便利ですね。Juliaでこれを書くと、
mutable struct Individual
energy
Individual(energy=10) = new(energy)
end
function eat_fruit(self::Individual)
self.energy += 1
return self
end
function eat_meat(self::Individual)
self.energy += 2
return self
end
function run(self::Individual)
self.energy -= 3
return self
end
anyone = Individual()
println("energy: $(anyone.energy)")
eat_meat(anyone)
println("energy after eat_meat: $(anyone.energy)")
eat_fruit(anyone)
println("energy after eat_fruit: $(anyone.energy)")
run(anyone)
println("energy after run: $(anyone.energy)")
eat_meat(anyone) |> run
println("energy after eat_meat and run: $(anyone.energy)")
となります。出力は
energy: 10
energy after eat_meat: 12
energy after eat_fruit: 13
energy after run: 10
energy after eat_meat and run: 9
です。Juliaではパイプライン演算子|>を使うことで、Pythonと同様に連結することができます。
次に、元の記事のPythonコードは
class Boy(Individual):
def daily_activity(self):
self.eat_meat().eat_meat().run().eat_meat().eat_fruit().run().eat_meat()
print("boy's daily energy: {}".format(self.energy))
class Girl(Individual):
def daily_activity(self):
self.eat_meat().eat_fruit()
print("girl's daily energy: {}".format(self.energy))
bob = Boy()
bob.daily_activity()
mary = Girl()
mary.daily_activity()
とIndividualの性質を持ったBoyとGirlを定義しています。いちいちBoy用にeat_meatなどを定義しなくてもよい、というところが便利なところかと思います。
これをJuliaで実現する場合は、例えば、
abstract type Individual end
function eat_fruit(self::Individual)
self.energy += 1
return self
end
function eat_meat(self::Individual)
self.energy += 2
return self
end
function run(self::Individual)
self.energy -= 3
return self
end
mutable struct Boy <: Individual
energy
Boy(energy=10) = new(energy)
end
function daily_activity!(self::Boy)
eat_meat(self) |> eat_meat |> run |> eat_meat |> eat_fruit |> run |> eat_meat
println("boy's daily energy: $(self.energy)")
end
mutable struct Girl <: Individual
energy
Girl(energy=10) = new(energy)
end
function daily_activity!(self::Girl)
eat_meat(self) |> eat_fruit
println("girl's daily energy: $(self.energy)")
end
bob = Boy()
daily_activity!(bob)
mary = Girl()
daily_activity!(mary)
となります。先程のIndividualを抽象型に変更しました。これはIndividualを自分でいじれるようなコードであればよいですが、そうでない場合、Individualを変更しないPythonのような形で書けるでしょうか。一つの方法は、Individual型をフィールドに持たせ、
mutable struct Boy
indivisual::Individual
end
のようにすることです。これだと、Individualで定義した関数を使いたい場合には毎回A.indivisualのようにしなければならないですが、これは楽に定義する方法があります。こちらの記事を参考にしますと、MacroToolsというパッケージを使うと楽そうです。これは
import MacroTools
@MacroTools.forward Boy.individual eat_fruit
@MacroTools.forward Boy.individual eat_meat
@MacroTools.forward Boy.individual run
@MacroTools.forward Boy.individual get_energy
とします。全体としては、
mutable struct Individual
energy
Individual(energy=10) = new(energy)
end
function eat_fruit(self::Individual)
self.energy += 1
return self
end
function eat_meat(self::Individual)
self.energy += 2
return self
end
function run(self::Individual)
self.energy -= 3
return self
end
function get_energy(self::Individual)
return self.energy
end
mutable struct Boy
individual::Individual
end
import MacroTools
@MacroTools.forward Boy.individual eat_fruit
@MacroTools.forward Boy.individual eat_meat
@MacroTools.forward Boy.individual run
@MacroTools.forward Boy.individual get_energy
function Boy(energy=10)
individual = Individual(energy)
return Boy(individual)
end
function daily_activity!(self::Boy)
eat_meat(self) |> eat_meat |> run |> eat_meat |> eat_fruit |> run |> eat_meat
println("boy's daily energy: $(get_energy(self))")
end
mutable struct Girl
individual::Individual
end
function Girl(energy=10)
individual = Individual(energy)
return Girl(individual)
end
@MacroTools.forward Girl.individual eat_fruit
@MacroTools.forward Girl.individual eat_meat
@MacroTools.forward Girl.individual run
@MacroTools.forward Girl.individual get_energy
function daily_activity!(self::Girl)
eat_meat(self) |> eat_fruit
println("girl's daily energy: $(get_energy(self))")
end
bob = Boy()
daily_activity!(bob)
mary = Girl()
daily_activity!(mary)
となります。ここで、energyを手に入れる関数としてget_eneryを新しく定義しておきました。
どちらにせよ、Pythonのオブジェクト指向によく似た書き方になっているかと思います。ですので、元記事にあるような
また、主語, 動詞, 目的語の構造は比較的に理解しやすいです。上記の例では、まずeat_meat()、次にrun()という一連の動作が永遠に続いても理解できます。プロセス指向で実現するとboy_energy = eat_meat(boy_energy); boy_energy = run(boy_energy);...のような長文になるか、eat_meat(run(boy_energy))のような階層構造になるので、理解しにくくなるでしょう。
プロセス指向の問題はJuliaでは解決されていることがわかります。
まとめ
以上のように、オブジェクト指向でやれることは、Juliaにおいては、多重ディスパッチによってほぼ同じように実現されていることがわかります。