[Pythonクイズ]データをまとめるクラス、もっとラクに書けるって知ってる?:Pythonステップアップクイズ
複数のデータをまとめて1つのクラスとして表現したいことってありますよね。でも、いっつも決まった初期化とか比較とか、文字列化とかのコードを書くのも面倒くさくないですか?
【問題】
以下は名前と名字、年齢を保持するPersonクラスの定義だ。このクラスは問題なく使えるが、あるモジュールのある機能を使うとよりシンプルなコードに書き直せる。その機能を使ってこのコードを書き直してみよう。
class Person:
def __init__(self, first, last, age=0):
self.first = first
self.last = last
self.age = age
def __repr__(self):
return f'Person({self.first}, {self.last}, {self.age})'
def __str__(self):
return f'{self.first} {self.last}({self.age})'
どうもHPかわさきです。
唐突ですが、今回から筆者が書き終わった原稿をChatGPT、Claude、Geminiの3つのLLMに入力して、クイズの難易度を判定してもらうことにしました。今回の判定結果は10段階評価で「5、6、6」ですか。無難な判定をしやがって。1つは2で、もう1つは4、最後の1つは8とか、振り切った判定にしてくれれば面白かったのに……。
今回は時間に追われて棒グラフという単純なモノになっています。でも、表示方法は棒グラフ以外にも五つ星で表示するとか、りんごの数で表示するとか、いろいろあるでしょう(筆者の体重はりんご3個分です。うそ)。そもそも3つのLLMの判定結果を個別に表示するのではなく、平均しちゃってもよいかもしれません。
ちなみに今回の判定結果を平均すると、10段階評価なら5.67。5段階なら「2.8」くらい。これを五つ星で表現すると「★★★☆☆」みたいな感じです。ただし、真ん中の★はちょっと加工が必要そう。どんな形式になるかはまだ分かりません(編集会議の結果しだい)。いずれにせよ、難易度は問題画像の左下に表示しますので、毎回チェックしてみてくださいね。
【答え】
正解のコード例を以下に示します。
from dataclasses import dataclass
@dataclass
class Person:
first: str
last: str
age: int = 0
def __str__(self):
return f'{self.first} {self.last}({self.age})'
正解のコード例では、dataclassesモジュールのdataclassデコレーターを使ってコードを書き直しています。dataclassデコレーターを使うと、__init__特殊メソッドや__repr__特殊メソッドなど、幾つかのメソッドが自動的に定義されたクラスを手に入れられます。
以下では、dataclassデコレーターについて少し詳しく見ていきましょう。
【解説】
人名と住所、電話番号など、幾つかのデータをひとまとめのデータとして取り扱うことはプログラミングの世界においてはよくあります。リストやタプルなどを使ってもそうした複雑なデータ構造は記述できますが、多くの場合はクラスを使って、そうしたデータを表現することが多いでしょう。クラスを使って、これを行っているのが問題文のクラス定義です。
class Person:
def __init__(self, first, last, age=0):
self.first = first
self.last = last
self.age = age
def __repr__(self):
return f'Person({self.first}, {self.last}, {self.age})'
def __str__(self):
return f'{self.first} {self.last}({self.age})'
このような一連のデータをまとめて1個のデータとして表現したい場合に便利に使えるのがdataclassesモジュールのdataclassデコレーターです。名前から分かる通り、Pythonでは今述べたような特性を持つクラスを「データクラス」と呼んでいます。
データクラスを定義する例が先ほども示した正解例のコードです。クラス定義(class文)の前に「@dataclass」というデコレーターが付加されている点に注目してください。
from dataclasses import dataclass
@dataclass
class Person:
first: str
last: str
age: int = 0
def __str__(self):
return f'{self.first} {self.last}({self.age})'
dataclassデコレーターを付加したクラス定義では、個々のインスタンスが持つメンバー(フィールド*1、インスタンス変数)をクラス変数に型アノテーションを付加した「first: str」のような形で定義している点には注意が必要です(ただし、これらのフィールドが実際にその型かどうかのチェックは基本的にされません)。dataclassデコレーターが付加されたクラスにインスタンスメソッドがあれば、フィールドにはインスタンス変数と同様に「self.名前」のような形でアクセスできます。上のコードの__str__特殊メソッドでは実際にこれを行っているのが分かるはずです。
*1 dataclassesモジュールのドキュメントでは、インスタンス属性という用語を使わずに「フィールド」という用語が使われているので、ここでもそれを踏襲しています。
dataclassデコレーター付きで定義したクラスからは__init__特殊メソッドと__repr__特殊メソッドがなくなっています。また、元のコードにあった__init__特殊メソッドではageパラメーターにデフォルト引数値がありました。__init__メソッドがなくなると、デフォルト引数値をどうすればよいの? というと、書き直したコードでは「age: int = 0」のようになっていますね(ミュータブルなオブジェクトを何かのフィールドのデフォルト値にするにはdataclasses.field関数を使えます。これについてはドキュメントを参照してください)。
上のようなクラスを定義する際には、これらの定型的なメソッドを毎回毎回定義するのが面倒くさいと感じることが多いはずです。データクラスを定義すれば、そうした処理が自動で行われるのです。これだけでもコードを書く側としてはありがたいと感じませんか? 例えば、問題文のコードから__repr__特殊メソッドと__str__特殊メソッドの定義を省いて、次のようなクラス定義にしたとしましょう。
class Person:
def __init__(self, first, last, age=0):
self.first = first
self.last = last
self.age = age
「シンプルでいいじゃん。dataclassデコレーターとか要らないんや!」と思うかもしれません。でも、次のようにインスタンスを生成して、それを出力してみるとどうなるでしょう。
hpk = Person('HP', 'kawasaki', 88)
print(hpk) # <__main__.Person object at 0x10c66cec0>
print関数にPersonクラスのオブジェクトを渡すと、それをstr関数で文字列化したものが出力されます。実際には、str関数はそのオブジェクトの__str__特殊メソッドを呼び出しますが、そのオブジェクト(のクラス)で__str__特殊メソッドが定義されていなければ、そのクラスの__repr__特殊メソッドが呼び出されるようになっています。そして、上のPersonクラスではそのどちらも定義していないので、全ての元であるobjectクラスが持つ__repr__特殊メソッドが使われて、「<__main__.Person object at 0x10c66cec0>」のような結果が出力されてしまっているというわけです。
これに対して、データクラスでは__repr__特殊メソッドが自動的に定義されます。実際にデータクラスとして定義したPersonクラスのインスタンスに対して__repr__特殊メソッドを呼び出してみましょう(というか、repr関数にPersonクラスのインスタンスを渡して、その結果をprint関数で出力してみます)。
from dataclasses import dataclass
@dataclass
class Person:
first: str
last: str
age: int = 0
def __str__(self):
return f'{self.first} {self.last}({self.age})'
hpk = Person('HP', 'kawasaki', 88)
print(repr(hpk)) # Person(first='HP', last='kawasaki', age=88)
「Person(first='HP', last='kawasaki', age=88)」という文字列が得られました。
repr関数(__repr__特殊メソッド)は「オブジェクトの印字可能な表現を含む文字列」を返します。「印字可能な表現」とは「eval関数にそれを渡すと、そのオブジェクトを復元できる」といった意味です。つまり、「Person(first='HP', last='kawasaki', age=88)」をeval関数に渡せば(そして、Personクラスが定義されていれば)元のオブジェクトと同様なオブジェクトを復元できるということです(これは実際にそうすることを意味するのではなく、オブジェクトの文字列表現として適切なものが何かを表すものです)。
これに対して、str関数(__str__特殊メソッド)はより簡潔にそのオブジェクトを文字列として表現したものを返すことを目的としています。「Person(first='HP', last='kawasaki', age=88)」は分かりやすいのですが、オブジェクトをより簡潔に表現した文字列があると便利なので、上のデータクラス(と問題文)のクラス定義では__str__関数も定義していたというわけです。
print関数にPersonオブジェクトを渡せば、内部で__str__特殊メソッドが呼び出されるので、実際に試してみましょう。
print(hpk) # HP kawasaki(88)
こちらは「HP kawasaki(88)」という文字列が得られました。
このようにデータクラスであっても、自分でメソッド(や特殊メソッド)を定義することも可能です。自動的に定義される特殊メソッドでは都合が悪ければ、自分で定義すればよいでしょう。そして、データクラスではどの特殊メソッドを自動で定義するかをdataclassデコレーターに対する引数として指定可能です。
引数 | 特殊メソッド | デフォルト値 |
---|---|---|
init | __init__特殊メソッド | True(自動で定義) |
repr | __repr__特殊メソッド | True(自動で定義) |
eq | __eq__特殊メソッド | True(自動で定義) |
order | __lt__/__le__/__gt__/__ge__特殊メソッド | False(自動で定義しない) |
unsafe_hash | __hash__特殊メソッド | 以下を参照 |
dataclassデコレーターに指定可能な引数の値と対応する特殊メソッド |
例えば、正解例のコードは次のように書くのと同じです。
@dataclass(init=True, repr=True, eq=True)
class Person:
first: str
last: str
age: int = 0
def __str__(self):
return f'{self.first} {self.last}({self.age})'
データを格納するクラスを定義するためのものだと考えると、オブジェクトとオブジェクトの等価性を比較できることは重要です。そういう意味ではeq=True(デフォルト)として__eq__特殊メソッドが自動で定義されるのはうれしいことです。
また、オブジェクト間の順序(大小関係)を比較できるのが望ましい場合もあります。このときには、order=Trueを指定することで、大小関係の比較に必要な特殊メソッドが定義されます(ただし、order=Trueにする場合にはeq=Trueである必要があります)。
__hash__特殊メソッドについては少し注意が必要です。デフォルトでは、オブジェクトのハッシュ化が安全でない場合、__hash__特殊メソッドは自動定義されません。ですが、eq=Trueかつfrozen=Trueであれば、自動的に__hash__特殊メソッドが定義されます(frozenはそのデータクラスのメンバーを初期化以降は変更できないようにするものです)。eq=Trueでfrozen=Falseであれば、__hash__はNoneに設定されます(これはオブジェクトがハッシュ不可能であることを意味します)。eq=Falseであれば、__hash__特殊メソッドには何の手も付けず親クラスに全てを任せます。unsafe_hash=Trueにすると、そのデータクラスのオブジェクトをハッシュするのが安全ではない場合でも__hash__特殊メソッドを自動で定義するようになります(これをTrueにするのは、論理的にはオブジェクトがイミュータブルなのに、コード上ではそうは見えない場合ということです)。
この他にもフィールドを変更不可としたり、キーワード専用引数としたりといった細かな制御も可能です。詳しくはdataclassesモジュールのドキュメントを参照してください。
というわけで、dataclassデコレーターの話のはずが、特殊メソッドの話が予想以上に増えちゃいましたが、知っておくと便利です。きっと。3つのLLMが判定した難易度は5と6と6でしたが、皆さん的にはどうでしたか? 試しに始めてみたばかりで、以下のようなことを検討中です。
- 難易度指数の画像表示はそもそもあった方がよい?
- 3つのAIそれぞれで10段階のグラフを表示した方がよい?
- 3つのAIの判定結果を平均したものを10段階のグラフとして表示する?
- 3つのAIの判定結果を平均したものを5つ星で表示する?
皆さんの希望やより良い案があれば、どしどしXやはてブでお寄せください。
解決!Pythonの「データクラスを定義するには」でもちょびっと触れています。よろしかったら読んでみてくださいね。
Copyright© Digital Advantage Corp. All Rights Reserved.