7
3

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.

株式会社TORICOAdvent Calendar 2022

Day 8

お前は Python で無駄なクラス変数を定義している

Last updated at Posted at 2022-12-10

Java や PHP などの一般的なオブジェクト指向言語経験者が Python を書いた時に書いてしまいがちな、誤ったコードを書きます。

私の会社でも、経験の浅い方のコードレビューでたまに見ますので、解説のために記事を書きました。

間違っているコード

python
class Employee:
    name: str = ''

    def __init__(self, name: str):
        self.name = name

この書き方は、無駄なクラス変数を定義しており、おそらく間違いです。

Python 経験者であれば、そこは @dataclass デコレータを使ったほうがいいという話になりますが、その話題ではありません。

他の言語の場合

このクラスは、 Java でいうと

java
public class Employee {
  public static String name = "";

  public String name;

  public Employee(String name) {
    this.name = name;
  }
}

このコードに相当します。

PHP では

php
public class Employee {

    public static string $name = '';

    public string $name;

    function __construct(string $name) {
        $this->name = $name;
    }
}

こうなります。

どちらも、 name という同名の static 変数インスタンス変数 を定義してしまっています。

ちなみに、どちらもコンパイル時にエラーとなりますが、それは無視します。

Java や PHP のコードを見ると、 static 変数の static String name は、紛らわしいだけで意味が無い変数に見えます。

せめて static 変数でクラス自体の名前? みたいなものを定義する変数が必要なら、インスタンス変数で使う name と同名の変数名である name はやめてくれと思います。(実際、コンパイルエラーで使えません。)

それと同じ意味のコードが、冒頭の Python コードです。

Python コードの解説

python
class Employee:
    name: str = ''  # name という名前のクラス変数 (staticみたいなもの)

    def __init__(self, name: str):
        self.name = name  # name という名前のインスタンス変数

Python はインスタンス変数の定義というものがそもそもありません。
エディタによっては、 __init__ (コンストラクタ) の中で self に代入したものをインスタンス変数の定義とみなします。

そのため

java
class Employee {
    public String name;
    
    public Employee(String name){
        this.name = name;
    }
}

このコードを、Python で真似ると

python
class Employee:

    def __init__(self, name: str):
        self.name = name

こうなります。

Python は、クラス定義内に、インスタンス変数の定義は書くことができません。

最初の例のように、 class Employee の下に name: str = '' と書くと、それはクラス自体に所属する変数となり、インスタンス変数とは別の変数なので、おそらく使われることがありません。

もし、クラス変数とインスタンス変数を同名で意図的に定義しているとしたら、ミスの発生しやすいコードとなります。

クラス変数とインスタンス変数

それでは、実際に下記のクラス変数がどのように動くか、実験をしてみます。

pythonコード
class Employee:

    name: str = 'クラス変数'

    def __init__(self, name: str):
        self.name = name

print('Employee.name:', Employee.name)
結果
Employee.name: クラス変数

↑ クラスをインスタンス化せずに、直接クラス変数を表示しています。

コード
employee = Employee('インスタンス変数')
print('employee.name:', employee.name)
結果
employee.name: インスタンス変数

↑ クラスのインスタンスを作り、コンスタラクタの引数で name を設定しています。

コード
employee2 = Employee('インスタンス変数2')
print('employee2.__class__.name:', employee2.__class__.name)
結果
employee2.__class__.name: クラス変数

↑ 新しいインスタンスを作りました。 インスタンスの .__class__ で、そのインスタンスのクラスにアクセスできます。

コード
print('id(employee.__class__):', id(employee.__class__), ', id(employee2.__class__):', id(employee2.__class__))
id(employee.__class__): 67727568 , id(employee2.__class__): 67727568

id() は、そのクラスのメモリの番地みたいなものを表示する関数です。同じIDであれば、同じオブジェクトであることを表します。最初に作ったインスタンス employee と、次に作った employee2 の __class__ は、同じメモリを参照しています。

コード
print(employee.__class__ is employee2.__class__)
結果
True

↑演算子 is は、同一のオブジェクトかどうかを表します。ID が同じであれば True になります。

employee.__class__employee2.__class__ は、同一のメモリを参照しており、全く同じものです。

コード
print(employee.__class__ is Employee)
結果
True

そして、 employee.__class__Employee全く同じものです。

コード
employee2.__class__.name = '破壊したクラス変数'
print('employee.__class__.name:', employee.__class__.name)  # employee2 ではなく employee
結果
employee.__class__.name: 破壊したクラス変数

employee2__class__ の状態を変更すると、employee.__class__ も同様に変化します。同じものだからです。

コード
print('Employee.name:', Employee.name)
結果
Employee.name: 破壊したクラス変数

Employee.name も変化しています。同じものだから。

変数の参照優先度

もう一つ、Python のインスタンス変数とクラス変数の特徴的な動作を書きます。

コード
class Fruit:
    name: str = 'クラス変数'

fruit = Fruit()
print('fruit.name:', fruit.name)
結果
fruit.name: クラス変数

↑クラス変数しか持たないクラスをインスタンス化し、インスタンスから変数にアクセスすると、クラス変数が取得できます

コード
print(fruit.name is fruit.__class__.name)
結果
True
コード
print(id(fruit.name), id(fruit.__class__.name))
結果
140144425798800 140144425798800

class を指定してもしなくても、両方は同じメモリを指しています。

コード
fruit.name = 'インスタンス変数'

print(fruit.name is fruit.__class__.name)
結果
False

ただし、インスタンスの変数にセット(代入)を行うと、先程のクラス変数には代入されず、新たなインスタンス変数が生成されます。

コード
print(fruit.name, fruit.__class__.name)
結果
インスタンス変数 クラス変数

↑ インスタンス変数ができている場合、優先して表示されます。

コード
del fruit.name
print(fruit.name, fruit.__class__.name)
結果
クラス変数 クラス変数

↑さきほど作ったインスタンス変数を削除した場合、その前から存在していたクラス変数にアクセスできるようになります。

インスタンス.変数 にアクセスした際は、インスタンスに変数名が存在するかを調べ、あればそれを返す。
なければ、次はクラスに探しに行って、あれば返す。という動作になります。

print(fruit.name) とした場合、name がインスタンス変数なのか、クラス変数なのか、コードを見ただけではわかりません。
その時のインスタンスの状態により、動的に変化します。

@dataclass デコレータ

クラス変数とインスタンス変数に対し、同じ名前をつけると混乱につながりがちですが、 dataclasses パッケージの @dataclass デコレータをつけたクラスは別です。

python
@dataclass
class Employee:

    name: str = ''

このようなクラスを定義した場合、

employee = Employee(name='鈴木')

を行うと、自動的に employee インスタンスのインスタンス変数 name に 鈴木 が入ります。

@dataclass デコレータを使うと、定義したクラス変数を引数にとるコンストラクタが自動生成され、コンストラクタの中で代入処理が行われるようになります。

コードの見た目としても、クラス内にメンバー変数の一覧が定義されているように見えるため、他のオブジェクト指向言語のような見た目となります。

詳しくは、Python ドキュメントの dataclass をご覧ください。

dataclasses --- データクラス — Python 3.11.0b5 ドキュメント

7
3
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
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?