トラック
/
Python
Python
/
演習
/
エレンの宇宙人ゲーム
エレンの宇宙人ゲーム

エレンの宇宙人ゲーム

学習演習

はじめに

Pythonのオブジェクト指向プログラミング

関数型、宣言型、命令型のスタイルでプログラミングしてきた人なら、オブジェクト指向プログラミング(OOP)に視点を移すのは、少しとっつきにくく感じるかもしれません。 オブジェクト指向のアプローチでは、問題を、互いにやり取りし、状態を保ち、データに働きかける1つ以上のobjectsとしてモデル化することを考えます。 オブジェクトは、現実世界の実体(車や猫など)を表すこともできますし、もっと抽象的な概念(整数や乗り物、哺乳類など)を表すこともできます。 それぞれのオブジェクトは、コンピューターメモリーの中で一意のインスタンスとなり、モデル全体の一部を表します。

クラス

Classesは新しいオブジェクトの型の定義であり、そこからオブジェクトの新しいinstancesが作られます。 多くの場合、データと、そのデータを操作するコードや関数をひとまとめにします。 この意味で、クラスは_設計図_であり、同じような型の多くのオブジェクトを作って使うための手順の集まりでもあります。 複雑なプログラムにはたくさんのクラスがあり、それぞれがさまざまな種類のオブジェクトを作ります。 クラスからオブジェクトを作るプロセスをinstantiation(クラスのインスタンスを作ること)といいます。

Pythonでのクラスの定義はシンプルです。

class MyClass:
    # Class body goes here

クラスの属性

クラスの本体には、クラスのフィールド(別名properties、attributes、data members、variables)を追加できます。

class MyClass:
    number = 5
    string = "Hello!"

MyClassのインスタンス(オブジェクト)は、クラスを呼び出すことで作成し、名前に束縛できます(関数を呼び出すのと同じ方法です)。

>>> new_object = MyClass()

# Class is instantiated and resulting object is bound to the "new_object" name.
# Note: the object address 'at 0x15adc55b0' will vary by computer.
>>> new_object
<__main__.MyClass at 0x15adc55b0>

Class attributesは、クラスから作られたすべてのオブジェクト(またはインスタンス)で共有され、dot notationでアクセスできます。これは、オブジェクト名の後、属性名の前に.を置く書き方です。

>>> new_object = MyClass()

# Accessing the class attribute "number" via dot-notation.
>>> new_object.number
5

# Accessing the class attribute "string" via dot-notation.
>>> new_object.string
'Hello!'

# Instantiating an additional object and binding it to the "second_new_object" name.
>>> second_new_object = MyClass()

>>> second_new_object
# Note: the object address "at 0x15ad99970" will vary by computer.
<__main__.MyClass at 0x15ad99970>

# Second_new_object shares the same class attributes as new_object.
>>> new_object.number == second_new_object.number
True

クラスの属性は、ほかのどのメソッドよりも先に、クラス本体の中で定義します。 これらはクラスが所有するもので、クラスのインスタンス間で共有できます。 これらの属性は共有されているので、その値にはクラスから_直接_アクセスして操作できます。 クラスの属性の値を変えると、_そのクラスからインスタンス化されたすべてのオブジェクト_の値が変わります。

>>> obj_one = MyClass()
>>> obj_two = MyClass()

# Accessing a class attribute from an object.
>>> obj_two.number
5

# Accessing the class attribute from the class itself.
>>> MyClass.number
5

# Modifying the value of the "number" class attribute.
>>> MyClass.number = 27

# Modifying the "number" class attribute changes the "number" attribute for all objects.
>>> obj_one.number
27

>>> obj_two.number
27

たくさんのオブジェクトが常に同期したデータを持っている状態は、あまり役に立ちません。 幸いなことに、クラスから作られたオブジェクトは、それぞれ独自のinstance attributes(インスタンスプロパティ、変数、フィールドともいいます)でカスタマイズできます。 名前のとおり、インスタンス属性は各オブジェクトに固有のもので、独立して変更できます。

__init__()でオブジェクトのインスタンス化をカスタマイズする

特別な「ダンダーメソッド」(「double underscore method」の略)である__init__()は、クラスのインスタンスをカスタマイズするために使われ、オブジェクトのインスタンス属性やプロパティを初期化できます。 インスタンス属性を初期化する役割から、__init__()はclass constructorやinitializerとも呼ばれます。 __init__()はselfという必須の仮引数を1つ受け取ります。これは新しく初期化、作成されたオブジェクトを指します。 インスタンス属性やプロパティのデータは、self仮引数のあとに続けて、__init__()の引数として渡せます。

以下のMyClassには、location_xとlocation_yというインスタンス属性があります。 ご覧のとおり、この2つの属性には、__init__()に渡されたlocation(タプル)引数の1番目と2番目のインデックスが代入されています。 これで、クラスをインスタンス化してオブジェクトを作ると、そのクラスのインスタンスのlocation_xとlocation_y属性が初期化されます。

class MyClass:
    # These are class attributes, variables, or fields.
    number = 5
    string = "Hello!"

    # This is the class "constructor", with a "location" parameter that is a tuple.
    def __init__(self, location):

        # This is an instance or object property, attribute, or variable.
        # Note that we are unpacking the tuple argument into two separate instance variables.
        self.location_x = location[0]
        self.location_y = location[1]

# Create a new object "new_object_one", with object property (1, 2).
>>> new_object_one = MyClass((1, 2))

# Create a second new object "new_object_two" with object property (-8, -9).
>>> new_object_two = MyClass((-8, -9))

# Note that new_object_one.location_x and new_object_two.location_x two different values.
>>> new_object_one.location_x
1

>>> new_object_two.location_x
-8

上のMyClassを初期化するときに渡す引数は1つだけでよいことに注意してください。クラスを呼び出すと、Pythonがselfを渡してくれます。

メソッド

methodは、クラス自体(クラスメソッドと呼ばれ、後の演習で扱います)か、クラスの_インスタンス_(オブジェクト)に束縛されたfunctionです。 オブジェクト(インスタンス)を操作するメソッドは、最初の仮引数をselfとして定義する必要があります。 残りの仮引数は、通常の、束縛されていない関数と同じように定義できます。

class MyClass:
    number = 5
    string = "Hello!"

    # Class constructor.
    def __init__(self, location):
        # Instance properties
        self.location_x = location[0]
        self.location_y = location[1]

    # Instance method. Note "self" as first parameter.
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

属性へのアクセスと同じように、メソッドを呼び出すには、オブジェクト名の後、メソッド名の前に.を置くだけです。 呼び出されたメソッドは、最初の引数としてオブジェクトのコピーを受け取る必要はありません。Pythonがselfを自動的に埋めてくれます。

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

# Make a new test_object with location (3,7)
>>> test_object = MyClass((3,7))
>>> (test_object.location_x, test_object.location_y)
(3,7)

# Call change_location to increase location_x and location_y by 7.
>>> test_object.change_location(7)
(10, 14)

クラスの属性は、クラスの外からアクセスするのと同じように、インスタンスメソッドの中からもアクセスできます。

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

    # Alter class variable number for all instances from within an instance.
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1


>>> test_object_one = MyClass((0,0))
>>> test_object_one.number
5

>>> test_object_two = MyClass((13, -3))
>>> test_object_two.increment_number()
>>> test_object_one.number
6

passで実装を仮置き、またはスタブする

これまでのコンセプト演習や、プラクティス演習のスタブでは、実際のコードの代わりに関数の本体の中でpassキーワードが使われているのを見たことがあるでしょう。 passキーワードは文法的に有効なプレースホルダーです。空の関数やクラス定義に対してPythonが構文エラーを出すのを防いでくれます。 要するに、Pythonのインタプリターに「心配しないでください! ここにはいずれ_必ず_コードを書きます。まだ書いていないだけです」と伝える方法です。

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return  self.location_x, self.location_y

    # Alter class variable number for all instances
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1

    # This will compile and run without error, but has no current functionality.
    def pending_functionality(self):
       # Stubbing or placholding the body of this method.
       pass

説明

Ellenは、プレイヤーがエイリアンと戦うゲームを作っています。 彼女はオブジェクト指向プログラミング(OOP)を学んだばかりで、classesを使うことで自分のプログラムに何をもたらせるのか、ぜひ活用してみたいと思っています。

そこで手伝いを申し出ると、Ellenは大喜びで、プレイヤーが戦うエイリアンのプログラミングという任務を任せてきました。

1. Alienクラスを作成しましょう

二つのパラメーター<x_coordinate>と<y_coordinate>を受け取るコンストラクターを持つAlienクラスを定義し、それらをインスタンス変数x_coordinateとy_coordinateに格納します。 エイリアンはどの個体も体力が3の状態から始まるので、health変数もあわせて初期化します。

>>> alien = Alien(2, 0)
>>> alien.x_coordinate
2
>>> alien.y_coordinate
0
>>> alien.health
3

これで、各エイリアンは自分の位置と体力を内部で管理できるようになります。

2. hitメソッド

Ellenは、Alienclassにhitメソッドを持たせたいと考えています。呼び出されたときに、エイリアンオブジェクトの体力を1減らすものです。 そうすれば、エイリアンの体力を手作業で書き換えなくても、<alien>.hit()を呼び出すだけで済みます。 hit()が体力を0_まで_減らすのか、0_より下_まで減らすのかは、あなた次第です。

>>> alien = Alien(0, 0)

# Initialized health value.
>>> alien.health
3

# Decrements health by 1 point.
>>> alien.hit()
>>> alien.health
2

3. is_aliveメソッド

体力が減り続ければ、いつかは0に(それ以下にだって!)なるでしょう。 そこで、エイリアンが…その…生きているかどうかをすぐに確認できるis_aliveメソッドを追加しておくといいでしょう。😉 <alien>.is_alive()は真偽値を返すようにします。

>>> alien.health
1
>>> alien.is_alive()
True
>>> alien.hit()
>>> alien.health
0
>>> alien.is_alive()
False

4. teleportメソッド

Ellenのゲームでは、エイリアンはテレポートできます! 新しいx_coordinateとy_coordinateの値を受け取り、それに応じてエイリアンの座標を変更するteleportメソッドを書く必要があります。

>>> alien.teleport(5, -4)
>>> alien.x_coordinate
5
>>> alien.y_coordinate
-4

5. collision_detectionメソッド

当然ながら、エイリアンが何かに当たることがあるなら、そうした衝突が起きたことを検知できる必要があります。 とはいえ、衝突検知のアルゴリズムは厄介になりがちで、その実装方法をまだ知りません。 Ellenは後で自分がやると言っていますが、機能を作り込むときの備忘として、collision_detectionメソッドをクラスに置いておきたいとも思っています。 何らかの変数(おそらくは別のオブジェクト)を受け取る必要がありますが、それだけしかわかっていません。 メソッド定義をクラスに書いても、呼び出したときにエラーが起きないようにしておく必要があります。

>>> alien.collision_detection(other_object)
>>>

6. エイリアンのカウンター

Ellenが新たなお願いを持って戻ってきました。 ゲームが続く間に何体のエイリアンが作られたかを記録しておきたいのだそうです。 スコアシステムと何か関係があるのだとか。

例えば、次のようになります。

>>> alien_one = Alien(5, 1)
>>> alien_one.total_aliens_created
1
>>> alien_two = Alien(3, 0)
>>> alien_two.total_aliens_created
2
>>> alien_one.total_aliens_created
2
# Accessing the variable from the class directly
>>> Alien.total_aliens_created
2

7. エイリアンの配列を作成しましょう

Ellenはここまでの成果を気に入っていますが、もう一つお願いがあります。 位置の配列(tuplesとして)を受け取って、Alien()オブジェクトのlistを作る、独立した(Alien()クラスの外にある)関数がほしいと思っています。

例えば、次のようになります。

>>> alien_start_positions = [(4, 7), (-1, 0)]
>>> aliens = new_aliens_collection(alien_start_positions)
...
>>> for alien in aliens:
...     print(alien.x_coordinate, alien.y_coordinate)
(4, 7)
(-1, 0)
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Python Exercism

エレンの宇宙人ゲームを始める準備はできましたか?

Exercismに登録すれば、17個のコンセプト146個の演習、そして本物の人間によるメンタリングとともに、Pythonを学んでマスターできます。すべて無料です。