0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その21 クラスの属性

Last updated at Posted at 2023-10-22

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤の全てのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

前回までのおさらい

前回の記事では、dict とオブジェクトの名前空間について説明し、その後でクラスとインスタンスの仕組みについて説明しました。

クラスの属性

これまでのプログラムでは、クラスの定義の中で、メソッドの定義のみを記述 してきましたが、クラスの定義の中で、クラスのローカル変数に値を代入する という処理を 記述する ことで、クラスの属性 を定義することが出来ます。

前回の記事 の「クラスの定義で行われる処理は、クラスを作成 し、そのクラスが 管理 する属性やメソッドなどの 名前 から、名前に代入されたデータを管理するオブジェクトを 対応づける ための 名前空間を作成 するという処理です」という説明を思い出してください。

クラスの定義を実行 した際に、クラスのローカル変数値を代入 することで、クラスのローカル名前空間 にその 名前が登録 されます。クラスの定義の実行が終了した時点で、クラスのローカル名前空間 は、クラスの名前空間 として 利用される ようになるので、クラスの ローカル変数の名前 が、そのままクラスの 属性の名前 になります。

クラスの定義の中で、クラスの ローカル変数に値を代入 するという処理は、クラスの属性値を代入 する処理と 同じ意味 を持ちます。

具体的な記述例については少し後で説明します。

「クラスのローカル名前空間」と「クラスの名前空間」の違い

「クラスのローカル名前空間」と、「クラスの名前空間」は、名前が似ていますが、異なる場面と方法 で使われます。その違いが良くわからなくなっている人が多いかもしれないので、その違いについ補足します。

クラスのローカル名前空間は、クラスの定義を実行する際に作成 されるもので、クラスの定義の ブロックの中 に記述された 変数名 や、メソッド名 などを 管理 します。クラスのローカル名前空間は、クラスの定義実行されてから終了するまでの間 で利用されます。クラスの定義の実行中名前解決 を行う際に、クラスのローカル名前空間に名前が見つからなかった場合は、通常の名前解決で行われる手順と同様に、グローバル名前空間組み込み名前空間 の順で名前を探します。

クラスの名前空間は、クラスの定義の実行が終了した時点で、クラスのローカル名前空間そのまま引き継がれて利用 されます。クラスの名前空間は、クラスが管理するデータなので、クラスの定義を実行した後いつでも利用 することが出来ます。クラスの名前空間は、クラスの名前.属性名 のような記述が行われた際に利用されます。クラスの名前空間に名前が見つからなかった場合は、AttributeError という エラーが発生 します。グローバル名前空間やローカル名前空間を探しに行くことは ありません

前回の記事のノートでも言及しましたが、まだ説明していないクラスの継承が行われている場合や、クラスの名前空間に登録されない特殊属性などで、クラスの名前空間に名前が見つからなかった場合でもエラーが発生しない場合がありますが、基本的にはエラーが発生する と考えて良いでしょう。

下記の表は上記の説明をまとめたものです。

クラスのローカル名前空間 クラスの名前空間
対象となる名前 クラスの定義のブロックの中の変数名やメソッド名 クラス名. の後に記述された名前
スコープ クラスの定義のブロックの中 クラスの定義を実行した後
名前が見つからなかった場合 グローバル名前空間、組み込み名前空間の順で名前を探す エラーが発生する

クラスの属性の使い道

クラスの属性 は、全てのインスタンス共通するデータ扱う 場合に利用します。

逆に言うと、すべてのインスタンスで共通するデータが 存在しない 場合は、クラスの属性を利用する 必要はありません

Marubatsu クラスを具体例に挙げて説明します。Marubatsu クラスでは、ゲーム盤に配置する マーク文字列で表現 しており、マークを表す文字列は、すべての Marubatsu クラスのインスタンスで 共通するデータ です。例えば、Marubatsu クラスのインスタンスによって、〇 を表す文字列が異なるようなことはありません1。従って、Marubatsu クラスの属性 にマークを表す文字列を代入して利用することができます。

クラスの属性を使う利点

クラスの属性を利用する利点がピンとこない人が多いかもしれません。そこで、今回の記事では Marubatsu クラスを具体例に挙げて、「プログラムの修正が容易になる」と、「データの意味が分かりやすくなる」の 2 つの利点について説明します。

実際には、__init__ メソッド内で インスタンスの属性 にマークを表す文字列を代入して利用することでも、これから説明する利点を得ることが出来ますが、インスタンスの属性は本来は インスタンスごとに異なる ようなデータを代入する ためのもの なので、すべてのインスタンスで共通する データは クラスの属性を利用 したほうがプログラムの 意味が分かりやすくなる でしょう。

プログラムの修正が容易になる

Marubatsu クラスの定義では、最初は 空白のマスを表す文字列を 半角の空白文字 で表現していましたが、後から半角の "." に変更 するという 修正 を行いました。同様に、〇 や × のマークを表す文字列も 全角文字 から 半角文字変更 しました。

プログラムでこのような 変更 を行うと、変更した内容に 関連するプログラムすべて修正 しないと バグが発生 してしまいます。実際に 過去の記事 で、空白のマスを表す文字列を半角の空白文字から半角の "."変更 した際に、place_mark で行う処理を 修正し忘れた ため、バグが発生 しました。

このような問題は、データを 直接 プログラムに 記述 するの ではなくわかりやすい名前変数や属性 にデータを 代入 し、その 変数や属性 をプログラムに 記述する という方法で 改善 することが出来ます。

データの意味が分かりやすくなる

プログラムにデータを直接記述するのではなく、分かりやすい名前の属性にデータを代入し、その名前をプログラムに記述することで、プログラムのデータの意味が分かりやすくなるという利点が得られます。

Marubatsu クラスの場合は、空白、〇、× のマスを表す 文字列 を、分かりやすい名前 のクラスの 属性に代入 し、その属性を使って プログラムを 記述 します。

クラスの属性の記述方法

上記の利点の具体例を説明するために必要な知識として、クラスの属性の記述方法について説明します。

クラスの属性への値の代入

一般的に、クラスの定義の中 で、クラスの属性に値を代入 する処理は、クラスの メソッドの定義を記述する前に記述 します(理由は後述します)。Marubatsu クラスの場合は、下記のように記述します。

class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"

    def __init__(self):
        # 以下略

属性の名前 は、空を表す EMPTY、〇 を表す CIRCLE、× を表す CROSS にしました。また、マスのマークを表す文字列は、〇×ゲームの 実行中に変化 することは ありません。これらの属性の 名前を全て大文字 にしたのは、python では、プログラムの 実行中に値が変化しない ような変数や属性の 名前大文字半角の _(アンダースコア) のみで記述 するという 慣習がある からです。

プログラム言語によっては、一度値を代入すると、別の値を代入できなくなる 定数 と呼ばれる仕組みを持つものがあります。残念ながら Python には 定数 の仕組みは ありません ので、上記のように 名前を全て大文字 にしても、別の値を代入 することが できてしまいます。ただし、名前を全て大文字にすることで、その名前の変数や属性の値を 変えるべきではない ことが 明確 になります。間違って 別の値を代入するという処理を 記述しにくくなる という利点が得られるので、この慣習は 積極的に利用したほうが良い でしょう。

クラスの定義の中 では、属性の名前self.属性名 ではなく変数名と同様名前だけを記述 する点に 注意して下さい。その理由についてはこの後で説明します。

上記のプログラムで、self.EMPTY = "." のように記述すると、self という 名前の変数定義されていない ので エラーが発生 します。self を利用できる のは、仮引数 self が記述 されている メソッドのブロックの中だけ です。

self特別 な名前 ではない ので、self = 1 のような代入文を実行することで、self に任意のデータを代入することが出来ます。ただし、self という 名前 をメソッドを呼び出したインスタンスを代入する 以外の目的 で利用すると プログラムの意味が分かりにくくなる ので、避けたほうが良い でしょう。

メソッドの定義のブロックでのクラスの属性の記述

次に、クラスの メソッドの定義のブロックの中 で、空白のマスを表す "." の部分 を、対応する クラスの属性置き換える必要 があります。

現時点ではクラスの定義の中に 〇 と × を表すデータは 記述されていない ので CIRCLECROSS 属性に関する 修正 を行う必要はは ありません

メソッドの定義の中クラスの属性を記述 する場合は、Marubatsu.EMPTY のように、クラスの名前.属性の名前の前記述する必要 があります。その理由についてはこの後で説明します。

下記が修正したプログラムです。変更箇所の確認は、下記の修正箇所をクリックして下さい。

class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"

    def __init__(self):
        self.initialize_board()

    def initialize_board(self):
        self.board = [[Marubatsu.EMPTY] * 3 for y in range(3)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        for y in range(3):
            for x in range(3):
                print(self.board[x][y], end="")
            print()      
修正箇所
class Marubatsu:
+   EMPTY = "."
+   CIRCLE = "o"
+   CROSS = "x"

    def __init__(self):
        self.initialize_board()

    def initialize_board(self):
-       self.board = [["."] * 3 for y in range(3)]
+       self.board = [[Marubatsu.EMPTY] * 3 for y in range(3)]

    def place_mark(self, x, y, mark):
-       if self.board[x][y] == ".":
+       if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        for y in range(3):
            for x in range(3):
                print(self.board[x][y], end="")
            print()      

クラスの属性 は、クラスの定義の中 では以下のように記述する。

  • メソッドの定義の ブロックの外 では、属性名のみ を記述する
  • メソッドの定義の ブロックの中 では、クラスの名前.属性名 を記述する

クラスの属性の記述に関する補足説明

メソッドの 定義の外 でクラスの属性を記述する場合は、EMPTY = "." のように、属性名だけ を記述すれば良いのに、メソッドの 定義の中 では Marubatsu.EMPTY のように クラスの名前を記述する 必要がある点を不思議に思う人が多いかもしれません。

この違いを理解するためには、メソッドの定義の外 に記述されたプログラムと、メソッドの定義の中 に記述されたプログラムの 違い について、正しく理解する必要があります。ただし、その違いを理解するために新しいことを覚える必要はありません。

メソッドは、関数と同様の性質 を持つので、メソッドの定義を実行した際には、関数の定義を実行した際と 同様の処理 が行われます。従って、過去の記事 で説明したように、メソッドの定義を実行した際に、メソッドの名前名前空間に登録 されますが、その際にメソッドの定義の ブロックの中 に記述されたプログラムは 実行されません。メソッドの定義のブロックの中に記述されたプログラムは、関数の定義のブロックと同様に、メソッドを呼び出した際に実行 されます。

一方、クラスの定義の中で、メソッドの定義の外 に記述されたプログラムは、クラスの定義を実行した際に、実際に 実行されます

クラスの定義を実行 した際に、以下のような処理が行われる。

  • メソッドの定義の外に記述されたプログラムは 実行される
  • メソッドの定義の中に記述されたプログラムは 実行されない

Marubatsu クラスの定義を実行 した際に行われる処理は以下のようになります。

下記の 2 ~ 4 行目の 3 つの代入文は メソッドの外 に記述されているので 実行され、3 つの変数の名前が Marubatsu クラスのローカル名前空間に登録されます。

6、7 行目のような、メソッドの定義実行され__init__ というメソッドの 名前Marubatsu クラスの ローカル名前空間に登録 されますが、その際に 7 行目のような、メソッドの中 に記述されているプログラムは、Marubatsu クラスの定義を実行した際に 実行されません。これは関数の定義を実行した際に、関数の定義のブロックの中のプログラムが実行されないのと同様です。

1  class Marubatsu:
2      EMPTY = "."
3      CIRCLE = "o"
4      CROSS = "x"
5
6      def __init__(self):
7          self.initialize_board()
8
9      以下略

下図は、Marubatsu クラスの定義を実行した後で、Marubatsu クラスの定義の ブロックの処理がすべて終了した時点 の状況を表しています。図からわかるように、Marubatsu クラスの ローカル名前空間 に、3 つの変数名 と、3 つのメソッドの名前 が登録され、それぞれの名前に対して オブジェクトが対応づけ られています。

上記で「3 つの変数」のように表記したのは、この時点では、Marubatsu クラスはまだ 作成されていない ので、EMPTY などは属性ではなく、ローカル変数 だからです。EMPTY などがクラスの属性になるのは、クラスが 作成された後 です。

クラスの定義を実行 すると、クラスの定義のブロック に記述されたプログラムが 実行 され、その後クラスが作成 されます。クラスの名前 がグローバル 名前空間に登録 されるのは、クラスが作成された時 なので、図からわかるように、この時点では クラスの名前 はグローバル名前空間に 登録されていません

先程説明したように、クラスの定義を実行 した際に、クラスのブロックの中で行われる名前解決は、通常の名前解決の手順と同様 に、「クラスのローカル名前空間」、「グローバル名前空間」、「ビルトイン名前空間」の順で行われます。図からわかるように、Marubatsu という名前はどの名前空間にも登録されていないので、Marubatsu.EMPTY のような記述を実行すると、Marubatsu という名前の解決を行うことが出来ないのでエラーが発生します。これが、メソッドの 定義の外 でクラスの属性を記述する場合は、EMPTY = "." のように、属性名だけ を記述する理由です。

下図は、Marubatsu クラスが 作成 され、その名前が グローバル名前空間に登録 された直後の状況を表しています。先ほどの図の Marubatsu のローカル名前空間がそのまま Marubatsu の名前空間として 流用 されています。Marubatsu という名前がグローバル名前空間に登録された結果、今後は いつでも Marubatsu.CROSS のように記述することで、Marubatsu クラスの名前空間を利用 することが出来るようになります。

メソッドの定義のブロックに記述されたプログラムは、メソッドが呼び出された際に実行 されますが、メソッドは以下のような順番で、インスタンスから 呼び出されます

  1. クラスの定義を実行する
  2. クラスからインスタンスを作成する
  3. 作成したインスタンスからメソッドを呼び出す

上記の順番からわかるように、メソッドが呼び出された時点では、クラスの定義実行済 なので、その クラスの名前 は、グローバル名前空間に登録 されています。

また、メソッドは関数と同様の性質を持つので、メソッドが呼び出された際に、メソッドのブロックの中では、その メソッドのローカル名前空間 が作られます。

従って、メソッドのブロックの中で、EMPTY のような記述を行うと、メソッドのローカル名前空間、グローバル名前空間、組み込み名前空間の順で名前解決が行われますが、この名前はこの 3 つのどの名前空間にも登録されていないので、エラーが発生します。

一方、Marubatsu という名前は グローバル名前空間に登録 されており、EMPTY という名前は Marubatsu クラスの 名前空間に登録 されています。従って、メソッドのブロック内で Marubatsu クラスの EMPTY 属性は Marubatsu.EMPTY のように記述できます。

クラスの定義を実行した際に、クラスのローカル名前空間に EMPTY という名前が登録されるので、その名前空間を使えば EMPTY の名前解決を行うことが出来ると思う人がいるかもしれません。しかし、クラスのローカル名前空間と、メソッドのローカル名前空間は 異なる名前空間 です。また、メソッドのブロックの中の名前解決で使われる名前空間は、メソッドのローカル名前空間、グローバル名前空間、組み込み名前空間の 3 種類で、クラスのローカル名前空間は 使われません

前回の記事で説明するのを忘れましたが、__init__ メソッドの定義のブロックの中から、initialize_board メソッドを呼び出す際に、self.initialize_board() のように記述する必要がある理由は、上記で説明した理由と同様です。

下記のプログラムのように、クラスのブロックの中で グローバル変数グローバル関数組み込み関数 に対する 処理を行う ことができます。

a = [1]

def add(x, y):
    return x + y

class A:
    # クラスの定義のブロックの中で、グローバル変数 a の要素に値を代入する
    a[0] = 2
    # クラスの定義のブロックの中で、グローバル関数 add と組み込み関数 print を利用する
    print(add(1, 2))

# クラスの定義の実行により、グローバル変数 a の値が変化する
print(a)

実行結果

3
[2]

クラスのブロックの中で グローバル関数組み込み関数 を利用しても基本的には 問題は発生しません が、グローバル変数に対して上記の a[0] = 2 のような処理を実行すると、クラスの定義の実行によってグローバル変数の値が変化する(副作用が発生する)ことになるので、特別な理由がない限り、そのような処理は記述しないほうが良いでしょう。これは、過去の記事で説明した 関数の副作用 と同じなので、忘れた方は復習して下さい。

クラスの属性の代入を、メソッドの定義より前に記述する理由

クラスの属性に値を代入 する処理を、クラスの メソッドの定義の前 に記述する理由は、プログラムをわかりやすくする ためです。具体例を挙げて説明します。

下記のプログラムは、クラスの属性 X1 を代入し、クラスの属性 X を表示する printX メソッドを持つ A というクラスを定義しています。

class A:
    X = 1

    def printX():
        print(A.X)

上記のプログラムを、下記のプログラムのように、X = 1メソッドの定義の後 に記述するように修正しても 全く同じクラス が作成されますが、下記のプログラムでは、X = 1 がメソッドの定義の後に記述されているので、上から順番 にこのプログラムを 見た際 に、3 行目の print(A.X) で行われる 処理の意味 が、その後の 5 行目で記述された X = 1見るまで分かりません

一方、上記のプログラムの場合は、5 行目の print(A.X) より 前に X = 1 が記述されているので、上から順番 にプログラムを見た際に、5 行目で行われる処理の 意味が理解しやすくなる という利点が得られます。

class A:
    def printX():
        print(A.X)

    X = 1

動作の確認

先程修正した Marubatsu クラスが 正しく動作 することを 確認 します。

下記のプログラムでは、1 行目で Marubatsu クラスのインスタンスを作成し、2 行目で place_mark メソッドを呼び出して (0, 0) のマスに 〇 を配置し、3 行目で mb.display_board() を呼び出してゲーム盤を表示しています。実行結果から、プログラムが正しく動作することが確認できます。

mb = Marubatsu()
mb.place_mark(0, 0, "o")
mb.display_board()

実行結果

o..
...
...

クラスの属性の利点の具体例

利点その 1:マークを表すデータをわかりやすく記述できる

マークを表す文字列を、クラスの属性に代入することで、次のような、マークを表すデータをわかりやすく記述できる という利点が得られます。

例えば、Marubastu クラスの事を よく知らない人mb.place_mark(0, 0, "o") というプログラムを見たときに、"o" が小文字のオーではなく、〇 というマークを表す文字列だとすぐに 理解する ことは 困難 だと思いませんか?一方、下記のプログラムの 2 行目の Marubatsu.CIRCLE のような記述であれば、少なくとも 〇(circle)に関する データを実引数に記述しているということを 推測する ことは容易でしょう。

実行結果から、正しいマスに 〇 のマークが配置されることが確認できます。

mb = Marubatsu()
mb.place_mark(0, 0, Marubatsu.CIRCLE)
mb.display_board()
修正箇所
mb = Marubatsu()
- mb.place_mark(0, 0, "o")
+ mb.place_mark(0, 0, Marubatsu.CIRCLE)
mb.display_board()

実行結果

o..
...
...

同様に、initialize_board メソッドの中に記述された [["."] * 3 for y in range(3)]半角の "."(ピリオド)が空白を表す文字列であることを 理解する ことは 困難 ですが、[[Marubatsu.EMPTY] * 3 for y in range(3)] という記述であれば、Marubatsu.EMPTY空白(empty) を表すデータであることを 容易に推測 できるでしょう。

また、クラスの属性を利用することで、〇 を表す文字列が何であるかを 覚える必要がなくなる という利点を得ることができます。もちろん、〇 を表す文字列が代入された 属性名を覚える必要 はありますが、そのことは 覚えやすい属性名 をつけたり、docstring を記述する などの方法である程度は解決することが出来ます。

利点その 2:プログラムの変更が容易になる

クラスの属性を利用 するプログラムを記述することで、クラスの定義の中の、EMPTY などの 属性に代入する文字列を変更 する だけ で、マスを表す文字列簡単に変更 することが出来ます。例えば、空白のマス を表す文字列を 半角の "-"(マイナス) に変更したい場合は、下記のように EMPTY代入する値を変更 する だけ で、他の部分 は一切 変更する必要はありません

class Marubatsu:
    EMPTY = "-"
    # 略

クラスの定義の実行後のクラスの属性への代入

クラスの属性を変更 または 追加 するたびに、クラスの定義を全て記述 しなおして実行するのは 大変 です。既に定義 された クラスの属性変更 または 追加 は、下記のプログラムのように、クラス名.属性名値を代入 することで簡単に行うことが出来ます。実行結果から、1 行目の処理によって、空白のマスを表す文字列が "-" に変化することを確認できます。

Marubatsu.EMPTY = "-"
mb = Marubatsu()
mb.display_board()

実行結果

---
---
---

利点その 3:数値データの意味が分かりやすくなる

利点その 1 で マークを表す文字列 をクラスの属性に代入したように、一般的にプログラムの中で何らかの 意味のあるデータ変数や属性に代入 することで、利点 が得られる場合があります2。特に、意味のある 数値データ を変数や属性に代入することで利点が得られる 場合が多い ので、具体例を挙げてそのことを説明します。

例えば、Marubatsu クラスの中では、3 という数値頻繁に記述 されています。この数値は、ゲーム盤の縦横のサイズ を表す数値です。現時点では記述したプログラムが短いので、プログラム内に記述された 3 が何を表す数値であるかがわからなくなることは無いかもしれませんが、プログラムの量が増える と、様々な数値 がプログラムの中に 記述されるようになります

また、プログラムの中で 同じ数値が異なる意味 で記述されることが良くあります。実際に initialize_board メソッド内の for x in range(3)3 はゲーム盤の の数を、for y in range(3)3 はゲーム盤の の数を表す数値で、同じ 3 であってもその 意味は異なっています。そのような場合に、後からゲーム盤の の数を変更するには、数多くの 3 という数字の中から、列を表す 3 だけを探し出して変更する という 大変な作業 が必要になります。

このように、プログラムの中に記述した 3 のような数値何を表すかを理解 することが 困難 になる場合が良くあります。そこで、〇×ゲームのゲーム盤のサイズを クラスBOARD_SIZE という 属性に代入 し、その 属性を使って プログラムを 修正 することにします。先ほど説明したように、定義済の クラス への 新しい属性の追加 は、下記のプログラムのように 代入文を記述 するだけで行えます。

Marubatsu.BOARD_SIZE = 3

〇×ゲームのゲーム盤は 正方形 なので、BOARD_SIZE という一つの属性を使うことにしましたが、長方形 のゲームの場合は、横のサイズを BOARD_WIDTH、縦のサイズを BOARD_HEIGHT で表すように、2 つの属性 が必要となります。

クラスの定義の実行後のメソッドの修正

次に、ゲーム盤のサイズを表す 3 が記述 されている メソッドを修正 する必要がありますが、メソッドを修正するたびに クラスを定義し直すのは大変 です。属性の場合と同様 に、メソッドの修正や追加 も、メソッドの処理を行う関数を定義 してから、クラスの属性 に定義した 関数を代入する ことで行うことが出来ます。過去の記事 の、変数に関数を代入 することで、その 変数が、代入した関数と同じ機能を持つ ようになるという説明を思い出してください。

下記は、3 が記述されている initialize_board メソッドと、display_board メソッドを修正したプログラムです。このように修正することで、プログラムの記述が少し長くなるという欠点はありますが、それを上回る プログラムの意味が分かりやすくなる という利点が得られます。

def initialize_board(self):
    self.board = [[Marubatsu.EMPTY] * Marubatsu.BOARD_SIZE for y in range(Marubatsu.BOARD_SIZE)]

def display_board(self):
    for y in range(Marubatsu.BOARD_SIZE):
        for x in range(Marubatsu.BOARD_SIZE):
            print(self.board[x][y], end="")
        print()  

Marubatsu.initialize_board = initialize_board
Marubatsu.display_board = display_board
修正箇所
def initialize_board(self):
-   self.board = [[Marubatsu.EMPTY] * 3 for y in range(3)]
+   self.board = [[Marubatsu.EMPTY] * Marubatsu.BOARD_SIZE for y in range(Marubatsu.BOARD_SIZE)]

def display_board(self):
-   for y in range(3):
+   for y in range(Marubatsu.BOARD_SIZE):
-       for x in range(3):
+       for x in range(Marubatsu.BOARD_SIZE):
            print(self.board[x][y], end="")
        print()  

下記は、上記で修正した Marubatsu クラスが正しく動作しているかどうかを確認するプログラムです。実行結果から、正しく動作していることを確認することが出来ます。

mb = Marubatsu()
mb.place_mark(0, 0, Marubatsu.CIRCLE)
mb.display_board()

実行結果

o--
---
---

プログラムの修正が容易になるという利点

〇×ゲームは 3x3 のゲーム盤で行うゲームなので、このような修正を行う必要はありませんが、マークを表す文字列を属性に代入した場合と同様に、ゲーム盤のサイズを表す数値BOARD_SIZE 属性に代入 することで、下記のプログラムのように、他のプログラムを 一切変更することなく、ゲーム盤のサイズを 5x5 に 変更 することが出来ます。

Marubatsu.BOARD_SIZE = 5

mb = Marubatsu()
mb.display_board()

実行結果

-----
-----
-----
-----
-----

クラスの属性の代入に関する注意点

クラスの定義を実行した後で、クラスの属性やメソッド に対して 代入処理を行う ことで 属性やメソッド修正や追加 を簡単に行うことが出来ますが、修正したクラスを モジュールとして保存 し、他のファイルから利用する際には、修正した部分を反映 した クラスの定義 をモジュールの中に 記述 する必要があります。

記事の最後にリンクで示す github の marubatsu_new.py には、その回の 記事の修正を反映したクラスの定義 を記述します。

クラスとインスタンスの属性の使い分け

クラスの属性を使うか、インスタンスの属性を使うかは、その 属性の値 がすべてのインスタンスで 共通するかどうか によって以下のように 使い分ける 必要がある。

  • 全てのインスタンスで 共通するデータ は、クラスの属性 に代入する
  • 個々のインスタンスで 独立するデータ は、インスタンスの属性 に代入する

例えば、インスタンスによって、ゲーム盤のサイズを変更 できるようにしたい場合は、ゲーム盤のサイズを表す 属性 は、インスタンスの属性 にする必要があります。

インスタンスの作成時の実引数を使ったインスタンスの属性の設定

インスタンスの 作成時 に、実引数を使って インスタンスの 属性の値を設定 したい場合は、__init__ メソッドの仮引数 を使います。例えば、Marubatsu クラスで、インスタンスの 作成時ゲーム盤のサイズを設定 したい場合は下記のプログラムのように Marubatsu クラスの定義を修正します。なお、今回は、Marubatsu クラスの定義からの修正箇所がわかるようにするために、クラスを定義し直すことにします。

class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"

    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
        self.initialize_board()

    def initialize_board(self):
        self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        for y in range(self.BOARD_SIZE):
            for x in range(self.BOARD_SIZE):
                print(self.board[x][y], end="")
            print() 
修正箇所
class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"
-   BOARD_SIZE = 3

-   def __init__(self):
+   def __init__(self, board_size=3):
+       self.BOARD_SIZE = board_size
        self.initialize_board()

    def initialize_board(self):
-       self.board = [[Marubatsu.EMPTY] * Marubatsu.BOARD_SIZE for y in range(Marubatsu.BOARD_SIZE)]
+       self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
-       for y in range(Marubatsu.BOARD_SIZE):
+       for y in range(self.BOARD_SIZE):
-           for x in range(Marubatsu.BOARD_SIZE):
+           for x in range(self.BOARD_SIZE):
                print(self.board[x][y], end="")
            print() 

上記のプログラムは、下記の部分を修正しています。

  • BOARD_SIZE 属性クラスの属性 から、インスタンスの属性変更 したため、クラスの BOARD_SIZE 属性 への代入を 削除 する
  • __init__ メソッドの仮引数 に ゲーム盤のサイズを代入する board_size3 を追加 する
  • __init__ メソッドのブロックの中で、インスタンスの BOARD_SIZE 属性その値を代入 する。なお、ゲームの 開始後 にゲーム盤のサイズを 変更することはない ので、属性の名前は大文字と半角の _ のみの名前にした
  • 仮引数 board_sizeデフォルト値が 3デフォルト引数 にすることで、実引数を記述せずに にインスタンスを作成すると、3x3 のゲーム盤が作成される ようにする
  • BOARD_SIZE 属性クラスの属性 から、インスタンスの属性変更 したため、Marubatsu.BOARD_SIZE を、self.BOARD_SIZE に修正 する

下記のプログラムは、2 行目で 実引数を記述せずMarubatsu クラスのインスタンスを作成しているので、__init__ メソッドの 仮引数 board_sizeデフォルト値である 3 が代入 されて 3x3 のゲーム盤が作成され、3 行目でその表示が行われます。

一方、5 行目では 実引数に 5 を記述 して Marubatsu クラスのインスタンスを作成しているので 5x5 のゲーム盤が作成され、6 行目でその表示が行われます。

1  print("3x3 のゲーム盤")
2  mb = Marubatsu()
3  mb.display_board()
4  print("5x5 のゲーム盤")
5  mb = Marubatsu(5)
6  mb.display_board()
行番号のないプログラム
print("3x3 のゲーム盤")
mb = Marubatsu()
mb.display_board()
print("5x5 のゲーム盤")
mb = Marubatsu(5)
mb.display_board()

実行結果

3x3 のゲーム盤
...
...
...
5x5 のゲーム盤
.....
.....
.....
.....
.....

〇×ゲームでは、ゲーム盤のサイズを変更する必要はありませんが、せっかくなのでゲーム盤の サイズを変更できるようにしたまま 実装を進めていくことにします。

クラスの属性に対する docstring

クラスの定義に記述する docstring で、クラスの属性と、インスタンスの属性を区別して表記する方法を調べたのですが良くわからなかったので、本記事では下記のように記述することにします。

    Attributes:
        クラス属性
        EMPTY (str):
            空のマスを表す文字列
        (略)
            
        インスタンス属性
        BOARD_SIZE (int):
            ゲーム盤の縦横のサイズを表す整数
        (略)

値を変更するクラスの属性の利用例

ここまでの説明では、値を変更しない ような クラスの属性使用例 を紹介しましたが、値を変更する ようなクラスの属性の使用例についても紹介します。

例えば、クラスの属性を利用 することで、クラスから 作成したインスタンスの数数える ことができます。

下記のように、作成したインスタンスの数を数える ための count という名前の 属性Marubatsu クラスに追加 します。クラスの定義を実行 した時点では、インスタンスは 作成されていない ので、count には 0 を代入しています。この属性は、値が変化する ので 名前は小文字 にしています。

Marubatsu.count = 0

クラスから インスタンスを作成 した場合は、__init__ メソッドが呼び出される ので、__init__ メソッドの中 でこの count 属性の値に 1 を足す という処理を記述します。変数に値を加算 する処理は、下記のプログラムの 2 行目のように、+= という演算子を利用 することが出来ます。

1  def __init__(self, board_size=3):
2      Marubatsu.count += 1    # Marubatsu.count の値に 1 を加算する
3      self.BOARD_SIZE = board_size
4      self.initialize_board()
5
6  Marubatsu.__init__ = __init__
行番号のないプログラム
def __init__(self, board_size=3):
    Marubatsu.count += 1    # Marubatsu.count の値に 1 を加算する
    self.BOARD_SIZE = board_size
    self.initialize_board()

Marubatsu.__init__ = __init__
修正箇所
def __init__(self, board_size=3):
+   Marubatsu.count += 1    # Marubatsu.count の値に 1 を加算する
    self.BOARD_SIZE = board_size
    self.initialize_board()

上記の修正によって、Marubatsu クラスから インスタンスを作成するたび に、Marubatsu クラスの count 属性の値が 1 ずつ増えていく ようになります。下記のプログラムの実行結果から、そのことを確認することが出来ます。

mb1 = Marubatsu()
print(Marubatsu.count)
mb2 = Marubatsu()
print(Marubatsu.count)

実行結果

1
2

なお、〇×ゲームの実装を行う際に、Marubatsu クラスから作成したインスタンスを数える必要は特にないので、作成したインスタンスを数える処理は、github にアップロードする今回の記事の marubatsu_new.py には 反映しません

クラスの属性のインスタンスからの利用

クラスの属性と 同じ名前の属性 がインスタンスに 存在しない 場合は、インスタンスから クラスの 属性を利用 することが出来ます。先ほどのプログラムで mb1mb2 に代入された、Marubatsu クラスから作成された インスタンス には、いずれも count 属性が存在しない ので、下記のプログラムのように、mb1.countmb2.count を表示すると、いずれも クラスの count 属性 の値である 2 が表示されます。

先程のプログラムで、mb1 = Marubatsu() を実行した直後に print(Marubatsu.count) を実行すると 1 が表示されるので、mb1.count には 1 が代入されていると 勘違いする 人がいるかもしれませんが、countクラスの属性 なので、どのインスタンスから count 属性を表示しても 常に同じ数値 が表示されます。

print(mb1.count)
print(mb2.count)

実行結果

2
2

このようなことが起きる理由は、インスタンスの名前空間属性名が登録されていない 場合は、インスタンスからクラスのメソッドを利用する際と 同様 に、クラスの名前空間 から 名前を探す からです。

また、見つかった名前に対応づけられたオブジェクトが、関数オブジェクトでない場合 は、見つかったオブジェクトが そのまま 名前に 対応づけられます

前回の記事 で説明したように、見つかった名前に対応づけられたオブジェクトが 関数オブジェクトの場合 に、メソッドオブジェクトが作られるという特殊な処理が行われるのは、インスタンスからメソッドを呼び出す際に、最初の仮引数 self にメソッドを呼び出したインスタンスを代入するという、通常の関数呼び出しでは行われない 特殊な処理が必要 となるからです。

関数オブジェクト でない 場合は、そのような特殊な処理は 必要ではない ので、見つかったオブジェクトがそのまま名前に対応付けます。

だだし、このように クラスの属性インスタンスの属性を記述して利用 すると、プログラムの 意味が分かりづらくなる ので 避ける ことを個人的には お勧めします

特に、インスタンスとクラスに 同じ名前の属性がある 場合に、プログラムで行われる処理が 非常にわかりづらくなる ことがあります。

具体例を挙げます。下記のプログラムは、2 行目で Marubatsu クラスの インスタンスの CIRCLE 属性 を、3 行目で、Marubatu クラスの CIRCLE 属性を 表示しています。Marubatsu クラスの インスタンス には、CIRCLE 属性は存在しない ので、どちらも Marubatsu クラスの CIRCLE 属性の値 である "o" が表示されます。

一方、5 行目で、Marubatsu クラスの インスタンスの CIRCLE 属性"丸" を代入した後の 6、7 行目で、2、3 行目と同じ処理 を行うと、実行結果からわかるように、2、3 行目とは 異なる表示 が行われます。

1  mb = Marubatsu()
2  print(mb.CIRCLE)
3  print(Marubatsu.CIRCLE)
4
5  mb.CIRCLE = ""
6  print(mb.CIRCLE)
7  print(Marubatsu.CIRCLE)
行番号のないプログラム
mb = Marubatsu()
print(mb.CIRCLE)
print(Marubatsu.CIRCLE)

mb.CIRCLE = ""
print(mb.CIRCLE)
print(Marubatsu.CIRCLE)

実行結果

o
o
丸
o

上記のプログラムをぱっと見た際に、多くの人は、2 行目の mb.CIRCLEMarubatsu クラスの CIRCLE 属性を指すのだから、5 行目で、mb.CIRCLE"丸" を代入すると、Marubatsu クラスの CIRCLE 属性に "丸" が代入されると 勘違いする人が多い のではないでしょうか?

実際には 5 行目の処理によって、インスタンスに それまで存在しなかった CIRCLE 属性が 新しく作られる ことになります。その属性とクラスの同じ名前の属性は 異なる属性 なので、5 行目の処理によってクラスの CIRCLE 属性の値は 変化しません

これは、「関数やメソッドの ブロックの中変数に値を代入 すると ローカル変数 に値が 代入される こと」、「同じ名前 のローカル変数とグローバル変数が 異なる変数 であること」に似ています。

上記のような 紛らわしい処理 は、プログラムの バグの原因になる可能性が高い ので、避けたほうが良い でしょう。

クラスとインスタンスの 属性名 に関する 注意点 は以下の通りです。

  • クラスの属性同じ名前 を、インスタンスの属性つけない ほうが良い

  • クラスの属性 は、クラスの名前.属性名 のように 記述したほうが良い

プログラムの書き方に関しては、人によって さまざまな主張 があります。例えば、Marubatsu.CROSS のような記述では、Marubatsu という名前は グローバル名前空間 が管理する名前なので、メソッドのブロック内の ローカル名前空間のスコープ記述しないほうが良い という主張があります。これは 過去の記事 での「関数のブロックの中に、ローカル変数のみを記述することで、関数のブロックの中の変数が見分けやすくなる」という説明と同様の主張です。

個人的には、クラスの名前の 頭文字を大文字で記述 することで、クラスの名前 であるかどうかが 簡単に区別できる などの理由から、メソッドのブロックの中で Marubatsu.CROSS のような記述を行っても問題はないと思います。

ただし、この手の主張には、一般的に 一長一短 があるものなので、どの主張が正しいかを 一概に断言する ことは できません。興味がある方はそれぞれの主張を調べて比べてみて、自分が良いと思ったほうの主張を採用して下さい。

メソッドの種類

属性に、クラスの属性とインスタンスの属性の 2 種類 があるように、メソッドには インスタンスメソッドクラスメソッド静的メソッド3 種類 があります。

インスタンスメソッド

これまでの例で、クラスの定義の中で記述したメソッドは、インスタンスから呼び出し て利用してきました。このように、インスタンスから呼び出して利用 するメソッドの事を インスタンスメソッド と呼びます。インスタンスメソッドは以下のような特徴を持つメソッドです。

  • 最初の仮引数 self に、メソッドを呼び出した インスタンスが代入 される
  • 一般的に、メソッドを呼び出した インスタンスが管理する属性 に対する 処理 が行われる

本記事では、単にメソッドと記述 した場合は、インスタンスメソッドの事を表す ことにします。

クラスメソッド

クラスメソッドクラスから呼び出される メソッドで、以下のような特徴を持ちます。

  • メソッドの定義の直前の行に @classmethod を記述する
  • 最初の仮引数 cls に、メソッドを呼び出した クラスが代入 される
  • 一般的に、メソッドを呼び出した クラスが管理する属性 に対する 処理 が行われる

実際には、クラスメソッドはインスタンスから呼び出すこともできます。そのような場合に行われる処理については、この後で具体例を挙げて説明します。

メソッドの定義の直前の行に記述する @classmethod は、デコレータ4 と呼ばれるもので、関数やメソッドに対して何らかの 機能を付け加える(飾り付ける(decorate))という機能を持ちます。@classmethod はその直後で定義したメソッドを クラスメソッドにする という機能を持ちます。

クラスメソッドの使用例

クラスメソッドの使用例として、先ほどの「インスタンスを作成した数を数える」処理を クラスメソッドで行う 例を紹介します。先ほどの例では、__init__ メソッドのブロックの中 でクラスの属性 count1 を足すことで作成したインスタンスの数を数えていましたが、今回の例では、その処理をクラスメソッドで定義することにします。また、今回の例では それに加えて インスタンスを 作成した際 に、作成したインスタンスの数を表す メッセージを表示する ことにします。

下記のプログラムでは、9 行目に @classmethod を記述 することで、10 ~ 14 行目で add_count という クラスメソッドを定義 しています。クラスメソッドの 最初の仮引数 には、メソッドを呼び出した クラスが代入 されます。また、その仮引数の名前は cls という名前を付ける5 慣習 があります。12、14 行目のように cls.count を記述することで、クラスcount 属性 に対する処理を行うことが出来ます。

cls.count の代わりに、Marubatsu.count と記述しても 全く同じ処理 が行われますが、クラスメソッドでは、cls.count と記述したほうが良いでしょう。

このクラスメソッドを、__init__ メソッドの中の 20 行目 から呼び出すことで、インスタンスを作成するたびに、その数を数えて、メッセージを表示するという処理が行われます。

 1  class Marubatsu:
 2      EMPTY = "."
 3      CIRCLE = "o"
 4      CROSS = "x"
 5      count = 0
 6
 7      # クラスメソッド `add_count` を定義する
 8      # 最初の仮引数 cls に、このメソッドを呼び出したクラスが代入される
 9      @classmethod
10      def add_count(cls):
11          # クラスの count 属性の値に 1 を足す
12          cls.count += 1
13          # メッセージを表示する
14          print(cls.count, "個目のインスタンスを作成しました")
15
16      # イニシャライザの中で `add_count` を呼び出すように修正した
17      def __init__(self, board_size=3):
18          self.BOARD_SIZE = board_size
19          self.initialize_board()
20          Marubatsu.add_count()  # クラスメソッド add_count を呼び出す
21
22      def initialize_board(self):
23          self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
24
25      def place_mark(self, x, y, mark):
26          if self.board[x][y] == Marubatsu.EMPTY:
27              self.board[x][y] = mark
28          else:
29              print("(", x, ",", y, ") のマスにはマークが配置済です")
30
31      def display_board(self):   
32          for y in range(self.BOARD_SIZE):
33              for x in range(self.BOARD_SIZE):
34                  print(self.board[x][y], end="")
35              print()
行番号のないプログラム
class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"
    count = 0

    # クラスメソッド `add_count` を定義する
    # 最初の仮引数 cls に、このメソッドを呼び出したクラスが代入される
    @classmethod
    def add_count(cls):
        # クラスの count 属性の値に 1 を足す
        cls.count += 1
        # メッセージを表示する
        print(cls.count, "個目のインスタンスを作成しました")

    # イニシャライザの中で `add_count` を呼び出すように修正した
    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
        self.initialize_board()
        Marubatsu.add_count()  # クラスメソッド add_count を呼び出す

    def initialize_board(self):
        self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        for y in range(self.BOARD_SIZE):
            for x in range(self.BOARD_SIZE):
                print(self.board[x][y], end="")
            print()
修正箇所
class Marubatsu:
    EMPTY = "."
    CIRCLE = "o"
    CROSS = "x"
    count = 0

    # クラスメソッド `add_count` を定義する
    # 最初の仮引数 cls に、このメソッドを呼び出したクラスが代入される
+   @classmethod
+   def add_count(cls):
+       # クラスの count 属性の値に 1 を足す
+       cls.count += 1
+       # メッセージを表示する
+       print(cls.count, "個目のインスタンスを作成しました")

    # イニシャライザの中で `add_count` を呼び出すように修正した
    def __init__(self, board_size=3):
        self.BOARD_SIZE = board_size
        self.initialize_board()
-       Marubatsu.count += 1
+       Marubatsu.add_count()  # クラスメソッド add_count を呼び出す

    def initialize_board(self):
        self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]

    def place_mark(self, x, y, mark):
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")

    def display_board(self):   
        for y in range(self.BOARD_SIZE):
            for x in range(self.BOARD_SIZE):
                print(self.board[x][y], end="")
            print()

下記のプログラムの実行結果から、インスタンスを作成するたびに、何回クラスメソッドを作成したかを表すメッセージが表示されることが確認できます。

mb1 = Marubatsu()
mb2 = Marubatsu()

実行結果

1 個目のインスタンスを作成しました
2 個目のインスタンスを作成しました

インスタンスからのクラスメソッドの呼び出し

インスタンスに クラスメソッドと同じ名前の属性存在しない 場合は、下記のプログラムのように、インスタンスからクラスメソッドを呼び出すことが出来ます。

mb1.add_count()

実行結果

3 個目のインスタンスを作成しました

インスタンスからクラスメソッドを呼び出した場合は、そのインスタンスを 作成したクラスから クラスメソッドを呼び出した場合と 同じ処理 が行われます。

従って、クラスメソッドをクラスから呼び出した場合でも、インスタンスから呼び出した場合でも、最初の 仮引数 cls には 必ずクラスが代入 されます。

インスタンスからクラスメソッドを呼び出した場合は、見た目から インスタンスメソッドを呼び出しているか、クラスメソッドを呼び出しているかの 区別がつかない ので、クラスメソッドはクラスから呼び出す ようにすることを お勧めします

静的メソッド

クラスの定義の中で、クラスの属性と、インスタンスの属性の いずれに対しての処理を行わない メソッドを定義することが出来ます。そのようなメソッドの事を 静的メソッド と呼び、以下のような性質を持ちます。

  • メソッドの定義の直前の行に @staticmethod を記述する
  • 最初の仮引数 に特別な値は 代入されない通常の関数呼び出しと同様の処理 が行われる
  • クラスやインスタンスの 属性影響を与えない が、クラスに関連する処理 が行われる

現時点では Marubatsu クラスに対して静的メソッドを定義すると便利な良い例が思いつかないので、具体例は示しません。何か静的メソッドを使う良い例が出てきた場合に具体例を説明しようと思います。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu_new.py です。記事の中でも説明しましたが、作成したインスタンスの数を数える処理は、marubatsu_new.py には記述していません。

次回の記事

  1. 実際には、インスタンスによって 〇 のマーク表す文字列を変えるようなプログラムを記述することは不可能ではありませんが、そのようなプログラムを記述する利点はないでしょう

  2. とはいえ、すべてのデータを変数や属性に代入するのはさすがにやりすぎでしょう。利点が大きいと思ったデータを変数や属性に代入すると良いでしょう

  3. 仮引数の名前を全て大文字にすることはあまりないと思いますので、小文字にしました

  4. デコレータは自作することもできます。デコレータの自作については、必要になった時点で紹介します

  5. class という名前にしないのは、class という名前が クラスの定義 を行う際に 別の用途で使われる からです

0
1
1

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?