見出し画像

【python3】vars()でクラスの中身を書き出すと、参照渡しになる問題

これに気付かなくて丸二日くらいハマってた……

結論:vars()で辞書にした後forで回し直して辞書作りなおす

class Test:
    def __init__(self) -> None:
        self.hoge = "hoge"
        self.huga = "huga"

    def to_dict(self) -> dict:
        temp = vars(self)
        # こうするとインスタンス変数まで書き換わってしまう
        temp["huga"] = "HUGA"

        # 新しくnew_dictを作って、tempの中身を一度取り出して入れ直す
        new_dict = {}
        for key,val in temp.items():
            new_dict[key] = val

        # 一度作り直せば、new_dictの方で値を更新してもインスタンス変数に影響しない
        new_dict["huga"] = "hugahuga"
        return new_dict

あまりやり方がスマートじゃないので、もっと良い方法があったら教えてください。

クラスの中身を辞書形式で書き出したいからvers()しよう

こちらの記事でまとめたやつです。
とんだ落とし穴がありました。

参照渡しされてやがる……!

class Test:
    def __init__(self) -> None:
        self.hoge = "hoge"
        self.huga = "huga"
    def to_dict(self) -> dict:
        return vars(self)

ざっくりこういうクラスを作って、自身の持つ変数を辞書形式で返却するメソッドを作って置くじゃないですか。

で、to_dictメソッドで書きだした辞書をそのまま使うなら問題ないんですが、何かの都合で、
・to_dict内でvars()した後、何かを変更(後でjsonにするためにDateをstrにするとか)してreturnする
・to_dictから返却された辞書の値を、後から変更する
等を行うと、大元のクラスのインスタンス変数が変わります。

参照渡しって何?と言う方はググって下さいなんですが、大雑把に言えば「変数Aに変数Bを代入すると、変数Aに入るのは、変数Bに今入ってる数字そのものじゃなくて、「変数Bへのリンク」なので、Aを変えるとBもつられて変わる」ってやつです。
「pythonの代入は参照渡し」ということは知ってるはずなのによく忘れます。それに、こんなとこまで参照渡しされるとは。

「参照渡しされているから、大元のクラスのインスタンス変数が変わる」とはこういうこと

class Test:
    def __init__(self) -> None:
        self.hoge = "hoge"
        self.huga = "huga"
    def to_dict(self) -> dict:
        temp = vars(self)
        # temp内のhugaの値だけをhugahugaに書き換えたつもり
        temp["huga"] = "hugahuga"
        return temp

# インスタンスを作ってto_dict()する
test_instance = Test()
test_dict = test.instance.to_dict()

# 結果を確認
print(test_dict["huga"])  # これは当然 'hugahuga'
print(test_instance.huga)  # こっちまで 'hugahuga' に変わっている

まじかよ~~

対策を考える

1・インスタンス変数をプライベート変数化してみる

とりあえず真っ先に思いつく方法として、インスタンス変数をプライベート変数化してみます。

class Test:
    def __init__(self) -> None:
        self.__hoge = "hoge"
        self.__huga = "huga"

    def to_dict(self) -> dict:
        temp = vars(self)
        # temp内のhugaの値だけをhugahugaに書き換え
        temp["huga"] = "hugahuga"
        return temp

    def __str__(self) -> str:
        # 確認用
        return f"hoge:{self.__hoge}/huga:{self.__huga}"

# インスタンスを作ってto_dict()する
test_instance = Test()
test_dict = test.instance.to_dict()

# 結果を確認
print(test_instance.__huga)  # プライベート化したのでこれだとアクセス不可
print(test_dict["__huga"])  # これもアクセス不可

# どうなっているか見て見る
print(test_instance)  # hoge:hoge/huga:huga ← hugaはhugaのままだ!
print(test_dict) # {'_Test__hoge':'hoge','_Test__huga';'hugahuga'}  # OH

インスタンス変数が変更されることは防げる様になりましたが、プライベート変数にするとKeyが変わってしまうという問題が。OH。
Keyは何でも良いぞ、ということならそれでも構わないんですが、今回はKey何でも良くないのでさてどうしましょう。

2・一旦新しい辞書を作って返す

class Test:
    def __init__(self) -> None:
        self.hoge = "hoge"
        self.huga = "huga"

    def to_dict(self) -> dict:
        temp = vars(self)
        # 新しくnew_dictを作って、tempの中身を一度取り出して入れ直す
        new_dict = {}
        for key,val in temp.items():
            new_dict[key] = val
        # new_dictのhugaをhugahugaに変更して返却
        new_dict["huga"] = "hugahuga"
        return new_dict

    def __str__(self) -> str:
        # 確認用
        return f"hoge:{self.__hoge}/huga:{self.__huga}"

# インスタンスを作ってto_dict()する
test_instance = Test()
test_dict = test.instance.to_dict()



# 結果を確認
print(test_instance)  # hoge:hoge/huga:huga ← hugaはhugaのままだ!
print(test_dict) # {'hoge':'hoge','huga';'hugahuga'}  # できた!!!

やったやりました目的を達成しました!
なんか凄く納得が行かない気がしますがとりあえず出来ました!!
(他にもうちょっとスマートなやり方があるような気がしますので、ご存じの方がいたらご教示頂けると嬉しいです)

余談:「pythonの代入は参照渡し」について

……なんかものすごく納得行かないのでちょっと調べたんですが、「pythonの代入は参照渡し」について、私の理解が間違っていたようです。

「原則として参照渡し」なのはそうなのですが、「ただし、イミュータブルな値は値渡し」が行われているそうです。なんやて。
str型やint型などの、おおよそ主にやりとりする型はイミュータブルになっているため、事実上の値渡しが行われているそうです。へー。
pythonくん型がないの楽だなって思ってたんですけど、そういうのを暗黙でやるの止めてくれませんかとちょっと思います。
つまり、一旦辞書を作り直す際に、str型のkeyとvalの再代入が行われるため、そこで値渡しが行われて参照から解放されると、へー。

なるほど、普段おおよそやりとりするstrやintなどの値は値渡しだし、自作クラスは勝手にイミュータブルになるように作っているし、可読性を上げるため変数の使い回しを極力しないようにしていたので、今まであまり「pythonの代入は参照渡し」を意識してプログラムを書く必要が無かったから、よく忘れていたということ…………

この記事が気に入ったらサポートをしてみませんか?