Python - クラス理解への道(罠にはまらないために)

by everes | December 05, 2020
tips | #python #python3

システム本部CTO室のeveresです。

今年は、とあるインタビュー記事でディスクリプターについて触れてから、remote.py、PyConJP 2020 Onlineと、続けてPythonの属性について話してきました。

締めくくり…と気合を入れたいところですがAdvent Calendarですので、本エントリーではあまり踏み込まずさわりだけを紹介します。 読んでみて動作を理解していなかった人やクラス生成のカスタマイズなど踏み込んで知りたい方は、PyConJP 2020 Onlineの資料をたどってみてください。末尾にリンクを記載しておきます。

このエントリーは DeNA Advent Calendar 2020 の5日目のエントリーです。

では、始めましょう。

動作環境など

本エントリに登場するサンプルのコードは次の環境で動作を確認しています。

  • macOS: 11.0.1
  • Python: 3.9.0

クラス属性とインスタンス属性

実際にクラスとインスタンスの動作を見てみましょう。

>>> class Spam:  # まず、空っぽのクラスを作ります
...   pass
... 
>>> spam1 = Spam()  # Spamクラスのインスタンスを2つ作ります
>>> spam2 = Spam()

Spamという空っぽのクラスを定義して、Spamクラスのインスタンスを2つ作りました。

クラスの属性に文字列をアサインするとインスタンスの属性アクセスはどうなる?

>>> Spam.egg = 'fried over'  # Spamクラスのeggに'fried over'をアサイン

Spamクラスのeggに文字列 'fried over' をアサインします。

さて、インスタンスspam1のeggはどうなっているでしょうか?

>>> spam1.egg
'fried over'

Spamクラスのeggにアサインされた値 ‘fried over’ が見えています。

インスタンスに指定の属性が無い場合、クラスの属性が見えるのです。

インスタンスの同名属性に文字列をアサインすると?

>>> spam1.egg = 'fried sunny‐side up'  # spam1のeggに'fried sunny‐side up'をアサイン
>>> spam1.egg
'fried sunny‐side up'
>>> spam2.egg
'fried over'

インスタンスspam1のeggに別の文字列 'fried sunny‐side up' をアサインします。確認してみると、spam1のeggにはアサインされた文字列 'fried sunny‐side up' が保持されているのが見えています。

当然ですが、spam2のeggはSpamクラスのeggが見えているままです。

インスタンスの__class__を通じてクラスの属性にアクセスする

>>> spam1.__class__.egg
'fried over'
>>> Spam.egg
'fried over'

インスタンスspam1の __class__ には元となったクラスが設定されています。インスタンスspam1の__class__を通じて参照したeggはSpamクラスのegg属性が見えています。

Spamクラスのeggも元のままです。

クラス属性定義とクラスメソッド

ここからが罠にハマりやすいところです。

クラスに属性を定義する

class Ham:
    bacon = None  # clsもselfもついてない

    @classmethod
    def burn(cls, arg):
        cls.bacon = arg

    def bake(self, arg):
        self.bacon = arg

次にHamクラスを定義します。__init__のようなメソッドの中ではなく、クラス宣言の内側にbaconを定義しています。

burnというクラスメソッドと、bakeというメソッドも定義しています。

クラス宣言の内側に宣言したbaconはなんなのか?

>>> ham1 = Ham()
>>> ham2 = Ham()
>>> ham1.bacon  # インスタンスham1
>>> Ham.bacon  # クラスHam

Hamクラスのインスタンスを2つ作ります。

インスタンスham1経由でbaconにアクセスできます(属性が無ければAttributeErrorが送出されますよね)。当然、Hamクラス経由でもbaconにアクセスできます。

先ほど見てきたクラスの属性と同じ動作のようです。

クラスメソッドburnを呼び出してみる

>>> ham1.burn('charred')  # クラスメソッドをインスタンス経由で呼び出しています
>>> ham1.bacon
'charred'
>>> Ham.bacon
'charred'
>>> ham2.bacon
'charred'

インスタンスham1からクラスメソッドburnを呼び出してみます。インスタンス経由で呼び出しても、クラスメソッドの第一引数はクラスです。クラスのbacon属性が変わっています。

インスタンスメソッドbakeを呼び出してみる

>>> ham1.bake('underdone')  # インスタンスメソッド
>>> ham1.bacon
'underdone'
>>> ham2.bacon
'charred'
>>> Ham.bacon
'charred'
>>> ham1.__class__.bacon
'charred'

インスタンスham1のbakeメソッドを呼び出すと、インスタンスのbaconに引数がアサインされます。インスタンスham1経由でbaconにアクセスするとham1のインスタンス属性が見えるようになりました。

クラスやインスタンスham2のbaconは変わらないままです。

インスタンスの属性をdelしてみる

>>> del ham1.bacon
>>> ham1.bacon
'charred'

インスタンスham1のbaconを削除すると再びクラスの属性が見えるようになります。

インスタンス属性の初期化をしましょう!

普段、クラス属性とインスタンス属性で同じ名前を使うとはまりますので避けましょう。うまい具合にしているフレームワークなんかもありますけど、あれはもう少し面倒なことをしてます。

class Egg:

    def __init__(self):
        self.zen = None

ついうっかりはまらないように、__init__でインスタンス属性の初期化をしましょう!

DeNAのAdvent Calendar 2020について

DeNA では今年、以下の 3種 のアドベントカレンダーを書いてます!それぞれ違った種類なのでぜひ見てみてください。

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!

また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog 記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!

より深く知りたい人は

PyConJP 2020 Onlineの資料を参照してみてください。

私が一部を書いているパーフェクトPython第二版を読んでいただけるとより良いと思います!