Code Fix

上級

クラス属性がインスタンス間で共有される

クラス直下に書いたリストや辞書は、全インスタンスで共有される1つのオブジェクトになります。インスタンスごとに独立させたい場合は__init__で作ります。

なぜエラーが出ないのか

出力: ['apple']
(エラーなし)
Pythonは何も報告しません。文法として正しいためです
出力: ['apple']
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  class Cart:
 2      items = []
 3      def __init__(self):
 4          pass              
                     ^
 5      def add(self, item):
 6          self.items.append(item)
 7  
 8  c1 = Cart()
 9  c2 = Cart()
10  c1.add("apple")
11  print(c2.items)
出力: ['apple']期待: [] (エラーにはなりません)

クラス直下に書いたitems=[]は全インスタンスで共有される1つのリストです。__init__でself.itemsに新しいリストを代入すると、インスタンスごとに独立したリストになります。

直し方: pass を self.items = [] にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  class Product:
 2      tags = []
 3      def __init__(self):
 4          pass            
                    ^
 5      def tag(self, name):
 6          self.tags.append(name)
 7  
 8  p1 = Product()
 9  p2 = Product()
10  p1.tag("sale")
11  print(p2.tags)
出力: ['sale']期待: [] (エラーにはなりません)

商品にタグを付けるような処理でも同じ罠にかかります。1つの商品に付けたタグが、無関係な別の商品にも表示されてしまいます。

直し方: pass を self.tags = [] にします。

この問題を解いてみる →

パターン3

 1  class Robot:
 2      log = []
 3      def __init__(self):
 4          pass          
                   ^
 5      def move(self, step):
 6          self.log.append(step)
 7  
 8  r1 = Robot()
 9  r2 = Robot()
10  r1.move("north")
11  print(r2.log)
出力: ['north']期待: [] (エラーにはなりません)

動作ログを記録する場合も、__init__で自分専用のリストを作らないと他のインスタンスの記録と混ざります。

直し方: pass を self.log = [] にします。

この問題を解いてみる →

パターン4

 1  class Playlist:
 2      songs = []
 3      def __init__(self):
 4          pass              
                     ^
 5      def add(self, song):
 6          self.songs.append(song)
 7  
 8  p1 = Playlist()
 9  p2 = Playlist()
10  p1.add("song1")
11  print(p2.songs)
出力: ['song1']期待: [] (エラーにはなりません)

プレイリストのような機能でも同じ罠にかかります。1つのプレイリストに追加した曲が、無関係な別のプレイリストにも現れてしまいます。

直し方: pass を self.songs = [] にします。

この問題を解いてみる →

パターン5

 1  class Basket:
 2      fruits = []
 3      def __init__(self):
 4          pass                
                      ^
 5      def add(self, fruit):
 6          self.fruits.append(fruit)
 7  
 8  b1 = Basket()
 9  b2 = Basket()
10  b1.add("apple")
11  print(b2.fruits)
出力: ['apple']期待: [] (エラーにはなりません)

買い物かごのような機能でも同様です。__init__で自分専用のリストを作らないと、他のインスタンスの中身と混ざります。

直し方: pass を self.fruits = [] にします。

この問題を解いてみる →

よくある誤解

self.itemsのように書けばインスタンスごとの変数になると思いがちですが、代入していなければ同名のクラス属性を参照しているだけです。1つのインスタンスが変更すると全インスタンスに影響します。

実務での勘所

この現象の裏側には、Pythonの属性検索の順序があります。instance.attrと書くと、まずそのインスタンス自身の__dict__を探し、無ければクラスの__dict__にフォールバックします。self.items.append(x)のように中身を変更する操作は、この検索でクラス変数に辿り着いてそれ自体を変更しています。一方、self.items = [x]のように=で代入する操作は、検索をせずにインスタンス自身の__dict__に新しいエントリを作るため、以後はクラス変数ではなくインスタンス変数が優先して見つかるようになります。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)