カテゴリー
アーカイブ

04.19
2024

[python] Pythonでオブジェクト指向を学ぶのは危険と言う話~第一話~

  • LINE

今回はPythonのクラス変数とインスタンス変数について、個人的に罠にハマった件をご紹介していきます。
俺もハマった!と言う同志に読んでいただけると幸いですm(_ _)m

ハマったきっかけ

元々PHP畑出身のエンジニアだった私がPHPのノリで↓のようなクラスを書きました
※本当はもっと複雑なクラスだったのですが、状況を伝えやすくするために簡素化してますm(_ _)m

class Hoge:
    fuga = []

    def __init__(self, val):
        self.fuga.append(val)

    def print_fuga(self):
        print(self.fuga)

よし!クラスの準備ができたし、インスタンス化しますか〜
一つは初期値に fuga = [1] を設定して、もう一つは fuga = [2] を初期値に持たせてインスタンス化しよう!

fuga1 = Hoge(1)
fuga2 = Hoge(2)

よし、じゃあ一旦中身を表示するか

fuga1.print_fuga()
fuga2.print_fuga()
[1,2]
[1,2]

「!!!!????へ?」

PHPだと↓のようクラスを作ったつもりなのにどうして!?

<?php
class Hoge
{
    public $fuga = [];

    public function __construct($fuga=1) {
        $this->fuga[] = $fuga;
    }

    public function p() {
        var_dump($this->fuga); 
    }
}
$h1 = new Hoge(1);
$h2 = new Hoge(2);

$h1->p();
$h2->p();
?>
array(1) {
  [0]=>
  int(1)
}
array(1) {
  [0]=>
  int(2)
}

PHPならインスタンス毎に $this->fuga はそれぞれインスタンス毎の設定として値をもてるのにPythonはダメなのか...!?

よし!色々試して検証してみよう!

検証

試しに Hoge.fuga にリテラル値を設定して試してみよう

class Hoge:
    fuga = 1

    def __init__(self, val):
        self.fuga = val

    def print_fuga(self):
        print(self.fuga)
fuga1 = Hoge(1)
fuga2 = Hoge(2)
fuga1.print_fuga()
fuga2.print_fuga()

↓出力結果

1
2

あれ?リテラルはOK?
オブジェクトだとダメ?__init__ で self.fuga を初期化しているから大丈夫なのかな?

じゃあ、リストに戻して __init__ で初期化したらいけるのかな?

class Hoge:
    fuga = []

    def __init__(self, val):
        self.fuga = val

    def print_fuga(self):
        print(self.fuga)
fuga1 = Hoge([1])
fuga2 = Hoge([2])
fuga1.print_fuga()
fuga2.print_fuga()

↓出力結果

[1]
[2]

おぉ、__init__ で初期化するとPHPのメンバ変数と同じような使い方ができるのか
どうせ__init__ で初期化するならクラス直下で初期化宣言いらないじゃん...

じゃあ、やりたいことは↓で書けば十分なのかな

class Hoge:
    def __init__(self, val: list):
        self.fuga = val

    def print_fuga(self):
        print(self.fuga)

fuga1 = Hoge([1])
fuga2 = Hoge([2])

fuga1.print_fuga()
fuga2.print_fuga()

↓出力結果

[1]
[2]

大丈夫そう、__init__ で初期化するとはいえ、動的にプロパティを生やすようなやり方がちょっとやだなぁ

結論

調べたら、どうらや Python にはクラス変数とインスタンス変数なるものがある模様

class Hoge:
    fuga = "クラス変数"
    def __init__(self, val):
        self.val = val  # インスタンス変数

クラス変数 : すべてのインスタンス間で共通した値をもつ変数
インスタンス変数 : 個々のインスタンスに格納される変数

と言うことらしい。

まぁ、__init__ で初期化されれば、クラス変数として定義していたとしてもインスタンス変数となる動きなので、なんだか微妙に感じてしまう

少しだけ Python のことが嫌いになりました

もうちょろっと検証

一つの仮説を立てて、検証してみた

仮説

クラス変数とインスタンス変数は同名でつけられるだけで、別物なんじゃあないか?を試してみた

検証

クラス変数 fuga を設定しつつ、 __init__ で self.fuga を初期化して、初期化前後のオブジェクトIDを表示してみる

class Hoge:
    fuga = []

    def __init__(self):
        print(id(self.fuga))
        self.fuga = []
        print(id(self.fuga))

hoge = Hoge()

↓出力結果

132982011249984
132982368452480

オブジェクトIDが変わっている!やっぱり別物になっていたんだ!

念の為、__init__の初期化を append に置き換えて実行してみる

class Hoge:
    fuga = []

    def __init__(self):
        print(id(self.fuga))
        self.fuga.append(1)
        print(id(self.fuga))

hoge = Hoge()

↓出力結果

132982366564928
132982366564928

完全に一致!

仮説は証明された

つまり、クラス変数と同名のインスタンス変数 fuga を設定(__init__で初期化)すると、 self.fuga で アクセスした場合にインスタンス変数が優先して参照される仕組みとなっている模様

結論

Pythonでオブジェクト指向を学ぶのは危険!

プログラミング初学者が最初にPythonから入る場合には十分注意してほしい
他言語でオブジェクト指向に則った class の使い方を覚えることをお勧めします

ただ、Pythonは覚えやすく、ライブラリも豊富な良き言語なので、今後に期待しておりますm(_ _)m

次回は引き続き、クラス変数という存在とPythonでのclassという概念が、
実は「いわゆる一般的なクラス」という概念と一線を画すのでは?ということも検証してみようと思います。