1
0

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を一から作成する その8 Python の変数と代入処理

Last updated at Posted at 2023-09-19

目次と前回の記事

前回のおさらい

前回の記事では、欠陥のあるゲーム盤を初期化するプログラムの問題点を説明するために、プログラム A と、プログラム B を紹介しました。

プログラム B は、欠陥のあるゲーム盤を初期化するプログラムと、同じ種類の処理 を行う、より簡単なプログラムです。

その後で、問題点を理解するために必要な知識として Python のデータとオブジェクト について説明しました。

欠陥のあるゲーム盤を初期化するプログラム

board = [[" "] * 3] * 3
board[1][0] = ""
print(board)

実行結果

[['〇', ' ', ' '], ['〇', ' ', ' '], ['〇', ' ', ' ']]

プログラム A

a = 5
b = a
a = 2
print(a)
print(b)

実行結果

2
5

プログラム B

a = [1, 2, 3]
b = a
a[0] = 2
print(a)
print(b)

実行結果

[2, 2, 3]
[2, 3, 3]

Python の変数と代入処理

プログラム A、B の 1 ~ 3 行目はいずれも 代入文 です。そのため、プログラム A、B で行われている処理を理解するためには、Python の代入文が行う 代入処理 について理解する必要があります。

プログラム A、B の 1 ~ 3 行目では、変数に「数値型のデータを代入する」、「list を代入する」、「別の変数を代入する」など、一見すると異なる種類の処理が行われているように 見える かもしれません。しかし、実際には Python の代入処理では、常に同じ種類の処理 が行われます。

今回の記事では、最初に Python の代入文で行われる処理を説明し、その後で プログラム A、B で記述されている代入文を具体例に、それぞれの代入文で行われる処理について解説し、どの代入文でも下記で説明する 同じ種類の処理 が行われていることを示します。

下記が、Python の代入文で行われる代入処理の説明です。

Python の代入文は、変数に = の右に記述された 式の計算結果 を管理する オブジェクトの id を格納する 処理である。

Python の変数に関するよくある誤解

代入文は、変数 に対して行われる処理なので、代入で行われる処理を理解するためには、Python の 変数の仕組み について理解する必要があります。

プログラム言語の変数は、プログラムが扱うデータを 格納 し、好きな時に 取り出して利用 できるという 入れ物 のような性質を持ちます。そのため、変数を「代入したデータを格納 するための 」で例えて説明することが良くあります。

この例えでは、代入文は「変数を表す箱に 代入するデータを直接入れて 格納する」という風に例えられます。また、変数の値を使って計算する場合は、「箱の中からデータを 直接取り出して」計算するという風に例えられます。

プログラム A の 1 行目の a = 5 という代入文で行われる処理を、この例えで図示すると、以下のようになります。なお、下図の図形や線は以下のような意味を持ちます。

  • 変数を表す箱を 直方体 の図形1で表現する
  • 直方体の上の文字で 変数の名前 を表す
  • 直方体の中の文字で 変数が格納するデータ を表す2
  • データの代入を 黒い矢印 で表現する
  • 青い図形の矢印の先の図でプログラムの 代入処理の結果 を表現する

C 言語などのプログラミング言語では、この「箱の中に、代入するデータを 直接入れて格納する」という例えで変数を説明するのは正しいのですが、Python ではこの例えは 正確ではありません。変数を 箱で例える こと自体は 間違っていない のですが、Python の変数は箱の中に、代入するデータを 直接格納しない という点が間違っています。もし Python の変数を上記のように理解していた場合は、認識を改めて下さい

「箱の中に、代入するデータを直接入れて格納する」という例えは Python の変数を説明する例えとしては 間違っている のですが、おそらく以下のような理由で実際に Python の変数の説明でも 使われることがある ようです。

  • この例えの説明のほうが直観的でわかりやすい
  • C 言語のように、この例えによる説明が正しいプログラム言語が存在するので、それらを学んだことがある人にとってもわかりやすい
  • Python の 数値型文字列型 など、一部のデータ型の場合は、この例えの説明でも変数の処理をうまく説明できてしまう

ただし、上記の間違った例えで Python の変数に関する処理を理解してしまうと、その誤解のせいで Python のプログラムを 正しく記述できなくなる 可能性があるため、正確な処理を理解しておくことは重要です。

数値型のリテラルの代入(プログラム A の 1 行目)

忘れている人がいるかもしれないので、前回の記事で説明した リテラル という用語と、オブジェクトの作成についておさらいします。

リテラル とは、5[1, 2, 3] のように、プログラムの中に直接記述されたデータ のことを表します。Python では、リテラルが記述された文を実行すると、そのリテラルを管理する 新しいオブジェクトが作成 されます。

Python の変数は、先程の図のように「変数に代入されたデータを直接格納」しているのではなく、「変数に代入されたデータを管理する オブジェクトの id」を格納しています。

プログラム A の 1 行目の a = 5 を実行すると、Python では以下のような処理が行われます。

  1. 5 はリテラルなので、数値型の 5 を管理する オブジェクトが新しく作られる
  2. 作成された オブジェクトの ida に格納する

下図は上記の処理を図示したものです。図の点線などの意味は以下の通りです。

  • オブジェクトの id を表す数字を 赤い文字 で表記する
  • 変数が格納する オブジェクトの id参照するオブジェクト赤い点線 で表記する

上図が示すように、代入処理によって変数が格納するのは、変数に代入した 5 という数値型のデータではなく、5 を管理するオブジェクトの id です。

このように、変数に代入されたデータは、その変数が直接格納して 管理していない ため、変数から 直接取り出す ことは できません

そのため、変数に代入されたデータは、以下の手順で、変数が格納するオブジェクトの id が 参照 するオブジェクトを介して 間接的に 取り出して利用します。

  1. 変数に格納された id を取り出す
  2. 取り出した id を持つオブジェクトを 探し出す
  3. 探し出したオブジェクトが 管理するデータ を取り出す

上図の変数の a に代入されたデータを取り出す手順は以下のようになります。

  1. a に格納された 123 というオブジェクトの id を取り出す
  2. 123 という id を持つオブジェクトを探し出す
  3. 探し出したオブジェクトが管理する 5 というデータを取り出して利用する

上記の オブジェクトの id に関する処理 は、Python が すべて自動で行ってくれる ので、「変数がデータを直接格納する」という間違ったイメージでプログラムを記述しても問題が発生しない場合は確かに存在します。しかし、そのようなイメージでプログラムを記述すると、「欠陥のあるゲーム盤を初期化するプログラム」のように 大きな問題が発生する場合がある ので、正しい認識を持つことは 非常に重要 です。

図の赤い点線が表す 参照概念的 なもので 実体があるわけではありません。例えば、本の中に「詳しくは〇〇という本を参照して下さい」という、他の本への 参照を表す文章 が書かれていたとしても、その文章から〇〇という本に向かって実体がある線で 結ばれることがない 点に似ています。

参照を表す識別子は、参照先のものを探すための 手掛かり にすぎないので それだけでは役に立ちません。識別子を元に、参照先のものを 探すための仕組み が必要となります。例えば外国のような 見知らぬ場所の住所 が書かれた紙があっても、それだけでは その住所の場所にたどり着くことは 不可能 です。住所を元に 場所を探す ための地図のような 道案内をするもの があってはじめて住所が意味を持つようになるのです。

なお、Python にはオブジェクトの id を元に、オブジェクトを 探す仕組みが備わっている のでその点は問題はありません。

まとめと用語の整理

ここまでの内容をまとめると以下のようになります。

  • Python の変数は、代入されたデータを 直接格納しない
  • Python の変数は、代入されたデータを管理する オブジェクトの id を格納する
  • Python の変数に代入されたデータは、変数に格納された id を介して 間接的に データを取り出して利用する

上記のまとめで説明したように、変数は以下の 2 種類のデータ を扱います

  • 変数が 直接 格納する「オブジェクトの id のデータ」
  • 変数が直接格納するオブジェクトの id を介して 間接的 に扱う、「変数に代入された データ 」

変数が扱うこの 2 種類のデータを 区別 するために、本記事では以下のように用語を使い分けることにします。

  • 変数が直接格納する オブジェクトの id のデータのことを、「変数に格納されたオブジェクトの id」、または単に「変数に格納された id」と表記する
  • 変数に代入されたデータのことをこれまで通り、「変数に代入された値」、または単に「変数の値」と表記する

また、変数が格納したオブジェクトの id が 参照 するオブジェクトの事を、「変数が参照するオブジェクト」のように表記します。

変数に代入されたデータのことを、「変数が参照する値」のように表記することがありますが、「変数が参照するオブジェクト」と 区別しづらい ので、本記事ではそのような表記は行わないことにします。

Python がデータを直接格納しない理由

Python の変数が、オブジェクトを介して 間接的に代入するデータを扱う ことが、分かりづらいと感じた人が多いかもしれません。また、変数に代入したデータを取り出して利用する際に、上記で説明したような 3 つの手順を行う必要があるため、わかりづらいだけでなく、効率が悪いというデメリットを感じた人も多いかもしれません。

しかし、変数に直接データを格納するようにすると、いくつかの デメリット が発生します。デメリットの原因の 1 つは、プログラムが扱う データの種類によって、データの サイズが大きく異なる 点にあります。例えば "abc" という文字列は、3 バイトのデータで表現できますが、1 のような整数は一般的に 4 バイト のデータで表現されます。また、画像や動画などのデータは、非常に小さいものは数バイト、大きいものは数ギガバイトのようにその大きさは千差万別です。そのため、変数が直接データを格納する場合は、データの種類によって 異なる大きさの入れ物 を用意する必要があります。具体例については割愛しますが、このことは大きなデメリットになります。

一方、データをオブジェクトで管理し、オブジェクトの id を変数に格納する場合、オブジェクトの id を表す データのサイズ を同じバイト数のデータで 統一する ことができます。そのため、どんな種類のデータを扱う変数であっても、同じ大きさのデータ を格納する 共通の入れ物 があれば良くなります。これはプログラムが変数を扱う際に、大きなメリットになります。例えば、Python では、同じ変数に任意のデータ型のデータを代入できますが、これは変数がオブジェクトの id でデータを扱っているからです。

現実世界の例では、郵便番号は 7 桁で、電話番号は 10 桁で統一されています。そのため、郵便番号や電話番号を記述する書類は、それらの記入欄を 7 桁または 10 桁分の数字が入るような大きさの欄で統一できます。

Python がデータを直接格納しない理由は他にもありますが、本記事でここまで説明した知識ではうまく説明できないので割愛します。

なお、変数にデータを直接格納することには、メリットもあります。例えばプログラムの処理速度の面では、変数に直接データを格納したほうが有利になります。そのため、プログラミング言語によって、変数がデータをどのように扱うかは異なります。

list のデータの代入(プログラム B の 1 行目) 

プログラム B の 1 行目の [1, 2, 3]リテラル です。そのため、a = [1, 2, 3] の処理は、以下のように、プログラム A の 1 行目の処理と 同じ種類の処理 が行われます。異なるのは下記の手順 1 で新しく作られるオブジェクトが管理するデータが、数値型の 5 であるか、list の [1, 2, 3] であるか だけ です。

  1. list の [1, 2, 3] を管理するオブジェクトが新しく作られる
  2. 作成された オブジェクトの ida に格納する

下図は上記のプログラム B の 1 行目の処理を表しています。その下の、プログラム A の 1 行目の処理を表す図と比べてみて、どちらも 同じ種類の処理 が行われていることを確認して下さい。

プログラム B の 1 行目では、実際には下図より複雑な処理が行われますが、分かりやすさを重視 して今回の記事では下記のような図にしました。詳細は次回の記事で説明します。

プログラム B の 1 行目の処理


プログラム A の 1 行目の処理

プログラム A と B の 1 行目では、数値型のデータと list を変数に代入しましたが、Python の 他のデータ型でも同じ種類の処理 が行われます。これは重要なことなので、「Python の代入処理で行われる処理」の最初に書いたことを再掲します。

Python の代入処理は、変数に = の右に記述された 式の計算結果を管理するオブジェクトの id を格納する 処理である。

上記のノートの説明内で、「式の計算結果を」と記述されている点が気になっている人がいるかもしれませんので補足します。

一般的に、式という用語から連想されるのは 1 + 2 のような、演算子を使って何らかの計算を行うものでしょう。しかし 5 のように 何の計算も行わない場合式の一種 です。

従って、a = 5 のような場合でも、5 なので、5 そのもの を式の計算結果とみなして代入処理が行われます。これは、次で説明する a = b のような変数を直接代入する場合でも同様です。

別の変数の直接代入(プログラム A、B の 2 行目)

プログラム A、B の 1 行目では、5[1, 2, 3] のようなプログラムに直接記述されたデータである リテラル を変数に代入していました。

それに対して、プログラム A、B の 2 行目の b = a は、別の変数を直接代入 するという代入処理です。前回の記事で説明したように、変数が記述された文を実行する場合は、その変数に代入された値を管理するオブジェクトは作成済なので、新しいオブジェクトが 作られることはありません。従って、b = a を実行すると、 a に代入されたデータを管理する オブジェクトの id が、b に格納されます。

プログラム A、B の 2 行目の処理は、次の 3 ~ 5 行目の実行結果から、a に代入されている データの種類 によって、データを複製3するかどうかが変わるように 見えます。そのため、プログラム A と B で異なる処理が行われるように見える原因を、以下のように理解している人が多いのではないでしょうか?しかし、この理解は 間違っています

  • プログラム A の 3 行目を実行した結果、ab に異なるデータが代入されるのは、2 行目の b = a の処理で、a に代入されていた数値型の 5複製されて b に代入されたことが原因である
  • プログラム B の 3 行目を実行した結果、ab に同じデータが代入されるのは、2 行目の b = a の処理で、a に代入されていた list の [1, 2, 3]複製されずに b に代入されたことによって 共有 されたことが原因である

これまで何度も述べてきたように、Python の代入文では、常に下記の処理が行われます。このことは、「変数に 別の変数を直接代入 する」という代入文でも変わりはありません。

Python の代入処理は、変数に = の右に記述された 式の計算結果を管理するオブジェクトの id を格納する 処理である。

「変数に別の変数を直接代入する」という代入文で行われる処理では、代入元の変数の値が どのようなデータ型 のデータであっても、「代入元の変数に格納された id を複製して、代入先の変数に格納する」という処理が行われます。その際に重要となるのは、代入するデータを複製しない ということです。

以上の事から、Python の代入処理は どのような場合であっても、以下のような性質を持ちます。

Python の代入処理の性質

  • オブジェクトの id という、オブジェクトへの 参照を複製 する
  • 代入する データは複製されない

実際に、プログラム A、B の 2 行目の b = a では、a には数値型のデータの 5 と、list の [1, 2, 3] という異なるデータ型が代入されていますが、どちらの場合でも、データそのものは 複製されず、オブジェクトの id という オブジェクトへの参照が複製 されます。この後で、実際にそのことを確認します。

プログラム A の 2 行目の処理

下図は、プログラム A の 2 行目の処理を図示したものです。図の右で ba別々に 同じオブジェクトの id のデータを格納していることから、a が格納していたオブジェクトの id が 複製されて b に格納されていることがわかります。また、図の右で 5 を管理するオブジェクトが 1 つかないことから、代入するデータそのものが 複製されていない ことがわかります。

b = a の処理を実行しても、a が参照するオブジェクトが変化することはありません。図からわかるように、結果として ba5 というデータを管理する 同一のオブジェクト を参照することで、同一のデータを共有 するようになります。

このことをプログラムで確認してみます。id という関数の () の中に、変数を直接記述 した場合は、その変数が 参照するオブジェクトの id が得られます。そのため、id を使って、異なる変数が 同じオブジェクトを参照 して データを共有している かどうかを 確認 することができます。

下記のプログラムは、プログラム A の 1、2 行目を実行した後で、ab が参照するオブジェクトの id を表示しています。実行結果から、ab が同じオブジェクトを参照してデータを共有していることがわかります。

a = 5
b = a
print(id(a))
print(id(b))

実行結果

140731432670120
140731432670120

プログラム B の 2 行目の処理

プログラム B の 2 行目でも下図のように 同じ種類の処理 が行われます。その下の、プログラム A の 2 行目の処理を表す図と見比べて、どちらの b = a も、ba同一のオブジェクト を参照し、同一のデータを共有 するようにする処理であることを確認して下さい。

プログラム B の 2 行目の処理

プログラム A の 2 行目の処理

また、下記のプログラムで先程と同様に、プログラム B の場合でも、ab が同じオブジェクトを参照してデータを共有していることを確認することができます。

a = [1, 2, 3]
b = a
print(id(a))
print(id(b))

実行結果

2414732444352
2414732444352

上記の結果から、以下のことがわかります。

Python で、変数に別の変数を直接代入 する処理は 代入するデータの種類に関わらず、代入元の変数の値を管理するオブジェクトの id を複製して、代入先の変数が格納することによって、2 つの変数が、同一のデータを共有する 処理である。

変数の上書き(プログラム A の 3 行目の処理)

下記のプログラム A の 3 行目の a = 2 では、1 行目 や 2 行目と異なり、既にデータが代入されている 変数 a に、別の値である 2 を代入して 上書き するという処理を行っています。

a = 5
b = a
a = 2

このような場合に、下図のように、a が参照するオブジェクトが管理するデータを 2 で直接上書きするような処理が行われると 誤解している 人がいるかもしれませんが、Python では下図のような処理は行われません

上図のような処理が行われない理由は、上図の処理が、『Python の代入文は、変数に = の右に記述された式の計算結果を管理する オブジェクトの id を格納する処理 である』という説明と 矛盾する からです。

また、仮に上図のような処理が行われてしまうと、他の変数の値まで同時に変わってしまう という、プログラム B と同様の問題 が発生してしまいます。その問題について図で説明します。

プログラム A では、2 行目の b = a で、b に値が代入されます。先程の図ではこの b を省略していましたが、b を加えて プログラム A の 3 行目の処理を図にすると、以下のようになります。

先程説明したように、プログラム A の 2 行目の b = a を実行すると、上図左のように ab が、同一 の数値型の 5 を管理する「id が 123 のオブジェクト」を参照して、データを 共有 するようになります。

この状態で、上図右のように、「id が 123 のオブジェクト」が管理するデータを 2 に書き換えてしまうと、a だけでなく、b の値も 5 から 2 に変化してしまいます。ab が同じデータを 共有 しているため、オブジェクトが管理するデータの内容を 直接書き替え てしまうと、ab の値が 同時に変化 してしまうのです。

大事なことなので何度も繰り返しますが、Python の代入で行われる処理は、代入先の変数にデータが既に代入されているか どうかに関わらず、下記の処理が行われます。

Python の代入文は、変数に = の右に記述された 式の計算結果 を管理する オブジェクトの id を格納する 処理である。

従って、プログラム A の 3 行目の a = 2 で行われる処理は、実際には下図のような処理が行われます。下図からわかるように、a が参照するオブジェクト だけ が、a に代入されたデータを管理するオブジェクトに変更されます。従って、a が参照するオブジェクトが 変更されてもb が参照するオブジェクトが 変わることはありません。そのため b = a を実行した後で a に別の値を代入した場合は、ab のデータの 共有が解除 され、ab に代入された値は 別々のデータ になります。

上記が、プログラム A の 2 行目の b = a によって、実際にはデータを 複製していない にも関わらず、データが 複製されたように見える 理由です。

プログラム A の 3 行目の処理

下図は a にデータが 代入されていない時a = 5 を実行する、プログラム A の 1 行目の処理です。上図と見比べて、a に値が代入されているかどうかに関わらず、a に関しては 同じ種類の処理が行われている ことを確認して下さい。

プログラム A の 1 行目の処理

ここまでの内容をまとめると以下のようになります。

  • 変数に別の変数を代入 する処理により、2 つの変数が 同じデータを共有する ようになる
  • 代入処理を行う前に、代入先の変数同じデータを共有する変数 があった場合は、代入処理によって 共有が解除 され、結果として「その変数」と「共有していた変数」の値が 異なるようになる

プログラム B の 3 行目の処理に対する錯覚

ここまでで、プログラム A が行う処理を順に説明し、プログラム A を実行した結果、ab別のデータが代入 される理由を説明しました。

一方、下記のプログラム B を実行すると実行結果から ab に 同じデータが代入されているように見えます。

a = [1, 2, 3]
b = a
a[0] = 2
print(a)
print(b)

実行結果

[2, 2, 3]
[2, 2, 3]

プログラム A の処理に対し、プログラム B でこのような奇妙な処理が行われているように見えるのは、おそらく以下のような 錯覚 が原因でしょう。

  • プログラム A と B は、1 行目で a に代入するデータの種類は違うが、2、3 行目では a2 という、全く同じ値代入 する式を記述しているので、2 行目と 3 行目で行う処理は同じ種類の処理である
  • それにも関わらず、3 行目を実行すると、プログラム A では ab には別々の値が代入されるが、プログラム B では ab に代入される値が同じになってしまうのは奇妙である。

上記はもっともらしく見えます。確かに、2 行目では、どちらも b = a という全く同じ式が記述されていますが、「3 行目 で行う処理は 同じ種類の処理 である」は 本当 でしょうか?プログラム A と B の 3 行目をよく見比べて下さい。プログラム A の 3 行目は a = 2 で、プログラム B の 3 行目は a[0] = 2 なので、よく見ると 異なるプログラム が記述されていることがわかります。つまり、プログラム A と B の 3 行目は、似ているように見える が、実際には 大きく異なる処理 を行っていることが 錯覚の原因 です。

プログラム A と B の 3 行目では、以下のような 異なる処理 が行われています。

  • プログラム A の 3 行目を実行すると、a 2 を管理するオブジェクトを参照するようになる
  • プログラム B の 3 行目を実行すると、a の 0 番の要素が 2 を管理するオブジェクトを参照するようになる

この違いを理解するためには、Python の list がどのように データを管理 しているかについて知る必要があります。その説明については次回の記事で行います。

プログラム B の修正

今回の記事では最後に、プログラム B の 3 行目を、プログラム A の 3 行目と 同じ種類の処理 を行うプログラムに 修正する 方法について説明します。

プログラム A の 3 行目は a = 2 のように a に直接データを代入 しています。従って、プログラム B の 3 行目で、プログラム A と同じ種類の処理を行う場合は、プログラム B の 3 行目を a = ??? のようなプログラムに修正しなければなりません(??? には何らかのデータが入ります)。

プログラム B では 1 行目で a[1, 2, 3] という list を代入したので、3 行目で a に代入するデータも list にする のが自然でしょう。従って、プログラム B の 3 行目を a = [4, 5, 6] のように記述すれば、プログラム A と同じ種類の処理が行われることになります。

下記のプログラムは、そのようにプログラム B を修正したものです。以後は、このプログラムを、プログラム C と表記します。

プログラム C

a = [1, 2, 3]
b = a
a = [4, 5, 6]
print(a)
print(b)
修正箇所
a = [1, 2, 3]
b = a
- a[0] = 2
+ a = [4, 5, 6]
print(a)
print(b)

実行結果

[4, 5, 6]
[1, 2, 3]

修正したプログラム C を実行すると、実行結果からわかるように、プログラム A と同様に ab異なるデータ が代入されます。

下図は、プログラム C の処理を図示したものです。その下のプログラム A の 3 行目の処理を表す図と見比べて、同じ種類の処理 が行われていることを確認して下さい。

プログラム C の 3 行目の処理


プログラム A の 3 行目の処理

プログラム B に関する錯覚についてまとめると以下のようになります

  • プログラム A、B の 2 行目が、A はデータの複製、B はデータの共有を行っているように見えるのは 錯覚 である
  • プログラム A、B の 2 行目は、いずれも ba同じデータを共有する ようにするという、同じ種類の処理 を行っている。
  • プログラム A と プログラム B の 3 行目が同じ種類の処理を行うに見えるのは 錯覚 である
  • a = 2a[0] = 2 は似ているが、行われている処理は全く異なるものである
  • プログラム B の 3 行目を a = [4, 5, 6] のように、a に直接 データを代入するように修正することで、プログラム A と B の 3 行目が、ab のデータの 共有を解除 して、異なる値を持つようにするという、同じ種類の処理 を行うようになる

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

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

次回の記事

  1. 適当な箱のフリーの画像が見つからなかったので、直方体の画像で代用しました

  2. 図の左の直方体は、5 が代入される前の a を表しているので、直方体の中には何も表示していません

  3. コピー(copy)と呼ぶことの方が多いかもしれませんが、本記事では以後は 複製 で統一します

1
0
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?