クラス — データと振る舞いを一緒に
`class`、`__init__` と `self`、メソッド、`__repr__` で表示用の形、`__eq__` で等価性、クラスレベル対インスタンスレベル、クラスがいらないとき。
どうやって学びますか?
解決する問題
これまでは、1 つのものについての複数の事実を辞書に保存してきました。
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'このエラーは検出されました — それも何かですが、その行が実行されたときだけです。条件の中に隠れていれば、1 ヶ月後に初めて現れるかもしれません。
そして問題はスペルミスだけではありません。どんなキーも辞書に入れられるため、どこにも注文明細が何を含むべきかが書かれていません。合計を出す関数は別の場所にあり、だれもその辞書が呼び出し前にすべてのフィールドを持っているか確認しません。データは一つの場所に、それにできることは別の場所に、そしてどちらがどちらに属するかはあなたの頭の中にしかありません。
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0クラスはひな形です:この種類のものが何を含み、それに何ができるかを定めます。
この章を終える頃には
classを書き、__init__とselfの役割を言える- メソッドを書き、通常の関数とどう違うか言える
__repr__でオブジェクトに表示用の形を与えられる- クラス属性とインスタンス属性の違いを言え、よくある落とし穴を避けられる
__eq__で 2 つのオブジェクトを比較可能にできる- クラスがいらないときを言える
前提条件: JSON と CSV.
self が本当に何であるか
self は魔法の言葉ではありません — 最初のパラメータであり、そこに入るのはメソッドが呼ばれたオブジェクトです。
line.total() を書くことは「OrderLine の total メソッドを実行し、それに line を self として渡せ」を意味します。だから中の self.price がまさにその明細の価格を与えます。
self を忘れると、最初はメッセージが分かりにくいです:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given「1 was given」 — でも括弧は空です。渡されたのは line 自身で、静かに渡され、shout はそれを受け取る場所を用意していませんでした。
__init__ も特別な名前を持つ普通のメソッドです:オブジェクトが作られた直後に Python が呼び出します。その中で self.name = name が属性を設定します。
スペルミスならより分かりやすいエラー
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'率直に言えば:クラスはこのミスを早期には捕捉しません。AttributeError も KeyError と同じく、その行が実行されたときにだけ現れます。条件の中に隠れていれば、こちらも 1 ヶ月後に初めて現れるかもしれません。
では利点はどこにあるのか。3 つあります。
- エラーがより多くを伝えます。 メッセージにクラス名 —
'OrderLine' object— が出るので、間違った名前が何の種類のもので検索されたかが分かります。 - 何がそこにあるべきかが一ヶ所に書かれています。
__init__を読めばOrderLineがどの属性を持つか分かります。辞書にはそんなリストはありません。 - エディタが助けてくれます。 エディタがクラスを読み、
line.と打ったあとに属性名を提案し、多くのエディタは未知の名前を下線で示します — コードが走る前から。辞書のキーではたいていできません。
限界も覚えておいてください:スペルミスは書いている最中には見つかりません。order["nmae"] = 5 が静かに新しいキーを作るのと同じように、line.nmae = 5 も静かに新しい属性を作ります。
そして中身を見ることができます:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ が示す通り、オブジェクトの中には本当に辞書があります。クラスが辞書を取り除いたわけではなく — 名前と形を与えました。
__repr__ — 表示されるもの
__repr__ がない場合:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>末尾の数字はメモリアドレスで、実行ごとに異なります — あなたの画面では違う数字が出ます。デバッグでは役に立ちません。
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]2 行目が本当の利点です:リストの中でも動きます。 Python がリストを表示するとき、各要素の __repr__ を呼び、__str__ ではありません。だからこそ、この 2 つのうち __repr__ を先に書くのです。
慣例では、そのオブジェクトを作るコードのように見える文字列を返します。!r を使うと内部の文字列に引用符がつきます — 24 章と同じ理由で。
等価性 — デフォルトは望み通りではない
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
True2 つのオブジェクトは同じものを持っていますが、それでも == は False を返します。なぜならデフォルトの等価性は同一性だからです — == は is と同じように振る舞い、「これはまさに同じオブジェクトか?」と尋ねます。
これを変えるには、何が等しいかを定めます:
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
Falseこれで == は「似ている」を、is は「同じ一つ」を言います — 21 章の区別、今度はあなたの型で。
注意点が 1 つ:__eq__ を書くと、デフォルトではオブジェクトがハッシュ化できなくなり、セットに入れたり辞書のキーにしたりできません。次章の dataclass が両方を 1 行でやってくれます。
クラスレベルとインスタンスレベル
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
True2 つの別々のバスケットですが、リストは 1 つだけ。
見覚えがありますか? これは 21 章のミュータブルなデフォルトの落とし穴が、新しい場所で現れたものです。class 行の直下の items = [] は 1 回だけ実行されます — クラスが作られたときで、オブジェクトごとではありません。だからそのリストはクラスに属し、すべてのインスタンスが共有します。
直し方も同じ種類です:それぞれのオブジェクトに属するものは、__init__ の中で作ります。
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE はクラスレベルに残り、正しくそうなっています — すべてのバスケットで同じで、変えるつもりはないからです。しかし Python は変えるのを止めません:Basket.TAX_RATE = 0.2 でもエラーは出ません。大文字の名前は「これは定数として扱い、変えないでほしい」と読み手に伝える慣習に過ぎません。一言で言えば:定数はクラスレベル、変わるものは __init__ に。
クラスを書かないとき
クラスを学ぶと、何でもクラスにしたくなります。クラスがいらないいくつかの兆候:
クラスに __init__ とメソッドが 1 つだけ。 それは関数を理由なく 2 つに分けただけです。
メソッドがまったくなく、データだけ。 次章の dataclass がまさにこのためにあり、はるかに少ない行で済みます。
オブジェクトが決して変わらないなら、単一の値だけを持つ。 タプルか、素の値で十分です。
クラスが適切な道具なのは、データと振る舞いが本当に一体になっているときです:変わるデータがあって、それに対して常に実行されるいくつかの操作があるとき。
完全な例
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0見るべき 4 点です。
__init__ が検証します。 quantity < 1 ならオブジェクトは一切作られません。これがクラスが提供する最も有用なことです:OrderLine を持っているなら、その quantity は少なくとも 1 — それを使うあらゆる場所で、もうチェックする必要がありません。辞書ではそんな保証はできません。
Order は明細の価格の決め方を知りません。 line.total() を呼ぶだけです。税の適用は OrderLine の責任で、それを変えれば一ヶ所の変更で済みます。
両方のクラスに __repr__ があります、そして largest() の出力がなぜそれが重要かを見せています。なければ最後の行はメモリアドレスになり、何も伝えません。
add が自分を返します (return self)。 これで order.add(...).add(...) が可能になります。ここでは意図的に使っていません — 選択肢があることと、すべての行をつなげて書くこととは別だからです。
うまくいかないとき
TypeError: ... takes 0 positional arguments but 1 was given メソッドに self が足りません。最初のパラメータとして self を追加してください。
AttributeError: 'X' object has no attribute 'y' スペルミスか、self.y が __init__ で設定されていません。print(obj.__dict__) で実際にあるものが分かります。
TypeError: X() takes no arguments __init__ がないか、名前が間違っています (__init__ であり、__int__ ではありません、アンダースコアは両側に 2 つずつ)。
すべてのオブジェクトが 1 つのリストを共有している リストがクラスレベルにあります。__init__ の中に移してください。
2 つのオブジェクトが同じものを持っていて == が False になる デフォルトの等価性は同一性です。__eq__ を書く — または次章の dataclass を使う。
__eq__ を書いたらセットに入らなくなった __eq__ を書くとデフォルトのハッシュがなくなります。dataclass(frozen=True) が両方を与えます。
表示が <__main__.X object at 0x...> になる __repr__ がありません。
ステップ 4 / 6 — 予想する
理解度をチェック
shout() が引数なしで呼ばれます。何が起きますか?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- A`TypeError` — 1 個を取るのに 0 個渡された
- B`TypeError` — 0 個を取るのに 1 個渡された
- C`AttributeError`
- D`hello` が表示される
別々の 2 つのバスケット、それぞれに 1 アイテム。何が表示されますか?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
2 つのオブジェクトが同じ名前を持ちます。何が表示されますか?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- B`TypeError`
- CFalse False
- DTrue True
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
2 つのクラスを書いてください。
Book — title、author、pages から作ります。pages が 1 未満のとき ValueError を投げます。__repr__ を与え、タイトルと著者が一致したら 2 冊を等しいとする __eq__ を書きます。
Shelf — 本を集め、add(book)、total_pages()、longest()、by_author(name) でリストを返す機能を持ちます。
それから 5 つの実験:
Shelfのbooks = []を__init__の外に出し、class行の直下に移します。別々の 2 つの棚を作り、一方に本を追加して、もう一方を表示します。何が見えますか?__repr__を削除し、本のリストを表示します。デバッグがどれだけ難しくなりますか?__eq__を削除し、同じタイトルと著者の 2 冊を比較します。- 1 つのメソッドから
selfを外して呼び出します。エラーメッセージは何個の引数が渡されたと表示しますか? Bookを作り、book.pagse = 100と書きます (わざとスペルミス)。エラーは出ますか? それからprint(book.__dict__)— 何が見えますか?
最後の実験が重要です。なぜならクラスの限界が見えるからです。間違った名前は読むときにエラーを出しますが、書くときには出ません — 新しい属性を設定することは常に許されます。次章では、型ヒントと型チェッカがこの隙間も埋められることを示します。