Go to list of users who liked
More than 3 years have passed since last update.
Pythonのオブジェクト指向プログラミングを完全理解
オブジェクト指向
1. オブジェクト指向の起源
2003年チューリング賞の受賞者アラン・ケイさんはよくオブジェクト指向プログラミングの父と称されます。ご本人も憚ることなく、幾度、公の場で発明権を宣言しています。しかし、ケイさんは「C++」や「Java」などの現代のオブジェクト指向言語を蔑ろにしています。これらの言語は「Simula 67」という言語を受け継いだもので、私が作った「Smalltalk」と関係ないのだとケイさんは考えています。
オブジェクト指向という名称は確かにアラン・ケイさんに由来するものです。しかし、C++とJavaで使われている現代のオブジェクト指向は当初のと結構違います。ケイさん自身もこれらの言語を後継者として認めないです。では、ケイさん曰くC++とJavaの親であるSimula 67という言語はどんな言語でしょうか。ここで、簡単なサンプルコードを見てみましょう。
Class Rectangle (Width, Height); Real Width, Height; ! Class with two parameters; Begin Real Area, Perimeter; ! Attributes; Procedure Update; ! Methods (Can be Virtual); Begin Area := Width * Height; Perimeter := 2*(Width + Height) End of Update; Boolean Procedure IsSquare; IsSquare := Width=Height; Update; ! Life of rectangle started at creation; OutText("Rectangle created: "); OutFix(Width,2,6); OutFix(Height,2,6); OutImage End of Rectangle;
2つの変数を持つclass
ですね。文法は分からないですが、コメントを見てどういうものかは大体見当がつくでしょう。Simula 67は名前の通り、1967年に発表され、1973年にリリースされたプログラミング言語です。それに対して、Smalltalkは1975年に最初のバージョン(Smalltalk-72)が発表され、1980年代にリリースされた言語です。
class
があればオブジェクト指向プログラミングだというわけではないですが、Simula 67のclass
は「インスタンス」、「継承」、「メソッド」 や「late binding」までサポートしています。Simula 67は間違いなくオブジェクト指向系統の言語です。
しかし、Simula 67のオブジェクト指向の設計もオリジナルなものではないです。1965年、アントニー・ホーアさん(1980年チューリング賞受賞者)はある論文を発表しました。その論文に、record class
という概念が提出されました。ホーアさんは、ALGOLという言語でサンプルを書きました。
record class person; begin integer date of birth; Boolean male; reference father, mother, youngest offspring, elder sbling (person) end;
複合的なデータ型で、C言語の構造体と似ていますね。
そして1966年に、あるサマースクールで、ホーアさんはクリステン・ニガードさんとオルヨハン・ダールさんと出会いました。後ほどSimula 67を作ったのはこの2人なのです。ホーアさんはrecord class
のアイデアを2人に共有しました。ダールさんの話によると、その時ホーアさんはすでに「継承」の概念も思いつき、2人に教えました。そして、2001年に、クリステン・ニガードさんとオルヨハン・ダールさんはオブジェクト指向への貢献によりチューリング賞を受賞しました。アラン・ケイさんよりも2年早かったですね。
Simula 67について紹介しました。Simula 67は世界初のオブジェクト指向系統の言語ということも理解していただけたと思います。では、ケイさんが作ったSmalltalkは偽物ですか?結論から言うと、そうでもないです。Lisp言語の「Everything is a list」に対して、Smalltalkは初めて「Everything is an object(全てがオブジェクト)」という概念を作りました。更に、Smalltalkは演算子も含め式はすべてオブジェクトに対する「メッセージ」と解釈しています。Smalltalkこそが、オブジェクト指向に追い風を吹かせたプログラミング言語です。1980年代、Smalltalkのおかげで、オブジェクト指向プログラミング言語が輩出していました。その中に、今でもまだ健在しているC++などもあります。更に関数型プログラミング言語の元祖であるLisp陣営も「Common Lisp Object System」を手に持ち加勢していました。
最後に、1996年に現代のオブジェクト指向プログラミングパラダイムの最高峰であるJavaが発表されました。これが、オブジェクト指向史上の大きなマイルストーンとなる事件です。Java自体はオブジェクト指向において何も発明していないですが、今までの優秀な概念を吸収し、更にJVMの優れたマルチプラットフォーム性能とGCを備え合わせ、今でも世界TOP3にランクインするプログラミング言語となっています。
2. オブジェクト指向の特徴
オブジェクト指向の起源について紹介しました。しかし、そもそもオブジェクト指向とは何でしょうか?本題に入る前に、まず簡単な例を使って説明したいと思います。
オブジェクト指向はよくプロセス指向(手続き型プログラミングとも言う)と比較されます。下のコードはプロセス指向とオブジェクト指向の形式を表したものになります。
a=0# a+3の機能を実現したい# プロセス指向sum(a,3)# オブジェクト指向a.sum(3)
ただ書き方が違うだけじゃんと思うかもしれません。実は、オブジェクト指向プログラミングはコードのロジックを明瞭化することができます。そして、その威力はプログラムが大きければ大きいほど発揮されるものです。続いて、上記のコードの違いを詳しく見ていきましょう。
1. 構文
関数呼び出しの構文を語順の考え方で解釈することができます。
- プロセス指向は通常、
動詞(主語, 目的語)
という構造になっています。動詞がメインで、主語と目的語は引数として渡されます。 - オブジェクト指向は、SVO型、いわゆる
主語, 動詞, 目的語
の構造になっています。つまり、主語がメインになります。そして、主語がある動詞を呼び出して、目的語を引数として渡しています。日本語ですと、動詞が後に来るので、SVO型はしっくり来ないかもしれませんが、英語、ヨーロッパの多くの言語、中国語などはSVO型に準ずる言語なので、オブジェクト指向の方式は意味合い的には自然になります。
2. 定義方式
- プロセス指向は、
sum
という2つの引数を受け取り、その和をreturn
する関数を定義します。 - オブジェクト指向は、やや複雑で、まず
class
を定義します。そのclass
の中に、様々なメソッド(関数と理解しても良い)を定義します。そして、class
のインスタンスを作成し、そのインスタンスからメソッドを呼び出します。 - 上の例では、
a
は整数で、int
というclass
のインスタンスになります。整数のインスタンスは足し算や引き算のようなint
のメソッドが使えます。 - オブジェクト指向は変数を自動的に分類します。全ての変数はオブジェクトであり、ある
class
に属し、使えるメソッドも決まっています。例えば、文字列が来たら、どういうメソッドが使えるかはclass str
を見れば分かります。
3. 呼び出し方式
実践では、複数のオブジェクトに同じ処理をしたい時:
- プロセス指向は、1個または複数の関数を作って、全てのオブジェクトに対して関数を適用すれば実現できます。
- オブジェクト指向は、
class
とそのclass
のメソッドを定義し、全てのオブジェクトに対して、class
のインスタンスを作り、そのインスタンスからメソッドを呼び出します。
このような処理が多くなると
- プロセス指向は、たくさんの関数が定義され、関数の中に関数呼び出ししている可能性もあり、構造がどんどん不明瞭になります。そして、あるオブジェクトが来たら、関数で処理できるかどうかは中身を見ないと分からない場合もあります。
- オブジェクト指向は、
class
単位でメソッドをまとめて管理し、オブジェクトの使えるメソッドは自明です。
次に、オブジェクト指向のメリットをPythonのコード例を通して、少し詳しく見ていきます。
2-1. インターフェースの統一と管理
鳥、犬、魚の3つのclass
を定義します。
classBird:def__init__(self,name):self.name=namedefmove(self):print("The bird named {} is flying".format(self.name))classDog:def__init__(self,name):self.name=namedefmove(self):print("The dog named {} is running".format(self.name))classFish:def__init__(self,name):self.name=namedefmove(self):print("The fish named {} is swimming".format(self.name))
インスタンスを作ります。
bob=Bird("Bob")john=Bird("John")david=Dog("David")fabian=Fish("Fabian")
次に、全てのインスタンスのmove
メソッドを呼び出します。
bob.move()john.move()david.move()fabian.move()
実行結果:
The bird named Bob is flyingThe bird named John is flyingThe dog named David is runningThe fish named Fabian is swimming
インスタンスを作成する時、パラメータを渡す必要があります。このパラメータはオブジェクトが他のオブジェクトと区別するためのデータとなります。例えば、bob
というオブジェクトのname
はBob
で、john
のname
はJohn
なので、同じclass
から作成されたインスタンスにもかかわらず、違うオブジェクトになり、同じメソッドを実行しても結果が異なります。
また、違うclass
のmove
メソッドは、違う結果を出力しています。例えば、Bird
のmove
はThe bird named...
を出力し、Dog
はThe dog named...
を出力します。move
メソッドは「移動」という意味で、各動物class
は移動できるので、同じmove
として実装することで、インターフェースが統一していて記憶しやすくなります。
プロセス指向で実装すると、以下のような感じになるかもしれません。
defmove_bird(name):print("The bird named {} is flying".format(name))defmove_dog(name):print("The dog named {} is runing".format(name))defmove_fish(name):print("The fish named {} is swimming".format(name))bob="Bob"john="John"david="David"fabian="Fabian"move_bird(bob)move_bird(john)move_dog(david)move_fish(fabian)
bob
というオブジェクトが来たら、それが「鳥」なのか「犬」なのかをまず明確にしないと、move_bird
とmove_dog
のどれにするかが決められません。実際のプログラムではmove
だけではなく、数十種類の処理関数を実装するのが普通です。関数が多くなると、変数との対応関係を明確にするのが極めて難しくなります。また、これらの関数は内部で他の関数を呼び出している可能性もあり、この関数を他のプログラムで再利用する時に、内部で使われている関数も全部見つけ出して、移行する必要があります。
オブジェクト指向は変数を使って、class
からインスタンスを作成し、どのメソッドが使えるかはclass
を見れば分かります。そして、class
として抽象化することで、同じ文脈の関数が固まり、管理しやすくなります。
2-2. カプセル化
オブジェクト指向は関数とデータを一緒に束ねてくれるので、同じ変数(データ)をたくさんの関数で処理したい時はとても便利です。
classPerson:def__init__(self,name,age,height):self.name=nameself.age=ageself.height=heightdefdescribe(self):print("name: {}; age: {}; height: {}".format(self.name,self.age,self.height))defintroduce(self):print("My name is {}, and height is {}, and age is {}.".format(self.name,self.height,self.age))bob=Person("Bob",24,170)mary=Person("Mary",10,160)bob.describe()bob.introduce()mary.describe()mary.introduce()
実行結果:
name: Bob; age: 24; height: 170My name is Bob, and height is 170, and age is 24.name: Mary; age: 10; height: 160My name is Mary, and height is 160, and age is 10.
上記の処理をプロセス指向で実装すると、以下の2通りの方法があります。1つはそのまま引数として渡す方法です。
defdescribe(name,age,height):print("name is {}, age is {}, height is {}".format(name,age,height))defintroduce(name,age,height):print("My name is {}, and height is {}, and age is {}.".format(name,height,age))describe("Bob",24,170)describe("Mary",20,160)introduce("Bob",24,170)introduce("Mary",20,160)
上記の方法は毎回同じ引数を渡す必要があり、引数が多くなると、非常に面倒です。もう1つは毎回引数を渡す必要のない方法です。
bob=dict(name='Bob',age=24,height=170)mary=dict(name='Mary',age=20,height=160)defintroduce(**kwargs):print("My name is {name}, and height is {age}, and age is {height}.".format(**kwargs))defdescribe(**kwargs):print("Description: name is {name}, age is {age}, height is {height}".format(**kwargs))introduce(**bob)describe(**bob)introduce(**mary)describe(**mary)
この方法は引数を辞書で格納して、引数として辞書をアンパックして渡すようにしています。しかし、もし辞書の中にname, age, height
の3つのキーが存在しないと、エラーを起こしてしまいます。
このように、プロセス指向と比べて、オブジェクト指向は処理とデータをまとめてカプセル化してくれるので、コードのロジックが綺麗になりがちです。
2-3. オブジェクトの動的操作
オブジェクトの動的な一連の動作の実現は、プロセス指向には不向きです。
classIndividual:def__init__(self,energy=10):self.energy=energydefeat_fruit(self):self.energy+=1returnselfdefeat_meat(self):self.energy+=2returnselfdefrun(self):self.energy-=3returnselfanyone=Individual()print("energy: {}".format(anyone.energy))anyone.eat_meat()print("energy after eat_meat: {}".format(anyone.energy))anyone.eat_fruit()print("energy after eat_fruit: {}".format(anyone.energy))anyone.run()print("energy after run: {}".format(anyone.energy))anyone.eat_meat().run()print("energy after eat_meat and run: {}".format(anyone.energy))
実行結果:
energy: 10energy after eat_meat: 12energy after eat_fruit: 13energy after run: 10energy after eat_meat and run: 9
上記の「個体」のclass
は「エネルギー」という内部状態パラメータと「果物を食べる」、「肉を食べる」、「走る」の3つメソッドを持ちます。次に、さらに細分化した「男の子」と「女の子」の2つのclass
を定義します。
classBoy(Individual):defdaily_activity(self):self.eat_meat().eat_meat().run().eat_meat().eat_fruit().run().eat_meat()print("boy's daily energy: {}".format(self.energy))classGirl(Individual):defdaily_activity(self):self.eat_meat().eat_fruit()print("girl's daily energy: {}".format(self.energy))bob=Boy()bob.daily_activity()mary=Girl()mary.daily_activity()
実行結果:
boy's daily energy: 13girl's daily energy: 13
上記の処理をプロセス指向で実装すると、オブジェクトごとに、専用のenergy
という変数と、それぞれのenergy
を処理する関数を定義する必要があり、冗長になるのが避けられないです。
また、主語, 動詞, 目的語
の構造は比較的に理解しやすいです。上記の例では、まずeat_meat()
、次にrun()
という一連の動作が永遠に続いても理解できます。プロセス指向で実現するとboy_energy = eat_meat(boy_energy); boy_energy = run(boy_energy);...
のような長文になるか、eat_meat(run(boy_energy))
のような階層構造になるので、理解しにくくなるでしょう。
3. オブジェクト指向に関する概念
オブジェクト指向の特徴について簡単に紹介しました。ここからは少し高度な内容に入ります。オブジェクト指向には様々な概念がありまして、これらを説明しようと思います。
3-1. クラス
クラスは、同じ属性(変数、データ)と処理(メソッド、関数)を持つオブジェクトの設計図です。クラスは自身から生成されるオブジェクトの共通の属性と処理を定義します。プロセス指向言語では、変数は型によって分類されるのに対して、オブジェクト指向言語では、変数はクラスによって分類されます。そして、オブジェクト指向言語の型自体もクラスになっています。
ちなみに、Python 2には古いクラスと新しいクラスがあり、それぞれは以下のようになります。
classoldStyleClass:# inherits from 'type'passclassnewStyleClass(object):# explicitly inherits from 'object'pass
Python 3になると、全てのクラスはデフォルトで新しいクラスになるため、明示的にobject
継承する必要もなくなりました。
3-2. インスタンス
インスタンスは、単にオブジェクトと呼ぶこともありますが、クラスのコンストラクタとイニシャライザによって、属性に具体的な値が付与された実体のことを指します。
3-3. インスタンス化
インスタンス化は設計図であるクラスからインスタンスを生成する行為を指します。
3-4. インスタンス変数
インスタンス変数は、インスタンスごとに割り当てられた変数のことを指します。
3-5. クラス変数
クラス変数は、クラスとそのインスタンスが共有する変数のことを指します。
3-6. メソッド
メソッドはクラスまたはインスタンスに所属する関数のことを指します。
3-7. 静的メソッド
静的メソッドはインスタンス化しなくても、呼び出せるメソッドのことを指します。
3-8. クラスメソッド
クラスメソッドはクラスをオブジェクトとして操作するメソッドのことを指します。
3-9. メンバ
メンバはクラスまたはインスタンスの持つ名前空間に格納する要素です。名前空間には、通常メンバ変数(クラス変数またはインスタンス変数)とメンバ関数(各種メソッド)などが含まれます。
3-10. オーバーライド
オーバーライドは、子クラス(サブクラス・派生クラス)が親クラス(スーパークラス・基底クラス)から継承したメソッドを上書きする行為を指します。
3-11. カプセル化
カプセル化は、データと処理をオブジェクトとしてまとめて、境界線を作る行為を指します。
3-12. 継承
継承は、既存クラスの構造を受け継いだ子クラスを設計することを指します。is-aまたはhas-aの関係性を持たせるアーキテクチャです。
3-13. ポリモーフィズム
ポリモーフィズム(多態性)は、主にオーバーライドによって実現された子クラスの多様性を指します。例として以下のようなものが挙げられます。
classAnimal:defrun(self):print('Animal is running...')classDog(Animal):defrun(self):print('Dog is running...')classCat(Animal):defrun(self):print('Cat is running...')defrun_twice(animal):animal.run()animal.run()run_twice(Animal())run_twice(Dog())run_twice(Cat())
実行結果:
Animal is running...Animal is running...Dog is running...Dog is running...Cat is running...Cat is running...
つまり、あるクラスを入力とする処理は、その子クラスに対して何も修正する必要がなく、正常に動作できるという「リスコフの置換原則」による性質です。
3-14. 演算子オーバーロード
演算子オーバーロードは演算子の機能をユーザーが定義する行為を指します。Pythonでは全てのクラスはobject
クラスの子クラスで、それぞれの演算子オーバーロードは特殊メソッドにより実現されているので、性質としてはポリモーフィズムの1種になります。演算子オーバーロードに関する特殊メソッドは以下のようになります。
classMyNum:def__init__(self,x):self.__x=xdef__lt__(self,other):print("__lt__")returnself.__x<otherdef__le__(self,other):print("__le__")returnself.__x<=otherdef__eq__(self,other):print("__eq__")returnself.__x==otherdef__ne__(self,other):print("__ne__")returnself.__x!=otherdef__gt__(self,other):print("__gt__")returnself.__x>otherdef__ge__(self,other):print("__ge__")returnself.__x>=otherx=MyNum(100)x<10x<=10x==10x!=10x>10x>=10
実行結果:
__lt____le____eq____ne____gt____ge__
上記は、演算処理にprint
処理を追加したものです。PythonにはNumpy
という数値計算ライブラリがあります。そして、a * b
という形で行列のアダマール積の計算ができるのはPythonが演算子オーバーロードをサポートしているからです。
3-15. 抽象化
抽象化は、カプセル化で、強い関連性のあるデータと処理だけをオブジェクトとしてまとめて、概念を形成することを指します。例えば、動物をAnimal
というクラスとして設計し、動物の状態を変数にし、動物の動作をメソッドにすることで抽象化できます。
3-16. ダック・タイピングとモンキーパッチ
この2つの概念はRubyコミュニティ由来のもので、動的言語の性質を表します。
モンキーパッチはランタイムでコードを拡張や変更する方法です。Pythonのオブジェクト指向プログラミングでは、クラスを動的に変更する場合に用語として使われます。
ダック・タイピングは動的型付けオブジェクト指向プログラミング言語の性質で、例えばrun_twice(animal)
というような関数を実行するとします。静的型付け言語は、引数の型を評価して、Animal
クラスまたはその派生でないと、実行自体が許されません。複数の型に対応させるために、ジェネリックスやオーバーロードなどの仕組みがが必要になります。それに対して、動的型付け言語は型の評価をせずに、run()
というメソッドを持ってれば正常に実行できます。「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない」。
3-17. SOLID
SOLIDはオブジェクト指向プログラミングの分野において、ソフトウェア設計の5つの原則を記憶するための頭字語である。その5つの原則というのは、単一責任の原則、開放閉鎖の原則、リスコフの置換原則、インターフェース分離の原則と依存性逆転の原則です。
3-17-1. 単一責任の原則
単一責任の原則は1つのクラスに1つだけの責任を持たせるべきという原則です。「1つの責任」というのは少し曖昧ですので、実践では、あるクラスを変更する時の動機が2つ以上ある時は、単一責任と言えなくなります。例として、矩形を表すクラスRectangle
があるとして、GUIの描画機能と矩形の幾何学計算の2つのモジュールに使われています。ここのRectangle
クラスは単一責任の原則に違反しています。
3-17-2. 開放閉鎖の原則
開放閉鎖の原則は新しい要件に対して、コードを修正するのではなく、できるだけ拡張を行うべきという原則です。実践では、抽象化を用いてこの原則を実現することが多いです。Pythonのデコレーターは開放閉鎖の原則に則る機能で、既存メソッド、関数またはクラスを変更せずに新しい機能を実装できます。
3-17-3. リスコフの置換原則
リスコフの置換原則は親クラスが使われている箇所に、子クラスでも置換できるようにすべきという原則です。実践では、継承と多態性を用いてこの原則を実現しています。実例として、矩形を表すクラスRectangle
の子クラスとして、縦と幅が一致しないとエラーを起こすSquare
クラスがあります。そして、ある関数またはメソッドはRectangle
クラスを入力とし、内部で縦と幅に違い値を与えた場合、Square
クラスで置換できなくなるため、リスコフの置換原則に違反することになります。
3-17-4. インターフェース分離の原則
インターフェース分離の原則はクライアントに使わないメソッドへの依存関係を持たせるべきではないという原則です。言葉では理解しづらいが、下の例を見てください。
(出典:Agile Principles, Patterns, and Practices in C#)
この図はいくつかのクラスの関係を表しています。Door
クラスは、lock()
、un_lock()
やis_open()
のような扉と関連するメソッドを持っています。今度は、扉が一定時間開いていると、自動的に閉じるTimedDoor
を作ります。ここで、時間計測機能をTimerClient
というクラスに持たせ、Door
は直接TimerClient
を継承し、その機能を獲得します。そうすると、Door
を継承したTimedDoor
も時間計測機能を獲得できます。しかし、Door
は普通の扉で、時間計測機能は要らないので、インターフェース分離の原則に違反することになります。
解決策としては、以下のようなTimedDoor
の内部で、TimerClient
と接続するアダプターメソッドまたは変数を作成する方法とMixin継承の2種類の方法があります。
(出典:Agile Principles, Patterns, and Practices in C#)
3-17-5. 依存性逆転の原則
依存性逆転の原則は2つのルールを含みます。
- 上位モジュールは下位モジュールに依存してはならず、両方とも抽象に依存すべきです。
- 抽象は具体(実際の機能実現)に依存してはならず、具体は抽象に依存すべきです。
この原則はモジュール間のデカップリングのためのものです。例として以下のようなものがあります。
(出典:Agile Principles, Patterns, and Practices in C#)
ここの上位モジュールのPolicyLayer
は、下位モジュールのMechanismLayer
に依存し、下位モジュールのMechanismLayer
は実際の機能を実現するモジュールUtilityLayer
に依存しています。これは、依存性逆転の原則に違反するパターンです。
解決策として、以下のようなデザインができます。
(出典:Agile Principles, Patterns, and Practices in C#)
これで、PolicyLayer
は下位モジュールではなく、抽象インターフェースのPolicyServiceInterface
に依存するようになります。PolicyServiceInterface
と互換できるよう、MechanismLayer
は実装されます。
PolicyServiceInterface
が介在することで、PolicyLayer
とMechanismLayer
はお互い依存することなく、互換性を実現しました。MechanismServiceInterface
も同様です。抽象インターフェースは変更する可能性の低いもので、その介在によって各モジュールがデカップリングされます。
もう1つ例を挙げます。例えば、通常のPythonのWebアプリケーションでは、リクエスト→WSGIサーバー→WSGIアプリケーションという処理順序になります。ここのWSGIサーバーはGunicornやNginxのようなもので、WSGIアプリケーションはFlask、Djangoのようなものです。
GunicornはFlaskの実装を全く知らなくても、Flaskを呼び出すことが可能です。なぜなら、両方ともWSGIという抽象インタフェースに依存しているからです。ちなみに、ApacheはデフォルトではFlaskを呼び出すことはできません。しかし、mod_wsgiの拡張をインストールすればできるようになります。
WSGIの詳細の説明は割愛しますが、以下のコードを実行してみるのも良いでしょう。
fromwsgiref.simple_serverimportmake_serverdefapp(environ,start_response):start_response('200 OK',[('Content-type','text/plain')])return[b'Hello World']if__name__=="__main__":withmake_server('localhost',5000,app)ashttpd:httpd.serve_forever()
3-18. GRASP
GRASPは、「General Responsibility Assignment Software Pattern」というオブジェクト指向システムの設計方針です。GRASPは、情報エキスパート、生成者、コントローラ、疎結合性、高凝集性、多態性、純粋人工物、間接化、変動から保護という9つのパターンを例として示しています。
3-18-1. 情報エキスパート
情報エキスパートパターンは、ある責務を果たすために必要な情報を全部持っているクラスがあるなら、そのクラスに任せるべきとしています。例えば、ECサイトのシステムにショッピングカートのShopCar
、商品のSKU
の2つのクラスあるとします。そして、「ショッピングカートに重複した商品を許容しない」という機能を実装します。同一商品がどうかを判断するための情報としてSKUID
はSKU
クラスの中にあるので、情報エキスパートパターンに従い、この機能はShopCar
クラスではなく、必要な情報を全部持っているSKU
クラスに実装するべきです。
3-18-2. 生成者
生成者パターンはクラスAとクラスBがあるとして、以下の条件で1つ以上満たした時、BにAを作成させるべきとしています。この場合、BはAの生成者になります。
- BがAを含みます。
- BがAを集約しています。集約というのはhas-aの関係性です。
- BがAの初期化情報を持っています。
- BがAのインスタンスを記録します。
- Bは頻繁にAを使用します。
例えば、ECサイトのシステムで、商品SKU
を管理する注文Order
というクラスがある場合、SKU
の作成は、Order
の内部で行うべきです。
3-18-3. コントローラ
コントローラパターンは、システムイベントをコントローラというオブジェクトに制御させるべきとしています。このコントローラはクラス、システム、またはサブシステムで、UIやインターフェースとインタラクトしないものにすべきです。
例えば、Ruby on Railsに使用されるMVCというアーキテクチャの「C」はコントローラの略です。
3-18-4. 疎結合性
結合性はシステムの各コンポーネント間の依存関係の強弱を表す尺度です。疎結合性パターンは各コンポーネント間の依存関係を弱くするようにシステムを設計すべきとしています。依存関係を弱くするために、import
などを最小限にしたり、クラスメンバのアクセス権限を厳しくしたり、クラスをイミュータブルオブジェクトにしたりするような手法があります。
ECサイトのシステムを例とすると、例えば、商品のトータル価格を計算する機能を追加する時、新しいクラスなどを作成して、SKU
をimport
して、その金額を集計するメソッドを作るより、すでにSKU
と依存関係を持っているOrder
に追加したほうが、不必要な依存関係を作ることがなくなります。
3-18-5. 高凝集性
凝集性はあるオブジェクト(モジュール)の持っている責務(機能)間の関連性の強弱を表す尺度です。高凝集性パターンはオブジェクトに適切に責務を集中すべきとしています。
またECサイトのシステムを例としますが、注文データのDAOクラスOrderDAO
を作成し、データ保存用のメソッドSaveOrder()
を実装します。Excelに保存する機能とDBに保存する機能を実現したい時は、まとめてOrderDAO
に実装するより、それぞれ、違うクラスを実装し、OrderDAO
を継承して、仮想メソッド(Pythonでは抽象メソッドとして実装されているため以降抽象メソッドと記載する)のSaveOrder()
をオーバーライドしたほうがいいが凝集性が高くなります。
3-18-6. 多態性
多態性は3-13. ポリモーフィズムで紹介した概念で、ここではそれをパターン化して、システム設計のルールとしています。多態性パターンはクラスの変動しがちな部分を抽象的なメソッドなどとして実装し、ポリモーフィズムを持たせて、その具体的な実現は子クラスで実装すべきとしています。
例えば、Shape
という抽象クラスを作り、Draw()
という描画用の抽象メソッドを実装します。Shape
を継承して、矩形Rectangle
、円Round
をそれぞれ作り、内部でDraw()
をオーバーライドし、各自の描画機能を実現するのは多態性パターンに則った設計になります。こうすることで、次に菱形Diamond
を追加したい時は、システム構造を変えずに同じやり方で作成できます。
3-18-7. 純粋人工物
システムを設計する時、高凝集性と疎結合性は矛盾します。高凝集性はクラスを細分化して、責務をそれぞれに集中させるようにするが、それぞれのクラスは協力し合わないと、正常に動作しないので、どうしても結合性を高くしてしまいます。
純粋人工物は人工的なクラス、すなわち抽象クラスを作成し、凝集性と結合性をバランスを調整します。例えば、図形の描画機能の例ですが、今度はWindowsとLinux両方対応する機能を追加します。それぞれのOSのシステムコールや構造自体は違うので、描画機能Draw()
も違う形で実装しなければなりません。ここで、抽象基底クラスのAbstractShape
を追加することで、凝集性を下げず、結合性もそれほど上げないままシステムを実現できます。
3-18-8. 間接化
間接化パターンは、2つのクラスの間に仲介としたオブジェクトを設けることで、クラス間の結合性の軽減を促進する設計方法です。MVCアーキテクチャーでは、Model
に直接View
とやりとりさせず、間にContoroller
を置くのは間接化パターンに則った設計です。3-17-4. インターフェース分離の原則で紹介した中間にあるインターフェース抽象クラスも同じ思想の設計です。
3-18-9. 変動から保護
変動から保護パターンは3-17-2. 開放閉鎖の原則と類似しています。変動から保護するために、不安定な部分を統一したインターフェースでカプセル化します。そして、変化が生じた場合はインターフェースを変更するのではなく、追加をします。古いコードを変えなくても機能を拡張できるのが目的です。3-17-2. 開放閉鎖の原則で例として出したPythonのデコレーターの他に、ORMは典型的な変動から保護パターンで、DBを変更してもクライアント側に影響を与えることはないです
3-19. デザインパターン
デザインパターンは、オブジェクト指向プログラミングにおいての設計ノウハウです。前述のSOLIDとGRASPのような設計方針(Design Principle)と違って、デザインパターンは過去の開発者が案出した経験則のようなものです。
4. Pythonのオブジェクト指向の基本
オブジェクト指向に関する概念を説明しました。これから、4つのクラスを作って、Pythonのオブジェクト指向プログラミングの基本構造について見てみます。
- Animal:各種クラス変数、メソッドについて
- Dog:プロパティを定義する
property
について - Cat:プライベート変数とメソッドの継承・オーバーライドについて
- Tiger:クラスメンバの継承用の
super
について
4-1. クラスの変数とメソッド
Pythonのクラスには変数とメソッドがあります。そして、それぞれ色々な種類があります。
- 変数はクラス変数とインスタンス変数があります。
- メソッドはいクラスメソッド、インスタンスメソッド、静的メソッドがあります。
下のコードで、各種変数とメソッドの定義について、コメントで説明します。
fromtypesimportMethodTypeclassAnimal:# ここはクラス変数を定義する場所the_name="animal"# クラス変数def__init__(self,name,age):# イニシャライザself.name=name# インスタンス変数self.age=age# ここはメソッドを定義する場所defsleep(self):# インスタンスメソッドprint("{} is sleeping".format(self.name))defeat(self,food):# 引数付きのインスタンスメソッドprint("{} is eating {}".format(self.name,food))@classmethoddefspeak(cls,adjective):# クラスメソッドprint("I am a {} {}".format(adjective,cls.the_name))@staticmethoddefhappening(person,do):# 静的メソッドprint("{} is {}ing".format(person,do))defdrink_water(self):print("{} is drinking water".format(self.name))
検証:
adam=Animal(name="Adam",age=2)# インスタンス化print('adam.the_name: {}'.format(adam.the_name))# インスタンスからクラス変数を呼び出す# 実行結果:adam.the_name: animalprint('Animal.the_name: {}'.format(Animal.the_name))# クラスからクラス変数を呼び出す# 実行結果:adam.name: Adamprint('adam.name: {}'.format(adam.name))# インスタンス変数を呼び出す# 実行結果:Animal.the_name: animaladam.sleep()# インスタンスメソッドを呼び出す# 実行結果:Adam is sleepingadam.eat("meat")# 引数付きのインスタンスメソッドを呼び出す# 実行結果:Adam is eating meatadam.speak("happy")# インスタンスからクラスメソッドを呼び出す# 実行結果:I am a happy animalAnimal.speak("sad")# クラスからクラスメソッドを呼び出す# 実行結果:I am a sad animaladam.happening("Tim","play")# インスタンスから静的メソッドを呼び出す# 実行結果:Tim is playingAnimal.happening("Mary","watch")# クラスから静的メソッドを呼び出す# 実行結果:Mary is watchingAnimal.the_name="Animal"# クラス変数を修正print('adam.the_name: {}'.format(adam.the_name))# 実行結果:adam.the_name: Animaladam.the_name="animal"# インスタンスから修正print('Animal.the_name: {}'.format(Animal.the_name))# 実行結果:Animal.the_name: Animaladam.age=3# インスタンス変数を修正# メソッドのバインディング(モンキーパッチ)adam.drink_water=MethodType(drink_water,adam)# インスタンスにバインディングするadam.drink_water()# 実行結果:Adam is drinking waterprint(adam.drink_water)# 実行結果:<bound method drink_water of <__main__.Animal object at 0x7ffd68064310>>try:Animal.drink_waterexceptAttributeErrorase:print(e)# 実行結果:type object 'Animal' has no attribute 'drink_water'Animal.drink_water=MethodType(drink_water,Animal)# クラスにバインディングするadam.drink_water()# 実行結果:Adam is drinking waterAnimal.drink_water=drink_water# 直接代入でメソッドをバインディングするadam.drink_water()# 実行結果:Adam is drinking water
- クラス変数はクラスが持つ変数で、クラスとインスタンス両方で使えます。
- インスタンス変数は各インスタンスに所属するもので、そのインスタンスのみ使用できます。
- インスタンスメソッドはインスタンスが使うメソッドで、
self
というインスタンス自身を指す引数を定義する必要があります。 - クラスメソッドはクラスとインスタンス両方が使えるメソッドで、
cls
というクラスを指す引数を定義する必要があります。 - 静的メソッドはクラス内部で管理する普通の関数で、クラスとインスタンス両方が使えます。
- クラスからクラス変数を修正すると、インスタンスから呼び出す時に変更されます。
- インスタンスからクラス変数を修正すると、他のクラスやインスタンスに影響を与えません。
- メソッドのモンキーパッチは
MethodType
か直接代入で実現できます。 - インスタンスにメソッドをバインディングすると、元のクラスや他のインスタンスはバインディングされたメソッドが使えません。クラスににバインディングすると、全てのインスタンス(バインディングする前に作成したインスタンスも含む)に伝播します。
4-2. プロパティ
Animal
を継承したDog
クラスを作成し、property
やそれに関連するデコレーターを見てみます。これらのデコレーターはメソッドをプロパティ(変数)に変換するもので、以下の2つのメリットがあります。
- インスタンス変数のように
()
なしで呼び出せます。 - 変数の評価機能などの動的な処理を追加でき、合法性を保証できます。
デコレーター以外に、property
関数で上記の処理を実現できる方法もあります。
fromfunctoolsimportcached_propertyclassDog(Animal):# クラスの継承defeating(self):print("{} is eating".format(self.name))@propertydefrunning(self):ifself.age>=3andself.age<130:print("{} is running".format(self.name))elifself.age>0andself.age<3:print("{} can't run".format(self.name))else:print("please input true age")@property# プライベートな変数を取得するdefcountry(self):returnself._country@country.setter# メソッド名.setterdefcountry(self,value):# プライベートな変数に値を代入するself._country=value@country.deleter# メソッド名.deleterdefcountry(self):# プライベートな変数に値を削除するdelself._countryprint("The attr country is deleted")# property関数で上記のデコレーターと同じ機能を実現defget_city(self):returnself._citydefset_city(self,value):self._city=valuedefdel_city(self,value):delself._citycity=property(get_city,set_city,del_city,"I'm the'city' property.")@cached_property# キャッシュされるpropertydefofficial_name(self):return'Mr.{} - the Dog'.format(self.name)
検証:
david=Dog("David",2)david.eating()# 実行結果:David is eatingdavid.running# ()なしで呼び出す# 実行結果:David can't rundean=Dog("Dean",4)dean.running# 実行結果:Dean is running# デコレーターによる方法david.country="America"print(david.country)# 実行結果:Americadeldavid.country# 実行結果:The attr country is deleted# property関数による方法david.city="NewYork"print(david.city)# 実行結果:NewYork# キャッシュされるpropertyprint(david.official_name)# 実行結果:Mr.David - the Dog
@property
デコレーターはメソッドを変数に変換します。property
関数でも同じ処理を実現できます。4番目の引数"I'm the 'city' property."
という文字列はドキュメントで、Dog.city.__doc__
で確認できます。@cached_property
はPython 3.8で実装された値がキャッシュされるproperty
です。計算量の高い変数処理をする時、キャッシュされると再計算が必要なくなるので性能向上に繋がります。
4-3. プライベート変数とメソッドの継承・オーバーライド
Cat
クラスとその子クラスBlackCat
を定義し、プライベート変数とメソッドの継承・オーバーライドについて見ていきます。
- プライベートな変数は外部から使うことが制限される変数です。
- 子クラスは親クラスを継承する時、親クラスのメソッドを全部継承しますが、子クラスの中で同じ名前のメソッドを定義すると、継承されたメソッドがオーバーライドされます。イニシャライザメソッドの
__init__
も同様です。
classCat(Animal):def__init__(self,weight):# 親クラスの__init__をオーバーライドself.__weight=weightself._weight=weight+1self.weight=self._weight+1defget_weight(self):print("My _weight is {}kg".format(self._weight))defget_real_weight(self):print("Actually my __weight is {}kg".format(self.__weight))classBlackCat(Cat):defget_weight(self):# 親クラスのメソッドをオーバーライドprint("My weight is {}kg".format(self.weight))defget_real_weight(self):print("Actually my _weight is {}kg".format(self._weight))defget_actual_weight(self):print("My __weight is exactly {}kg".format(self.__weight))
検証:
cole=Cat(5)print("Cole's weight: {}kg".format(cole.weight))# 実行結果:Cole's weight: 7kg# _xは外部からの利用を推奨しないプライベート変数で、利用すること自体は制限されないprint("Cole's _weight: {}kg".format(cole._weight))# 実行結果:Cole's _weight: 6kg# __xは外部からの利用をを禁止するプライベート変数で、利用することは制限され、_<class>__xの形で強制的に呼び出せるprint("Cole's __weight: {}kg".format(cole._Cat__weight))# 実行結果:Cole's __weight: 5kgcole.get_real_weight()# メソッドで内部から__xを利用できる# 実行結果:Actually my __weight is 5kgcain=BlackCat(5)cain.get_weight()# 実行結果:My weight is 7kg# _xは制限されないため、子クラスからでも呼び出せるcain.get_real_weight()# 実行結果:Actually my _weight is 6kg# 親クラスのプライベート変数の__xを子クラスの内部から素直な方法では利用できないtry:cain.get_actual_weight()exceptAttributeErrorase:print(e)# 実行結果:'Blackcat' object has no attribute '_Blackcat__weight'
weight
は普通の変数で、外部から利用できます。_weight
のような1つのアンダースコアが付いてる変数は外部からの利用を推奨しないプライベート変数で、利用すること自体は制限されません。ただし、オブジェクト名(クラス名、関数名、モジュールスコープの変数名など)にする場合、from module import *
ではimportされません。__weight
のような2つのアンダースコアが付いてる変数は外部からの利用を禁止するプライベート変数です。ただし、<class>._<class>__x
の形で強制的に呼び出せます。継承による属性の衝突を避けたい場合に使用するべきです。- 変数名のパターンによる違う動作の実現は「名前修飾(Name Mangling)」と言います。
- 子クラスの中で親クラスが持っているメソッドと同じ名前のメソッドを定義すると、オーバーライドすることができます。
4-4. クラスメンバの継承
Tiger
とWhiteTiger
を定義し、super
の使い方について見ていきます。super
は子クラスの中で親クラスの変数やメソッドを呼び出すための関数です。
classTiger(Animal):defspeak(self):return"I'm a tiger not Lulu's song"defeat(self):return"{} is eating".format(self.name)classWhiteTiger(Tiger):def__init__(self,name,age,height):super().__init__(name,age)self.height=heightdefspeak(self):returnsuper().speak().replace("tiger",'white tiger')defeat(self):returnsuper().eat()
検証:
tony=WhiteTiger("Tony",10,100)print(tony.eat())# 実行結果:Tony is eatingprint(tony.speak())# 実行結果:I'm a white tiger not Lulu's song
return super().eat()
は親クラスのeat
メソッドを返しているだけで、子クラスの中でeat
メソッドを定義しなければsuper
を使う必要がありません。super().__init__(name, age)
は、親クラスのイニシャライザ__init__
を実行します。これがないと、self.name
とself.age
を呼び出せません。super().__init__(name, age)
と同等な書き方は以下のようにいくつかあります。
1. 親クラスの変数を再定義します。
def__init__(self,name,age,height):self.name=nameself.age=ageself.height=height
2. 親クラスの__init__
を明示的に呼び出します。親クラスの名前を変えると、呼び出された箇所を全部修正しなければなりません。
def__init__(self,name,age,height):Tiger.__init__(self,name,age)self.height=height
5. Pythonのオブジェクト指向の発展
Pythonのオブジェクト指向プログラミングの基本的な形式を見てきました。実務においても、4. pythonのオブジェクト指向の基本の内容でほぼ事足ります。しかし、高度な機能の実現、モジュールの自作またはデザインパターンに則った綺麗なシステムを作成したいなら、もう少し発展した内容を知る必要があります。
5-1. 特殊メソッド
3-14. 演算子オーバーロードで少し触れましたが、Pythonのクラスには__init__
のような、前後に2つのアンダースコアの付いた「特殊メソッド」、「マジックメソッド」または「__dunder__
(ダンダー:ダブルアンダースコア)」と呼ばれるメソッドや変数がたくさん存在します。これらのメソッドや変数は一部または全てのオブジェクト共通のもので、様々な機能を実現できます。
importcollectionsimportcopyimportmathimportoperatorimportpickleimportsysimportasyncioclassDunder:def__abs__(self):# abs(Dunder()); 絶対値を計算する時に呼び出されるreturnself.xdef__add__(self,other):# Dunder() + 123; 加算をする時に呼び出されるreturnself.x+otherasyncdef__aenter__(self):# `__aenter__`と`__aexit__`は一緒に実装しなければならない# async with Dunder() as coro; awaitable object限定awaitasyncio.sleep(1)asyncdef__aexit__(self,exc_type,exc_val,exc_tb):# `__aenter__`と`__aexit__`は一緒に実装しなければならない# async with Dunder() as coro; awaitable object限定awaitasyncio.sleep(1)def__aiter__(self):# `__aiter__`と`__anext__`は一緒に実装しなければならない# async for _ in Dunder()returnselfdef__and__(self,other):# Dunder() & 123; 論理積演算をする時に呼び出されるreturnself.x&otherasyncdef__anext__(self):# `__aiter__`と`__anext__`は一緒に実装しなければならない# async for _ in Dunder(); 要素がなくなったら、StopAsyncIterationを引き起こすべき# awaitable object限定val=awaitself.readline()ifval==b'':raiseStopAsyncIterationreturnvaldef__await__(self):# await Dunder(); 戻り値はiterator限定returnself.z# `__next__`と`__iter__`を実装したクラスdef__call__(self,*args,**kwargs):# Dunder()(); callable(Dunder()) == True; 関数のように呼び出せるreturnself.xdef__init__(self,**kwargs):# Dunder(y=2); イニシャライザself.x=1self.y=kwargs.get('y')self.z=[1,2,3]def__bool__(self):# bool(Dunder()) == True; ブール演算する時に呼び出されるreturnTruedef__bytes__(self):# bytes(Dunder()); バイト列returnbytes('123',encoding='UTF-8')def__ceil__(self):# math.ceil(Dunder()); 切り上げ計算する時に呼び出されるreturnmath.ceil(self.x)def__class_getitem__(cls,item):# Dunder[int] == "Dunder[int]"; このメソッドは自動的にクラスメソッドになるreturnf"{cls.__name__}[{item.__name__}]"def__complex__(self):# complex(Dunder()); 複素数returncomplex(self.x)def__contains__(self,item):# item not in Dunder(); item in Dunder()returnTrueifiteminself.zelseFalsedef__copy__(self):# copy.copy(Dunder()); 浅いコピーをする時に呼び出されるreturncopy.copy(self.z)def__deepcopy__(self,memodict={}):# copy.deepcopy(Dunder()); 深いコピーをする時に呼び出されるreturncopy.deepcopy(self.z)def__del__(self):# dunder = Dunder(); del dunder;# オブジェクトを削除する時に呼び出される。ガベージコレクションにも対応delselfdef__delattr__(self,item):# del self.params; インスタンス変数を削除する時に呼び出されるdelself.itemdef__delete__(self,instance):# class Owner: dunder = Dunder()# del Owner().medusa; ディスクリプタメソッド# 所有者クラスの属性として削除する時に呼び出されるdelself.xdef__delitem__(self,key):# del Dunder()['some_key']self.__dict__.pop(key)def__dir__(self):# dir(Dunder()); オブジェクトの全ての属性を格納するiterable objectを返すreturnsuper().__dir__()def__divmod__(self,other):# divmod(Dunder(), 123); 割り算の商と余りを同時に取得returndivmod(self.x,other)def__enter__(self):# with Dunder() as dunder: passreturnselfdef__eq__(self,other):# Dunder() == 123; 等価演算をする時に呼び出されるreturnself.x==otherdef__exit__(self,exc_type,exc_val,exc_tb):# with Dunder() as dunder: pass; 引数はそれぞれTypeError、ValueError、TracebackreturnTruedef__float__(self):# float(Dunder()); 浮動小数にするreturnfloat(self.x)def__floor__(self):# math.floor(Dunder()); 小数点を切り捨てるreturnmath.floor(self.x)def__floordiv__(self,other):# Dunder() // 123; 切り捨て除算する時に呼び出されるreturnself.x//otherdef__format__(self,format_spec):# '{:x}'format(Dunder()); format(Dunder(), 'x')ifformat_spec=='x':return'{}'.format(self.x)return'{}'.format(self.y)def__fspath__(self):# os.fspath(Dunder()) == '/var/www/html/mysite'; ファイルシステムパスを返すreturn'/var/www/html/mysite'def__ge__(self,other):# Dunder() >= 123returnself.x>=otherdef__get__(self,instance,owner):# class Test: dunder = Dunder(); ディスクリプタメソッド# `Test().dunder`または`Test.dunder`をする時にの時に呼び出されるreturnself.xdef__getattr__(self,item):# Dunder().a; 未定義のメンバーにアクセスする時に呼び出されるreturnf'object has no attribute"{item}"'def__getattribute__(self,item):# Dunder().a; 未定義・定義済みにかかわらず、全てのメンバーにアクセスする時に呼び出される# `return self.x`などすると無限ループになるのでご注意くださいreturnsuper().__getattribute__(item)def__getitem__(self,item):# Dunder()[item]returnself.__dict__.get(item)def__getnewargs__(self):# pickle.loads(pickle.dumps(Dunder())); unPickleする時に、`__new__`メソッドに渡される引数を定義できる# Python 3.6以前にpickle protocol 2または3を利用する時に使われる# Python 3.6以降にpickle protocol 2または3を利用する時に`__getnewargs_ex__`が使われる# 直接呼び出されるわけではなく、`__reduce__`メソッドを構成しているreturn(2*self.x,)def__getstate__(self):# pickle.dumps(Dunder()); Pickle処理する時に、オブジェクトの状態を取得できる# 直接呼び出されるわけではなく、`__reduce__`メソッドを構成しているreturnself.__dict__.copy()def__gt__(self,other):# Dunder() > 123returnself.x>123def__hash__(self):# hash(Dunder()); ハッシュ値を計算する時に呼び出されるreturnhash(self.x)def__iadd__(self,other):# dunder = Dunder(); dunder += 123; in-placeの加算をする時に呼び出されるself.x+=otherreturnselfdef__iand__(self,other):# dunder = Dunder(); dunder &= 123; in-placeの論理積演算をする時に呼び出されるself.x&=otherreturnselfdef__ifloordiv__(self,other):# dunder = Dunder(); dunder //= 123; in-placeの切り捨て除算をする時に呼び出されるself.x//=otherreturnselfdef__ilshift__(self,other):# dunder = Dunder(); dunder <<= 123; in-placeのビット左シフトを計算する時に呼び出されるself.x<<=otherreturnselfdef__imatmul__(self,other):# dunder = Dunder(); dunder @= 123; in-placeのバイナリ演算をする時に呼び出される# numpyではドット積として実装しているself.x@=other# 標準ライブラリでは機能未実装returnselfdef__imod__(self,other):# dunder = Dunder(); dunder %= 123; in-placeの剰余演算をする時に呼び出されるself.x%=otherreturnselfdef__imul__(self,other):# dunder = Dunder(); dunder *= 123; in-placeの乗算をする時に呼び出されるself.x*=123returnselfdef__index__(self):# slice(Dunder(), Dunder() * 2); bin(Dunder()); hex(Dunder()); oct(Dunder())# operator.index(Dunder()); 戻り値は整数限定で、`operator.index`関数から呼び出される# また、整数を必要とする`slice`、`bin()`、`hex()`、`oct()`はこのメソッドを呼び出すreturnself.xdef__init_subclass__(cls,**kwargs):# class Test(Dunder, **kwargs): ...; 継承される時に呼び出されるsuper().__init_subclass__()cls.x=kwargs.get('x',1)def__instancecheck__(self,instance):# class MetaClass(type):# def __new__(cls, name, bases, namespace):# return super().__new__(cls, name, bases, namespace)## def __instancecheck__(self, other):# return True## class Test(metaclass=MetaClass): ...# isinstance(int, Test) == True# このメソッドはクラスのタイプ(メタクラス)で定義しないと呼び出されない# また、`type(other) == self`の場合は直接Trueになり、呼び出されないpassdef__int__(self):# int(Dunder()); 整数に変換する時に呼び出されるreturnint(self.x)def__invert__(self):# ~Dunder(); ビット反転を計算する時に呼び出されるreturn~self.xdef__ior__(self,other):# dunder = Dunder(); dunder |= 123; in-placeの論理和演算をする時に呼び出されるself.x|=otherreturnselfdef__ipow__(self,other):# dunder = Dunder(); dunder ** 2; in-placeの冪乗を計算する時に呼び出されるself.x**otherreturnselfdef__irshift__(self,other):# dunder = Dunder(); dunder >>= 2; in-placeのビット右シフトを計算する時に呼び出されるself.x>>=otherreturnselfdef__isub__(self,other):# dunder = Dunder(); dunder -= 2; in-placeの減算をする時に呼び出されるreturnselfdef__iter__(self):# dunder = iter(Dunder()); next(dunder); iterable objectを作成するためのメソッド# `__next__`と一緒に実装しなければならないself._i=0returnself.z[self._i]# self.zはリストとして定義しているdef__itruediv__(self,other):# dunder = Dunder(); dunder /= 123; in-placeの除算をする時に呼び出されるself.x/=otherreturnselfdef__ixor__(self,other):# dunder = Dunder(); dunder ^= 123; in-placeの排他的論理和演算をする時に呼び出されるself.x^=otherreturnselfdef__le__(self,other):# dunder = Dunder(); dunder <= 123returnself.x<=otherdef__len__(self):# len(Dunder())returnlen(self.z)def__lshift__(self,other):# Dunder() << 123; ビット左シフトを計算する時に呼び出されるreturnself.x<<otherdef__lt__(self,other):# Dunder() < 123returnself.x<otherdef__matmul__(self,other):# Dunder() @ 123; バイナリ演算をする時に呼び出されるreturnself.x@other# 標準ライブラリでは機能未実装def__missing__(self,key):# class Dict(dict):# def __missing__(self, key):# return f'__missing__({key})'# dunder = Dict({'key': 1})# print(dunder['unk_key'])# 辞書内にキーが存在しない時に呼び出されるメソッドpassdef__mod__(self,other):# Dunder() % 123; 剰余演算をする時に呼び出されるreturnself.x%otherdef__mro_entries__(self,bases):# クラス定義の親リストにクラスオブ ジェクトではないものが指定された時に呼ばれる# 型アノテーションの実装で、継承関係を正しくするためのメソッド# https://www.python.org/dev/peps/pep-0560/#mro-entriespassdef__mul__(self,other):# Dunder() * 123; 乗算をする時に呼び出されるreturnself.x*ohterdef__ne__(self,other):# Dunder() != 123; 不等価演算をする時に呼び出されるreturnself.x!=otherdef__neg__(self):# -Dunder(); 反数を計算する時に呼び出されるreturn-self.xdef__new__(cls,*args,**kwargs):# Dunder(); コンストラクタ# __init__や他のインスタンスメソッドで使われるself(インスタンスそのもの)を作成するreturnsuper().__new__(cls)def__next__(self):# dunder = iter(Dunder()); next(dunder); iterable objectを作成するためのメソッド# `__iter__`と一緒に実装しなければならないself._i+=1returnself.z[self._i]def__or__(self,other):# Dunder() | 132; 論理和演算をする時に呼び出されるreturnself.x|otherdef__pos__(self):# +Dunder(); 正数に変換する時に呼び出されるreturn+self.xdef__post_init__(self):# データクラス用のメソッドで、`__init__`が定義されている場合のみ、`__init__`の後に呼び出されるpassdef__pow__(self,power,modulo=None):# Dunder() ** 123; 冪乗を計算する時に呼び出されるifmodulo:returnself.x**power%moduloelse:returnself.x**power@classmethoddef__prepare__(metacls,name,bases,**kwargs):# class MetaClass(type):# def __new__(cls, name, bases, namespace):# return super().__new__(cls, name, bases, namespace)## @classmethod# def __prepare__(cls, name, bases, **kwargs):# return dict()## class Test(metaclass=MetaClass): ...# namespace = MetaClass.__prepare__(name, bases, **kwargs)# クラス本体を評価する前に呼び出されて、クラスメンバを格納する辞書形オブジェクト(名前空間)を返す# 通常`types.prepare_class`と一緒に使用する# このメソッドはメタクラスでクラスメソッドとして定義しないと呼び出されないreturncollections.OrderedDict()def__radd__(self,other):# 123 + Dunder(); 被演算子が反射した加算をする時に呼び出されるreturnother+self.xdef__rand__(self,other):# 123 & Dunder(); 被演算子が反射した論理積演算をする時に呼び出されるreturnother&self.xdef__rdiv__(self,other):# 123 / Dunder(); 被演算子が反射した除算をする時に呼び出されるreturnother/self.xdef__rdivmod__(self,other):# divmod(123, Dunder()); 被演算子が反射した割り算の商と余りを同時に取得returndivmod(other,self.x)def__reduce__(self):# pickle.dumps(Dunder())# `__getstate__`、`__setstate__`、`__getnewargs__`を利用し、Pickleの挙動をコントロールできる# なるべく`__reduce__`を直接定義せず、上記のメソッドを定義するこること# 後方互換の`__reduce_ex__`が定義されると優先的に使用されるreturnsuper().__reduce__()# return super().__reduce_ex__(protocol)def__repr__(self):# repr(Dunder()); オブジェクトの印字可能な表現を含む文字列を返すreturnsuper().__repr__()def__reversed__(self):# reversed(Dunder()); 反転したiterator objectを返すnew_instance=copy.deepcopy(self)new_instance.z=new_instance.z[::-1]returnnew_instancedef__rfloordiv__(self,other):# 123 // Dunder(); 被演算子が反射した切り捨て除算をする時に呼び出されるreturnother//self.xdef__rlshift__(self,other):# 123 << Dunder(); 被演算子が反射したビット左シフトを計算する時に呼び出されるreturn'__rlshift__'def__rmatmul__(self,other):# 123 @ Dunder(); 被演算子が反射したバイナリ演算をする時に呼び出されるreturnother@self.x# 標準ライブラリでは機能未実装def__rmod__(self,other):# 123 % Dunder(); 被演算子が反射した剰余演算をする時に呼び出されるreturnother%self.xdef__rmul__(self,other):# 123 * Dunder(); 被演算子が反射した乗算をする時に呼び出されるreturnother*self.xdef__ror__(self,other):# 123 | Dunder(); 被演算子が反射した論理和演算をする時に呼び出されるreturnother|self.xdef__round__(self,n=None):# round(Dunder()); 四捨五入returnround(self.x)def__rpow__(self,other):# 123 ** Dunder(); 被演算子が反射した冪乗を計算する時に呼び出されるreturnother**self.xdef__rrshift__(self,other):# 123 >> Dunder(); 被演算子が反射したビット右シフトを計算する時に呼び出されるreturnother>>self.xdef__rshift__(self,other):# Dunder() >> 123; ビット右シフトを計算する時に呼び出されるreturnself.x>>otherdef__rsub__(self,other):# 123 - Dunder(); 被演算子が反射した減算をする時に呼び出されるreturnother-self.xdef__rtruediv__(self,other):# 123 / Dunder(); 被演算子が反射した除算をする時に呼び出されるreturnother/self.xdef__rxor__(self,other):# 123 ^ Dunder(); 被演算子が反射した排他的論理和を計算する時に呼び出されるreturnother^self.xdef__set__(self,instance,value):# class Test: dunder = Dunder(); ディスクリプタメソッド# `Test().dunder=123`または`Test.dunder=123`をする時にの時に呼び出されるinstance.x=valuedef__set_name__(self,owner,name):# ディスクリプタの変数名のアサイン# class Test: pass; オーナークラスが作成される時に自動的に呼び出されるが、# dunder = Dunder(); 後でバインディングする時は明示的に呼び出す必要がある# Test.dunder = dunder# dunder.__set_name__(Test, 'dunder')# dunderというディスクリプタをTestクラスの命名空間の'dunder'にアサインするowner.__dict__[name]=selfdef__setattr__(self,key,value):# dunder = Dunder(); dunder.x = 123; 属性設定する時に呼び出されるself.__dict__[key]=valuedef__setitem__(self,key,value):# dunder = Dunder(); dunder['x'] = 123; ; 添字で属性を設定する時に呼び出されるself.__dict__[key]=valuedef__setstate__(self,state):# pickle.loads(pickle.dumps(Dunder()))# unPickleする時に、`__getstate__`で取得しといたオブジェクトの状態を利用できる# 直接呼び出されるわけではなく、`__reduce__`メソッドを構成しているself.__dict__.update(state)def__sizeof__(self):# sys.getsizeof(Dunder()); オブジェクトのサイズを返すreturnsuper().__sizeof__()def__str__(self):# str(Dunder())# print(Dunder())# オブジェクトの文字列表現を定義するreturnf'{self.x}'def__sub__(self,other):# Dunder() - 123; 減算をする時に呼び出されるreturnself.x-otherdef__subclasscheck__(self,subclass):# class MetaClass(type):# def __new__(cls, name, bases, namespace):# return super().__new__(cls, name, bases, namespace)## def __subclasscheck__(self, subclass):# return True## class Test(metaclass=MetaClass): ...# issubclass(int, Test) == True# このメソッドはクラスのタイプ(メタクラス)で定義しないと呼び出されないreturnNotImplemented@classmethoddef__subclasshook__(cls,subclass):# class Test: x = 1; # クラス変数を定義# issubclass(Test, Dunder) == True# このメソッドは仮想基底クラスのクラスメソッドとして定義しなければならないifclsisDunder:returnhasattr(subclass,'x')def__truediv__(self,other):# Dunder() // 123; 切り捨て除算をする時に呼び出されるreturnself.x//otherdef__trunc__(self):# math.trunc(Dunder()); 端数処理をする時に呼び出されるreturnint(self.x)def__xor__(self,other):# Dunder() ^ 123; 排他的論理和演算をする時に呼び出されるreturnself.x^other
上記のものは一般的な特殊メソッドです。全てを覚える必要はなく、こういうものもあったなぐらいでちょうど良いと思います。その他に、もう少し特殊な属性やメソッドも存在します。
属性 | 意味 |
---|---|
__dict__ | オブジェクトの (書き込み可能な) 属性を保存するために使われる辞書またはその他のマッピングオブジェクトです。ビルトイン関数vars()でその辞書を参照できます。 |
__class__ | クラスインスタンスが属しているクラスです。 |
__bases__ | クラスオブジェクトの基底クラス(親クラス)のタプルです。 |
__name__ | クラス、関数、メソッド、デスクリプタ、ジェネレータインスタンスまたはモジュールの名前です。 |
__qualname__ | クラス、関数、メソッド、デスクリプタ、ジェネレータインスタンスの修飾名です。 |
__mro__ | この属性はメソッドの解決時に基底クラス(親クラス)を探索する時に考慮されるクラスのタプルです。 |
mro() | このメソッドは、メタクラスによって、そのインスタンスのメソッド解決の順序をカスタマイズするために、上書きされるかも知れません。このメソッドはクラスのインスタンス化時に呼ばれ、その結果は__mro__に格納されます。 |
__subclasses__ | それぞれのクラスは、それ自身の直接のサブクラスへの弱参照を保持します。このメソッドはそれらの参照のうち、生存しているもののリストを返します。 |
__doc__ | クラスや関数のドキュメンテーション文字列で、ドキュメンテーションがない場合はNoneになります。サブクラスに継承されません。 |
__module__ | クラスや関数が定義されているモジュールの名前です。モジュール名がない場合はNoneになります。 |
__defaults__ | デフォルト値を持つ引数に対するデフォルト値が収められたタプルで、デフォルト値を持つ引数がない場合にはNoneになります |
__code__ | コンパイルされた関数本体を表現するコードオブジェクトです。 |
__globals__ | 関数のグローバル変数の入った辞書 (への参照) です --- この辞書は、関数が定義されているモジュールのグローバルな名前空間を決定します。 |
__closure__ | Noneまたは関数の個々の自由変数 (引数以外の変数) に対して値を束縛しているセル(cell)群からなるタプルになります。セルオブジェクトは属性 cell_contents を持っています。 これはセルの値を設定するのに加えて、セルの値を得るのにも使えます。 |
__annotations__ | 型アノテーション情報が入った辞書です。辞書のキーはパラメータ名で、返り値の注釈がある場合は、'return'がそのキーとなります。 |
__kwdefaults__ | キーワード専用パラメータのデフォルト値を含む辞書です。 |
__slots__ | このクラス変数には、インスタンスが用いる変数名を表す、文字列、イテラブル、または文字列のシーケンスを代入できます。slots は、各インスタンスに対して宣言された変数に必要な記憶領域を確保し、dict とweakref が自動的に生成されないようにします。 |
__weakref__ | 主にガベージコレクションのための属性で、弱参照を格納しています。 |
__func__ | クラスメソッドが持つ属性で、メソッドの実体である関数オブジェクトを返します。 |
__self__ | クラスメソッドが持つ属性で、自身の所属するオブジェクトを返します。 |
__isabstractmethod__ | 抽象基底クラスにおいて、抽象メソッドかどうかを判断するための属性です。 |
__members__ | 列挙型クラス専用の属性で、各要素を保存するために使われる辞書です。 |
__loader__ | from package import * の時に、import すべきモジュール名をリストとして限定できます。 |
__package__ | パッケージの場合は__name__ に、パッケージじゃない場合はトップレベルのモジュールは空の文字列に、サブモジュールは親パッケージの__name__ にすべきです。 |
__spec__ | python -m <module> <file> の時に、パッケージやモジュールのスペック情報を格納する属性です。 |
__path__ | importする時にモジュールを探す場所で、リストとして定義できます。__path__ を定義すると、モジュールがパッケージになります。 |
__file__ | モジュールの絶対パスを格納する変数です。 |
__cached__ | .pyc ファイルとしたコンパイルされたパッケージのパスを格納する変数です。 |
__all__ | from package import * の時に、import すべきモジュール名をリストとして限定できます。 |
表で示したものの一部は関数オブジェクトが所有する属性です。Pythonは全てがオブジェクトなので、関数もオブジェクトになり、第一級関数であるプログラミング言語です。その他に、モジュールに使われる属性もありますが、__init__.py
ファイルの中に定義して使うことができます。また、上記の表で示したもの以外に、特定のモジュールに使われている属性もあります。
クラスのメンバを参照したい時は、vars()
とdir()
が使えます。vars()
はオブジェクトの__dict__
属性を参照しますので、継承されたメンバは表示されません。それに対して、dir()
はオブジェクトの__dir__
メソッドを呼び出します。__dir__
メソッドのデフォルトの実装はスコープ内にある名前を全部返すため、継承されたメンバも取得できます。そして、メンバの値も一緒に参照したい時はinspect.getmembers()
が使えます。inspect.getmembers()
はメンバとその値を格納したリストを返します。また、inspect.getmembers(obj, inspect.ismethod)
で、メソッドだけ絞り込むこともできます。他にも、is
から始まるinspectモジュールの関数がありまして、それらを使用して特定のメンバを取得できます。詳しくはドキュメントを参照してください。
5-2 タイプとオブジェクト
Pythonのtype
とobject
は「鶏が先か、卵が先か」のような関係性を持っています。つまり、どれが先かははっきり説明できないです。そして、type
とobject
は共生関係で、常に同時に出てきます。
まず、Pythonは「全てがオブジェクト」のプログラミング言語です。そして、3. オブジェクト指向に関する概念で紹介したように、オブジェクト指向の枠組みには主に以下の2種類の関係性が存在します。
- 継承関係。子クラスは親クラスを継承し、is-aの関係性を作ります。例えば、
reptile
を継承したsnake
クラスがあるとして、「snake is a kind of reptile」と言えます。親クラスを参照したい時は、__base__
が使用できます。 - クラス・インスタンス関係。あるタイプのクラスをインスタンス化するとこの関係が生まれます。例えば、
Squasher
というsnake
のインスタンスを作ることができ、「Squasher is an instance of snake」と言えます。ここのsnake
はSquasher
のタイプクラスと定義します。インスタンスのタイプクラスを参照したい時は、__class__
か、type()
関数が使用できます。
次に、type
とobject
を見てみます。
print(object)# 実行結果:<class 'object'>print(type)# 実行結果:<class 'type'>
Pythonの世界では、object
は継承関係の頂点であり、全てのクラスの親クラスになります。それに対して、type
はクラス・インスタンス関係の頂点で、全てのオブジェクトのタイプクラスになります。2者の関係性を「object is an instance of type」と表現できます。
print(object.__class__)# 実行結果:<class 'type'>print(object.__bases__)# 継承関係の頂点なので、それ以上は存在しない# 実行結果:()print(type.__class__)# type自身もtypeのインスタンス# 実行結果:<class 'type'>print(type.__bases__)# 実行結果:(<class 'object'>,)
続いて、list
、dict
、tuple
などのビルトインデータクラスについて見てみます。
print(list.__bases__)# 実行結果:(<class 'object'>,)print(list.__class__)# 実行結果:<class 'type'>print(dict.__bases__)# 実行結果:(<class 'object'>,)print(dict.__class__)# 実行結果:<class 'type'>print(tuple.__bases__)# 実行結果:(<class 'object'>,)print(tuple.__class__)# 実行結果:<class 'type'>
同じく、親クラスはobject
で、type
のインスタンスになります。list
をインスタンス化して検証してみましょう。
mylist=[1,2,3]print(mylist.__class__)# 実行結果:<class 'list'>print(mylist.__bases__)# 実行結果:# ---------------------------------------------------------------------------# AttributeError Traceback (most recent call last)# <ipython-input-21-0b850541e51b> in <module># ----> 1 print(mylist.__bases__)## AttributeError: 'list' object has no attribute '__bases__'
インスタンス化したlist
には親クラスがないらしいです。次に、自分でクラスを定義して、そのインスタンスについて見てみましょう。
classC:# Python3ではクラスはデフォルトでobjectを継承するpassprint(C.__bases__)# 実行結果:(<class 'object'>,)c=C()print(c.__class__)# 実行結果:<class '__main__.C'>print(c.__bases__)# 実行結果:# ---------------------------------------------------------------------------# AttributeError Traceback (most recent call last)# <ipython-input-30-bf9b854689d5> in <module># ----> 1 print(c.__bases__)## AttributeError: 'C' object has no attribute '__bases__'
ここのC
クラスのインスタンスにも親クラスが存在しません。
ここまでの各種の関係性を図にすると以下のようになります。ここでは、実線は継承関係を表し、矢印は親クラスを指します。点線はクラス・インスタンス関係を表し、矢印はインスタンスのタイプクラスを指します。
上記の検証から、以下の結果に辿り着きました。
- 全ての
object
はtype
のインスタンスです。 type
の直属のインスタンスはobject
やobject
を継承したクラスで、これらはPythonのオブジェクト指向においての「クラス」です。type
の直属のインスタンス、つまり「クラス」の更なるインスタンスは__bases__
を持たないクラスで、これらはPythonのオブジェクト指向においての「インスタンス」です。
では、type
を継承したクラスはどんなものになるでしょうか?
classM(type):passprint(M.__class__)# 実行結果:<class 'type'>print(M.__bases__)# 実行結果:(<class 'type'>,)
ここのM
クラスのタイプクラスも親クラスもtype
です。上記の図のルールでは、1列目に置くべきですね。しかし、M
のインスタンスはどこに置くべきでしょうか?
classTM(metaclass=M):passprint(TM.__class__)# 実行結果:<class '__main__.M'>print(TM.__bases__)# 実行結果:(<class 'object'>,)
実はこのM
はメタクラスというクラスのクラスです。メタクラスM
から作成したTM
は上記の図の2列目の「クラス」に所属するでしょう。メタクラスの使い方に関しては後でまた紹介します。
type
は全てのメタクラスの親で、type
を継承してメタクラスを作成できます。object
は全ての「クラス」の親で、ほとんどのビルトインデータクラスはこの「クラス」です。- 「クラス」をインスタンス化して作られたのは「インスタンス」で、継承やインスタンス化に使用できません。
なぜPythonにはtype
とobject
両方必要だろうと思うかもしれません。例えば、type
がないと、上の図は2列になり、1列目が「タイプクラス」、2列目が「インスタンス」になります。静的オブジェクト指向プログラミング言語は大体この2列の構造です。Pythonが3列構造になったのは、ランタイムでクラスを動的に作成するためです。2列目のobject
はただtype
のインスタンスなので、ランタイムでメソッドや属性を変更できるわけです。この性質を実現するために、3列構造が必要になります。
5-3. メタクラス
5-3-1. クラスはオブジェクト
Pythonのクラスはsmalltalk
から拝借したものです。殆どのオブジェクト指向プログラミング言語では、クラスというのは「オブジェクトをどう生成するか」を記述したコードになります。
classObjectCreater:passmy_object=ObjectCreater()print(my_object)# 実行結果:<__main__.ObjectCreater object at 0x7fbc76f9a970>
しかし、繰り返しにはなりますが、Pythonのクラスはクラスであると同時に、オブジェクトでもあります。class
予約語を実行する時に、Pythonはメモリ上オブジェクトを作成します。上記のコードでは、ObjectCreater
というオブジェクトが作成されました。この「クラス」オブジェクトは「インスタンス」オブジェクトを作成することができます。これが「クラス」の役割です。そして、オブジェクトであるため、ObjectCreater
に対して以下の操作が可能です。
- 他の変数に代入する
- コピーする
- 属性を増やす
- 引数として関数に渡す
classObjectCreator:passdefecho(obj):print(obj)echo(ObjectCreator)# 引数として渡すObjectCreator.new_attr='foo'# 属性を増やすasserthasattr(ObjectCreator,'new_attr')==TrueObjectCreatorMirror=ObjectCreator# 他の変数に代入する
5-3-2. クラスの動的作成
クラスもオブジェクトなので、ランタイムでの作成は他のオブジェクトと同じくできるはずです。まず、class
予約語を使って、クラスを作成する関数を作ってみます。
defchoose_class(name):ifname=='foo':classFoo:passreturnFooelse:classBar:passreturnBarMyClass=choose_class('foo')print(MyClass)print(MyClass())
実行結果:
<class '__main__.choose_class.<locals>.Foo'><__main__.choose_class.<locals>.Foo object at 0x7fad2abc8340>
クラスを条件分岐で作成できました。しかし、この方法はそれほど「動的」とは言えないですね。クラスもオブジェクトなら、クラスを作る何かがあるはずです。実は、その「何か」が5.2 タイプとオブジェクトで紹介したtype
です。
殆どの人は使ったことがあると思いますが、Pythonにはtype
という関数があります。
print(type(1))# 実行結果:<class 'int'>print(type('1'))# 実行結果:<class 'str'>print(type(ObjectCreatorMirror))# 実行結果:<class 'type'>
しかし、type
にはもう1つの機能があります。それは、ランタイムでクラスを作成するという機能です。なぜ1つの関数に2つの機能があるかというと、3-1. クラスで紹介したようにPython 2には、type
を継承した古いクラスが存在します。その後方互換のために、type
に2つの機能を持たせました。
MyShinyClass=type("MyShinyClass",(),{})print(MyShinyClass)print(MyShinyClass())
実行結果:
<class '__main__.MyShinyClass'><__main__.MyShinyClass object at 0x7f9cd02bddc0>
type
でクラスを作る時に、3つの引数が必要です。
- クラス名
- クラスが継承するクラスのタプル
- クラスの属性を格納する辞書型オブジェクト(名前空間)
次に、type
の使い方をもう少し見ていきます。
classFoo:bar=Truedefecho_bar(self):print(self.bar)
上記と同じ構造のクラスをtype
で作ると以下のようになります。
defecho_bar(self):print(self.bar)Foo=type('Foo',(),{'bar':True,'echo_bar':echo_bar})
継承関係のあるクラスを作成します。
classFooChild(Foo):pass
type
で作ると以下のようになります。
FooChild=type('FooChild',(Foo,),{})
5-3-3. メタクラスの定義
前述のように、メタクラスはクラスのクラスで、クラスを作るクラスになります。「type
は全てのメタクラスの親で、type
を継承してメタクラスを作成できます。」というのを説明しましたが、実はtype
自身もメタクラスです。メタクラス、クラス、インスタンスの関係性は以下の図のようになります。
type
関数は特殊なメタクラスです。実はclass
を使ってクラスを作成する時に、Pythonは裏でtype
を使っています。そのため、全てのobject
はtype
のインスタンスになるわけです。
x=30print(x.__class__)# 実行結果:<class 'int'>print(x.__class__.__class__)# 実行結果:<class 'type'>
type
はビルトインのメタクラスです。メタクラスの自作については5-2. タイプとオブジェクトでも説明しましたが、以下のようになります。
classMeta(type):passclassFoo(metaclass=Meta):pass
5-3-4. メタクラスの使い方
メタクラスを使う目的は、クラスの作成時に、自動的に何らかのカスタマイズをすることです。例えば、あるモジュールにおいて、全てのクラスの属性名を大文字にしたい時に、このようなメタクラスが作れます。
classUpperAttrMetaClass(type):# __new__はインスタンスselfを作成するコンストラクタ# __init__は作成されたインスタンスselfを初期化するイニシャライザdef__new__(cls,new_class_name,new_class_parents,new_class_attr):uppercase_attr={}forname,valinnew_class_attr.items():# 特殊メソッドを除くifnotname.startswith('__'):uppercase_attr[name.upper()]=valelse:uppercase_attr[name]=valreturntype.__new__(cls,new_class_name,new_class_parents,new_class_attr)# 下の書き方と同様# return super().__new__(cls, new_class_name, new_class_parents, new_class_attr)
メタクラスはデータ型のチェックや継承のコントロールなどに使うことができます。メタクラスを導入すると、コードがやや複雑になるかもしれませんが、メタクラスの役割自体はシンプルです。クラスのデフォルトの作成過程に割り込み、修正を加え、修正後のクラスを返すだけです。
また、Pythonの標準ライブラリにはtypes
というモジュールがあり、メタクラスやクラス生成に関する関数が提供されています。
types.prepare_class(name, bases=(), kwds=None)
はこれから作る新しいクラスの適切なメタクラスを選ぶ関数です。関数の戻り値はmetaclass, namespace, kwds
のタプルになります。types.new_class(name, bases=(), kwds=None, exec_body=None)
は新しいクラスを作成する関数で、exec_body
は新しいクラスの名前空間を構築するためのコールバック関数を受け取ります。例えば、exec_body=lambda ns: collections.OrderedDict()
で順序付き辞書を使って名前空間を構築できます(Python 3.6以降は不要)。
importtypesclassA(type):expected_ns={}def__new__(cls,name,bases,namespace):returntype.__new__(cls,name,bases,namespace)@classmethoddef__prepare__(cls,name,bases,**kwargs):expected_ns.update(kwargs)returnexpected_nsB=types.new_class("B",(object,))C=types.new_class("C",(object,),{"metaclass":A})# メタクラスの継承チェーンの1番下はtypeではなく、Aになるmeta,ns,kwds=types.prepare_class("D",(B,C),{"metaclass":type,'x':1})assertmetaisA# 継承チェーンの1番下にあるメタクラスAが選択されたのが確認できるassertnsisexpected_ns# Aの__prepare__が使用されているのが確認できるprint(kwds)# metaclassキーワード引数が削除されたのが確認できる(適切なメタクラスを戻り値として返したため)# 実行結果:{'x': 1}
メタクラスの実用例として、ORMが挙げられます。ここでは、DjangoのORMを例として見てみましょう。
fromdjango.dbimportmodelsclassPerson(models.Model):name=models.CharField(max_length=30)age=models.IntegerField()guy=Person.objects.get(name='bob')print(guy.age)# output is 35
DjangoのORMは上記のように非常に簡単に使えます。Djangoはメタクラスを使用して、データベースの複雑なクエリなどを実現しています。後でORMの実装例も紹介しますので、Django ORMの詳細はdjango.db.models.base.ModelBaseを参照してください。
5-4. ディスクリプタ
5-4-1. ディスクリプタの基本
4-2. プロパティのところで、property
デコレーターについて見てきました。property
はメソッドをインスタンス変数のようにするだけではなく、値の代入などに対してチェックを行うこともできます。
classStudent:def__init__(self,name,score):self.name=nameself._score=score@propertydefscore(self):returnself._score@score.setterdefscore(self,value):ifnotisinstance(value,int):print('Please input an int')returnself._score=value
property
デコレーターでの値チェックには2つの問題点があります。
- 変数に対して、
property
とx.setter
デコレーターをそれぞれ適用する必要があり、変数が多いととコードが冗長になります。 - 初期化の時点でのチェックができません。
ディスクリプタ(Descriptor)はこの問題を解決するためのソリューションです。ディスクリプタはオブジェクトの属性の参照、保存と削除をカスタマイズするためのものです。クラスの中に、__get__
、__set__
、__delete__
のどれか1つを実装すればそれがディスクリプタになります。使用する時はがディスクリプタを所有クラスのクラス変数として定義しなければなりません。
ディスクリプタは以下の2種類があります。
__get__
のみ実装したクラスはノンデータディスクリプタ(non-data descriptor)と言います。__get__
と__set__
を実装したクラスはデータディスクリプタ(data descriptor)と言います。
5-4-2. ノンデータディスクリプタとデータディスクリプタ
ディレクトリーのファイル数を取得する簡単なディスクリプタを作ってみまょう。
importosclassDirectorySize:def__get__(self,instance,owner):returnlen(os.listdir(instance.dirname))classDirectory:size=DirectorySize()# ディスクリプタdef__init__(self,dirname):self.dirname=dirnamedebug=Directory('debug')print(debug.size)
ディスクリプタメソッド__get__
はself
以外に、自身を所有するクラスowner
とそのインスタンスinstance
の2つの引数を受け取ります。Directory
クラスの中でディスクリプタDirectorySize
をインスタンス化し、クラス変数size
に入れます。そして、size
を呼び出すと、DirectorySize
の__get__
メソッドが呼び出されます。
print(vars(debug))# 実行結果:{'dirname': 'debug'}
上記のコードを見れば分かりますが、ノンデータディスクリプタはインスタンスの名前空間には存在しません。
次に、__get__
と__set__
を実装したデータディスクリプタを実装してみます。
importlogginglogging.basicConfig(level=logging.INFO)classLoggedAgeAccess:def__get__(self,instance,owner):value=instance._agelogging.info('Accessing %r giving %r','age',value)returnvaluedef__set__(self,instance,value):logging.info('Updating %r to %r','age',value)instance._age=valueclassPerson:age=LoggedAgeAccess()# ディスクリプタdef__init__(self,name,age):self.name=name# 普通のインスタンス変数self.age=age# ディスクリプタを呼び出すdefbirthday(self):self.age+=1# __get__と__set__両方が呼び出されるmary=Person('Mary M',30)mary.agemary.birthday()
実行結果:
INFO:root:Updating 'age' to 30INFO:root:Accessing 'age' giving 30INFO:root:Accessing 'age' giving 30INFO:root:Updating 'age' to 31
ディスクリプタメソッド__set__
は、所有クラスのインスタンスinstance
とディスクリプタに代入する値value
を受け取ります。ディスクリプタに値を代入すると__set__
メソッドが呼び出されます。そして、__init__
の初期化の時も同じです。
print(vars(mary))# 実行結果:{'name': 'Mary M', '_age': 31}
データディスクリプタで、インスタンス変数に値を代入すると、名前空間に現れます。
ディスクリプタには、__set_name__
というメソッドがあります。ディスクリプタにアサインされた変数名(上の例ではage
)を取得でき、修正を加えることもできます。下記の例はメタクラスと__set_name__
を使用したデータディスクリプタで実装したシンプルなORMです。
importsqlite3conn=sqlite3.connect('entertainment.db')classMetaModel(type):def__new__(cls,clsname,bases,attrs):table=attrs.get('table')iftable:col_names=[kfork,vinattrs.items()iftype(v)==Field]# ダミーのデータ型を付与col_names_with_type=[f'{c}{attrs[c].datatype} PRIMARY KEY'ifattrs[c].is_primary_keyelsef'{c}{attrs[c].datatype}'forcincol_names]# テーブルの作成create_table=f"CREATE TABLE IF NOT EXISTS{table} ({','.join(col_names_with_type)});"conn.execute(create_table)conn.commit()attrs['_columns_']=col_names# 各モデルのカラム名を格納returnsuper().__new__(cls,clsname,bases,attrs)classModel(metaclass=MetaModel):def__init__(self,*col_vals):self.col_vals=col_vals# レコードの各カラムの値を格納cols=self._columns_table=self.tablepk=self.primary_keypk_val=self.primary_key_value=col_vals[cols.index(pk)]record=conn.execute(f'SELECT * FROM{table} WHERE{pk}=?;',(pk_val,)).fetchone()ifnotrecord:params=','.join(f':{c}'forcincols)conn.execute(f"INSERT INTO{table} VALUES ({params});",col_vals)conn.commit()else:params=','.join(f"{c}=?"forcincols)update_col_vals=col_vals+(pk_val,)conn.execute(f"UPDATE{table} SET{params} WHERE{pk}=?;",update_col_vals)classField:def__init__(self,datatype,primary_key=False):self.datatype=datatypeself.is_primary_key=primary_keydef__set_name__(self,owner,name):ifself.is_primary_key:owner.primary_key=nameself.fetch=f'SELECT{name} FROM{owner.table} WHERE{owner.primary_key}=?;'self.store=f'UPDATE{owner.table} SET{name}=? WHERE{owner.primary_key}=?;'def__get__(self,instance,owner):returnconn.execute(self.fetch,[instance.primary_key_value]).fetchone()[0]def__set__(self,instance,value):conn.execute(self.store,[value,instance.primary_key_value])conn.commit()ifself.is_primary_key:instance.primary_key_value=valueclassMovieModel(Model):table='Movie'title=Field(datatype='TEXT',primary_key=True)director=Field(datatype='TEXT')year=Field(datatype='INTEGER')classMusicModel(Model):table='Music'title=Field(datatype='TEXT',primary_key=True)artist=Field(datatype='TEXT')year=Field(datatype='INTEGER')genre=Field(datatype='TEXT')star_wars=MovieModel('Star Wars','George Lucas',1977)print(f'{star_wars.title} released in{star_wars.year} by{star_wars.director}')star_wars.director='J.J. Abrams'print(star_wars.director)country_roads=MusicModel('Country Roads','John Denver',1973,'country')print(f'{country_roads.title} is a{country_roads.genre} song of{country_roads.artist}')
実行結果:
Star Wars released in 1977 by George LucasJ.J. AbramsCountry Roads is a country song of John Denver
このように、メタクラスとデータディスクリプタを組み合わせればORMも簡単に実装できます。もちろん両方とも使わなければならないという制約はなく、例えばDjangoのField
はディスクリプタを使用していません。実際のORMはできる限りDBとの通信回数を減らすように、アプリケーション層での型評価やキャッシュなどもっと複雑な機能が実装されています。
5-4-3. ディスクリプタの仕組み
5-1. 特殊メソッドで、__getattribute__
について触れました。__getattribute__
は「未定義・定義済みにかかわらず、全てのクラスメンバーにアクセスする時に呼び出される」の機能を持っているメソッドで、ディスクリプタを使用したクラスに対して、b.x
のような呼び出しをtype(b).__dict__['x'].__get__(b, type(b))
のような処理に置き換えています。
classDescriptor:def__get__(self,instance,owner):returnself._xdef__set__(self,instance,value):self._x=valueclassB:x=Descriptor()def__init__(self,x):self.x=xdef__getattribute__(self,key):attr=type(self).__dict__[key]ifhasattr(attr,'__get__'):returnattr.__get__(self,type(self))else:returnattr
そのため、__getattribute__
をカスタマイズすると、ディスクリプタが使えなくなります。そして、当たり前かもしれませんが、__get__
と__set__
を実装したデータディスクリプタは、変数の代入にチェックを入れるので、常にインスタンス変数を上書きします。上記の例では、b = B(1); b.x = 2
にしても、b.x
はディスクリプタのままです。それに対して、__get__
だけ実装したノンデータディスクリプタは変数の代入をチェックしないので、クラス変数を直接更新すれば、ディスクリプタが上書きされます。
5-4-4. ディスクリプタの使い方
実はディスクリプタを使って、4. pythonのオブジェクト指向の基本で紹介したproperty
、classmethod
、staticmethod
デコレーターと同じ機能を実現できます。
5-4-4-1. property
property
は以下のように実装できます。
classProperty:def__init__(self,fget=None,fset=None,fdel=None,doc=None):self.fget=fgetself.fset=fsetself.fdel=fdelifdocisNoneandfgetisnotNone:doc=fget.__doc__self.__doc__=docdef__get__(self,obj,objtype=None):ifobjisNone:returnselfifself.fgetisNone:raiseAttributeError("unreadable attribute")returnself.fget(obj)def__set__(self,obj,value):ifself.fsetisNone:raiseAttributeError("can't set attribute")self.fset(obj,value)def__delete__(self,obj):ifself.fdelisNone:raiseAttributeError("can't delete attribute")self.fdel(obj)defgetter(self,fget):returntype(self)(fget,self.fset,self.fdel,self.__doc__)defsetter(self,fset):returntype(self)(self.fget,fset,self.fdel,self.__doc__)defdeleter(self,fdel):returntype(self)(self.fget,self.fset,fdel,self.__doc__)
上記のディスクリプタをデコレーター@Property
の形でsomemethod
に使う時は、実はsomemethod = Property(somemethod)
の処理になります。そして、Property
のself.fget
に第一引数のfget
を代入し、インスタンスを作ります。次に、@somemethod.setter
で作成済みのProperty
インスタンスのsetter
メソッドで、fset
をインスタンス引数に代入します。続いて、 @somemethod.deleter
で同じく、fdel
をインスタンスに代入できます。この流れは4-2. プロパティと同じですね。
5-4-4-2. methodとfunction
4-1. クラスの変数とメソッドでMethodType
を簡単に紹介しました。同じ機能をPythonコードで実装すると、以下のようになります。
classMethodType:def__init__(self,func,obj):self.__func__=funcself.__self__=objdef__call__(self,*args,**kwargs):func=self.__func__obj=self.__self__returnfunc(obj,*args,**kwargs)
そして、クラスの内部で関数をメソッドに変えるディスクリプタはこんな感じで作成できます。
classFunction:def__get__(self,obj,objtype=None):ifobjisNone:returnselfreturnMethodType(self,obj)
5-1. 特殊メソッドで、「instance.method.__func__
はメソッドの実体である関数オブジェクトを返す」を説明しました。しかし、instance.method
でアクセスすると、メソッドオブジェクトが返ってきます。この挙動は上記のディスクリプタでシミュレートできます。
5-4-4-3. classmethodとstaticmethod
この2つのデコレーターは上記のMethodType
を使って、非常に簡単に実現できます。まず、classmethod
は以下のように実装できます。
classClassMethod:def__init__(self,f):self.f=fdef__get__(self,obj,cls=None):ifclsisNone:cls=type(obj)ifhasattr(obj,'__get__'):returnself.f.__get__(cls)returnMethodType(self.f,cls)
@ClassMethod
の形で使用すると、somemethod = ClassMethod(somemethod)
になり、somemethod
をインスタンスではなく、クラスにバインディングできます。
次に、staticmethod
について見ていきます。
classStaticMethod:def__init__(self,f):self.f=fdef__get__(self,obj,objtype=None):returnself.f
Pythonの静的メソッドstaticmethod
の実体は普通の関数です。上記のStaticMethod
を@StaticMethod
の形で使用すると、同じくsomemethod = StaticMethod(somemethod)
になり、単純にディスクリプタのインスタンス変数self.f
に関数somemethod
を保存し、クラスやインスタンスにバインディングされるのを阻止します。そして、呼び出された時はself.f
をそのまま返します。
5-4-4-5. types.DynamicClassAttribute
あまり知られてないですが、Pythonの標準ライブラリにはtypes.DynamicClassAttribute
というディスクリプタがあります。使い方は、property
と同じです。このディスクリプタはインスタンスからアクセス時は普通のproperty
と全く一緒で、クラスからアクセスする時のみ機能が変わります。クラスからアクセスすると、クラスの__getattr__
メソッドに、__getattr__
が定義されてない時は、メタクラスの__getattr__
に振り替えられます。
fromtypesimportDynamicClassAttributeclassEnumMeta(type):def__new__(cls,name,bases,namespace):reserved_names=('name','value','values')enum_namespace=namespace.copy()enum_namespace['_member_map_']={}enum_namespace['_member_map_']['values']=[]fork,vinnamespace.items():ifnot(kinreserved_namesork.startswith('_')):member_namespace=namespace.copy()member_namespace.update({"_name_":k,"_value_":v})member_cls=super().__new__(cls,name,bases,member_namespace)enum_namespace['_member_map_']['values'].append(v)enum_namespace['_member_map_'][k]=member_cls()enum_namespace[k]=enum_namespace['_member_map_'][k]returnsuper().__new__(cls,name,bases,enum_namespace)def__getattr__(self,item):returnself._member_map_[item]classEnum(metaclass=EnumMeta):@DynamicClassAttributedefname(self):returnself._name_@DynamicClassAttributedefvalue(self):returnself._value_@DynamicClassAttributedefvalues(self):returnself._values_classColor(Enum):red=1blue=2green=3print(Color.red.value)# 実行結果:1Color.red._values_=[1]print(Color.red.values)# インスタンスのvalues# 実行結果:[1]print(Color.values)# クラスのvalues# 実行結果:[1, 2, 3]
上記は自作の簡易版列挙型です。列挙型については後で詳しく紹介します。ここのEnum
クラスの各クラス変数はメタクラスEnumMeta
によってEnum
のインスタンスに変換されました。そして、types.DynamicClassAttribute
によって、クラスのvalues
とインスタンスのvalues
はお互い干渉せずに共存できました。このように、クラスとインスタンスに違う動作を実現させたい時はtypes.DynamicClassAttribute
を使用すると楽です。
5-4-4-5. __slots__
Pythonには特殊な属性__slots__
が存在し、既存クラスに対して、モンキーパッチでの新しい属性の追加を阻止できます。使い方としては以下のようになります。
classStudent:__slots__=('name','age')student=Student()student.name='Mike'student.age=20student.grade='A'# 実行結果:# Traceback (most recent call last):# File "oop.py", line 10, in <module># student.grade = 'A'# AttributeError: 'Student' object has no attribute 'grade'
公式ドキュメントによると、この機能も実はディスクリプタによって実現されたのです。ここでは実装しませんが、メタクラスとディスクリプタを組み合わせることでこのような機能も実現できます。
5-5. 抽象基底クラス
3-18-7. 純粋人工物で少し触れた概念ではあるが、抽象基底クラスはオブジェクト指向プログラミングにおいて非常に強力な武器です。抽象基底クラスを使って、クラスがある特定のインターフェースを提供しているかを判定することができます。Pythonの標準ライブラリにはabc
という抽象基底クラスに必要なツールを提供するモジュールがあり、それを使えば抽象基底クラス、抽象メソッドなどを作成できます。
5-5-1. インターフェース
3. オブジェクト指向に関する概念で「インターフェース」という用語を何度も繰り返しました。インターフェースはソフトウェア工学分野において非常に重要な概念です。よく知られているインターフェースとして、API (Application Programming Interface)が挙げられます。他に、最近「マイクロサービス」で多用されている「gRPC」は本質で言うと、インタフェースを「Protocol Buffers」という「インターフェース定義言語」で定義し、サーバー側とクライアント側はそれぞれそのインタフェースを実装することで、相互通信を実現したものです。また、オブジェクト指向においてのインターフェースはオブジェクトレベルのものを指します。
しかし、JavaやC++と違って、Pythonにはビルトインのインターフェースクラスは存在しません。Pythonでインターフェースと同じ機能を実現するためにはいくつかの方法があります。
5-5-2. 仮想基底クラスによるインターフェース
仮想基底クラスは明示的な継承関係がないにも関わらず、インターフェースに制約をかけられるクラスです。Pythonではメタクラスを利用して、仮想基底クラスを実現できます。
classRESTAPIMeta(type):def__instancecheck__(cls,instance):returncls.__subclasscheck__(type(instance))def__subclasscheck__(cls,subclass):return(hasattr(subclass,'get')andcallable(subclass.get)andhasattr(subclass,'post')andcallable(subclass.post))classRESTAPIInterface(metaclass=RESTAPIMeta):...classItemList:defget(self,id):passdefpost(self,id):passclassUserList:defget(self,id):passprint(issubclass(ItemList,RestAPIInterface))# 実行結果:Trueprint(ItemList.__mro__)# 実行結果:(<class '__main__.ItemList'>, <class 'object'>)print(issubclass(UserList,RestAPIInterface))# 実行結果:False
上記はRESTAPIを定義する仮想基底クラスの例です。メタクラスRESTAPIMeta
の__subclasscheck__
で、get
とpost
メソッドを持つクラスを子クラスとして判定するよう実装しました。これで、明示的な継承をせずに、クラスのインターフェースに対してある程度の制約をかけることができます。
5-5-3. 抽象基底クラスによるインターフェース
abc
モジュールを使って、上記の仮想基底クラスを実装してみましょう。抽象基底クラスはclass MyClass(abc.ABC)
またはclass MyClass(metaclass=abc.ABCMeta)
の形式で作成できます。
importabcclassRestAPIInterface(metaclass=abc.ABCMeta):@classmethoddef__subclasshook__(cls,subclass):return(hasattr(subclass,'get')andcallable(subclass.get)andhasattr(subclass,'post')andcallable(subclass.post))classItemList:defget(self,id):passdefpost(self,id):passclassUserList:defget(self,id):passprint(issubclass(ItemList,RestAPIInterface))# 実行結果:Trueprint(ItemList.__mro__)# 実行結果:(<class '__main__.ItemList'>, <class 'object'>)print(issubclass(UserList,RestAPIInterface))# 実行結果:False
__subclasshook__
メソッドはabc.ABCMeta
から作られたインスタンスクラスのクラスメソッドとして実装することで、issubclass
を呼び出すとhookとして機能します。
それから、ABCMeta.register
を使うと、仮想サブクラスを登録することもできます。
importabcclassRestAPIInterface(metaclass=abc.ABCMeta):...classUserList:defget(self,id):passRestAPIInterface.register(UserList)print(issubclass(UserList,RestAPIInterface))# 実行結果:True
デコレーターとして使うこともできます。
importabcclassRestAPIInterface(metaclass=abc.ABCMeta):...@RestAPIInterface.registerclassUserList:defget(self,id):passprint(issubclass(UserList,RestAPIInterface))# 実行結果:True
また、abc.get_cache_token()
で現在の抽象基底クラスのキャッシュトークンを取得できます。このトークンはABCMeta.register
が実行される度に変更されるので、等価性検証に使えます。
importabcclassRestAPIInterface(metaclass=abc.ABCMeta):...classUserList:defget(self,id):passtoken_old=abc.get_cache_token()RestAPIInterface.register(UserList)token_new=abc.get_cache_token()print(f'{token_old} >>>{token_new}')# 実行結果:36 >>> 37
5-5-4. 抽象メソッド
これまでのインターフェイスはあくまでも仮想基底クラスなので、継承関係がなく、子クラスに対しての制限も弱いです。特定のインターフェースを実装しないと、エラーを起こす機能を実現したい時は仮想基底クラスではなく、抽象基底クラスと抽象メソッドを合わせて使う必要があります。
importabcclassRestAPIInterface(metaclass=abc.ABCMeta):@abc.abstractmethoddefget(self,id):raiseNotImplementedError@abc.abstractmethoddefpost(self,id):raiseNotImplementedErrorclassItemList(RestAPIInterface):defget(self,id):passdefpost(self,id):passclassUserList(RestAPIInterface):defget(self,id):passitem_list=ItemList()user_list=UserList()
実行結果:
Traceback (most recent call last): File "resource.py", line 29, in <module> user_list = UserList()TypeError: Can't instantiate abstract class UserList with abstract methods post
また、abc.abstractmethod
はclassmethod
、staticmethod
、property
などと併用することができます。
importabcclassModel(abc.ABC):@classmethod@abc.abstractmethoddefselect_all(cls):...@staticmethod@abc.abstractmethoddefshow_db_name(age):...@property@abc.abstractmethoddefrow_id(self):...
5-4-4. ディスクリプタの使い方で紹介したようなデコレーターの形で使うディスクリプタと一緒に使う時に、ディスクリプタの__isabstractmethod__
を実装することで、abc.abstractmethod
と併用できるようになります。
importabcclassStaticMethod:def__init__(self,f):self.f=fdef__get__(self,obj,objtype=None):returnself.f@propertydef__isabstractmethod__(self):returngetattr(self.f,'__isabstractmethod__',False)classModel(abc.ABC):@StaticMethod@abc.abstractmethoddefshow_db_name():...classItemModel(Model):passitem_model=ItemModel()# 実行結果:# Traceback (most recent call last):# File "oop.py", line 27, in <module># item_model = ItemModel()# TypeError: Can't instantiate abstract class ItemModel with abstract methods show_db_name
そして、Pythonの抽象メソッドはただのインターフェースではなく、super
で継承して、メソッドの中身を獲得できます。
5-5-5. コンテナの抽象基底クラス
標準ライブラリのcollections.abc
にはPythonビルトインデータ構造(コンテナ)の抽象基底クラスを提供しています。
ABC | 継承しているクラス | 抽象メソッド | mixinメソッド |
---|---|---|---|
Container | __contains__ | ||
Hashable | __hash__ | ||
Iterable | __iter__ | ||
Iterator | Iterable | __next__ | __iter__ |
Reversible | Iterable | __reversed__ | |
Generator | Iterator | send、throw | close、__iter__ 、__next__ |
Sized | __len__ | ||
Callable | __call__ | ||
Collection | Sized、Iterable、 Container | __contains__、 __iter__ 、__len__ | |
Sequence | Reversible、 Collection | __getitem__、 __len__ | __contains__、__iter__、__reversed__、 index、count |
MutableSequence | Sequence | __getitem__、 __setitem__、 __delitem__、 __len__、insert | Sequenceから継承したメソッドと、 append、reverse、extend、pop、remove、 __iadd__ |
ByteString | Sequence | __getitem__、 __len__ | Sequenceから継承したメソッド |
Set | Collection | __contains__、 __iter__、__len__ | __le__、__lt__、__eq__、__ne__、__gt__、 __ge__、__and__、__or__、__sub__、__xor__、 isdisjoint |
MutableSet | Set | __contains__、 __iter__、__len__、 add、discard | Setから継承したメソッドと、clear、pop、 remove、__ior__、__iand__、__ixor__、 __isub__ |
Mapping | Collection | __getitem__、 __iter__、__len__ | __contains__、keys、items、values、get、 __eq__、__ne__ |
MutableMapping | Mapping | __getitem__、 __setitem__、 __delitem__、 __iter__、__len__ | Mappingから継承したメソッドと、pop、 popitem、clear、update、setdefault |
MappingView | Sized | __len__ | |
ItemView | MappingView、Set | __contains__、__iter__ | |
KeysView | MappingView、Set | __contains__、__iter__ | |
ValuesView | MappingView、 Collection | __contains__、__iter__ | |
Awaitable | __await__ | ||
Coroutine | Awaitable | send、throw | close |
AsyncIterable | __aiter__ | ||
AsyncIterator | AsyncIterable | __anext__ | __aiter__ |
AsyncGenerator | AsyncIterator | asend、athrow | aclose、__aiter__、__anext__ |
(参照:collections.abc — Abstract Base Classes for Containers) |
使い方は通常の抽象基底クラスと同じです。
fromcollections.abcimportSetclassListBasedSet(Set):def__init__(self,iterable):self.elements=[]forvalueiniterable:ifvaluenotinself.elements:self.elements.append(value)def__str__(self):returnrepr(self.elements)def__iter__(self):returniter(self.elements)def__contains__(self,value):returnvalueinself.elementsdef__len__(self):returnlen(self.elements)s1=ListBasedSet('abcdef')s2=ListBasedSet('defghi')overlap=s1&s2# __and__は継承されたので、そのまま積集合を計算できるprint(overlap)# 実行結果:['d', 'e', 'f']
上記はリストベースの集合の実装です。Pythonの通常の集合は辞書と同じくハッシュテーブルを利用して実装されたので、時間計算量的にはリストより速いですが、空間計算量は少し多いので、この実装はメモリ消費量を節約したい時に使用できます。
5-6. 列挙型
列挙型は、変数(正式に言うと識別子)などを有限集合として束ねる抽象データ構造です。本来はオブジェクト指向プログラミングとは無関係のものです。例えば、プロセス指向プログラミング言語のCも列挙型をサポートしています。しかし、JavaやPythonのようなオブジェクト指向プログラミング言語では、列挙型はクラスオブジェクトの形で実現されています。
なぜ列挙型が必要かというと、現実世界ではある有限の範囲内に限定されているデータは結構多いからです。例えば、曜日というのは、週単位に限定された7種類の有限のデータになります。同じく、月も年単位で12種類のデータです。ソフトウェアの中でいうと、CSSのカラーネーム、HTTPコード、真偽値、ファイル記述子など有限な状態の集合は無数にあります。これらを訳のわからない数字ではなく、列挙型で表現すると、コードの可読性が向上し、ロジックも綺麗に見えます。
Pythonの標準ライブラリには、enum
という列挙型を作成するモジュールが提供されています。まず、基本的な使い方を見てみましょう。
importenumclassColor(enum.Enum):red=1green=2blue=3print(Color.red)# 実行結果:Color.redprint(Color['red'])# 実行結果:Color.redprint(Color(1))# 実行結果:Color.redprint(Color.red.value)# 実行結果:1print(Color.red.name)# 実行結果:redforcolorinColor:print(color)# 実行結果:# Color.red# Color.green# Color.blue
クラス変数と違って、列挙型はiterable object
になっています。
Color.red=4# 実行結果:# Traceback (most recent call last):# File "oop.py", line 26, in <module># Color.red = 4# File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 383, in __setattr__# raise AttributeError('Cannot reassign members.')# AttributeError: Cannot reassign members.
列挙型のメンバは外部から修正できません。
print(Color.redisColor.green)# 実行結果:Falseprint(Color.red==Color.green)# 実行結果:Falsered=Color.redprint(Color.red==red)# 実行結果:Trueprint(Color.red<Color.green)# 実行結果:# Traceback (most recent call last):# File "oop.py", line 30, in <module># print(Color.red < Color.green)# TypeError: '<' not supported between instances of 'Color' and 'Color'
enum.Enum
列挙型はメンバ間の一致性評価と等価性評価のみサポートします。それ以外の値の比較をしたい時は、enum.IntEnum
を使うことができます。
importenumclassColor(enum.IntEnum):red=1green=2blue=3purple=enum.auto()# valueのオートインクリメントprint(Color.purple>Color.blue)# 実行結果:True
また、ビッド演算でのメンバの組み合わせを実現したい時はenum.Flag
が使えます。
importenumclassColor(enum.Flag):red=enum.auto()green=enum.auto()blue=enum.auto()purple=enum.auto()print(Color.__members__)# 実行結果:# {'red': <Color.red: 1>, 'green': <Color.green: 2>, 'blue': <Color.blue: 4>, 'purple': <Color.purple: 8>}print(Color.purple|Color.blue)# 実行結果:Color.purple|blueprint(Color.purple|2)# 実行結果:# Traceback (most recent call last):# File "oop.py", line 13, in <module># print(Color.purple | 2)# TypeError: unsupported operand type(s) for |: 'Color' and 'int'
enum.Flag
はメンバ間のビッド演算をサポートしていますが、整数値との計算はできません。これを実現するために、enum.IntFlag
を使う必要があります。
importenumclassColor(enum.IntFlag):red=enum.auto()green=enum.auto()blue=enum.auto()purple=enum.auto()print(Color.__members__)# 実行結果:# {'red': <Color.red: 1>, 'green': <Color.green: 2>, 'blue': <Color.blue: 4>, 'purple': <Color.purple: 8>}print(Color.purple|Color.blue)# 実行結果:Color.purple|blueprint(Color.purple|2)# 実行結果:Color.purple|green
enum.IntFlag
はメンバを整数値として扱います。
続いて、普通の列挙型enum.Enum
についてもう少し見ていきます。
importenumclassMessageResult(enum.Enum):SUCCESS=1INVALID_MESSAGE=2INVALID_PARAMETER=3BAD_MESSAGE=2print(MessageResult(2))# 実行結果:MessageResult.INVALID_MESSAGEclassMessageResult(enum.Enum):SUCCESS=1INVALID_MESSAGE=2INVALID_PARAMETER=3INVALID_MESSAGE=4# 実行結果:# Traceback (most recent call last):# File "oop.py", line 14, in <module># class MessageResult(enum.Enum):# File "oop.py", line 18, in MessageResult# INVALID_MESSAGE = 4# File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 99, in __setitem__# raise TypeError('Attempted to reuse key: %r' % key)# TypeError: Attempted to reuse key: 'INVALID_MESSAGE'
列挙型はメンバのname
のユニーク性を保証しますが、value
に対しては制約しません。
importenum@enum.uniqueclassMessageResult(enum.Enum):SUCCESS=1INVALID_MESSAGE=2INVALID_PARAMETER=3BAD_MESSAGE=2# 実行結果:# Traceback (most recent call last):# File "oop.py", line 4, in <module># class MessageResult(enum.Enum):# File "/Users/kaito/opt/miniconda3/lib/python3.8/enum.py", line 865, in unique# raise ValueError('duplicate values found in %r: %s' %# ValueError: duplicate values found in <enum 'MessageResult'>: BAD_MESSAGE -> INVALID_MESSAGE
enum.unique
をデコレーターとしてクラスに適用すれば、value
のユニーク性も保証できるようになります。
importenumMessageResult=enum.Enum(value='MessageResult',names=('SUCCESS INVALID_MESSAGE INVALID_PARAMETER'),)print(MessageResult.__members__)# 実行結果:# {'SUCCESS': <MessageResult.SUCCESS: 1>, 'INVALID_MESSAGE': <MessageResult.INVALID_MESSAGE: 2>, 'INVALID_PARAMETER': <MessageResult.INVALID_PARAMETER: 3>}
ハードコーディングではなく、Functional API
で動的に列挙型を作成することもできます。names
引数にスペース区切りの文字列を渡すと、自動採番で数字を割り振ってくれます。
importenumMessageResult=enum.Enum(value='MessageResult',names=(('SUCCESS',3),('INVALID_MESSAGE',2),('INVALID_PARAMETER',1)))print(MessageResult.__members__)# 実行結果:# {'SUCCESS': <MessageResult.SUCCESS: 3>, 'INVALID_MESSAGE': <MessageResult.INVALID_MESSAGE: 2>, 'INVALID_PARAMETER': <MessageResult.INVALID_PARAMETER: 1>}
names
引数に階層化したiterable object
を渡すと、それぞれのメンバのvalue
を指定できます。
importenumclassMessage(enum.Enum):DB_SAVE_SUCCESS=('Saved successfully',201)INTERNEL_ERROR=('Internal error happened',500)DB_DELETE_SUCCESS=('Deleted successfully',200)DB_ITEM_NOT_FOUND=('Item not found',404)def__init__(self,message,code):self.message=messageself.code=code@propertydefok(self):ifstr(self.code).startswith('2'):returnTruereturnFalseprint(Message.DB_SAVE_SUCCESS)# 実行結果:Message.DB_SAVE_SUCCESSprint(Message.DB_DELETE_SUCCESS.ok)# 実行結果:Trueprint(Message.DB_ITEM_NOT_FOUND.ok)# 実行結果:False
列挙型のメンバは整数値に限らず、あらゆるデータ型を使うことができます。また、イニシャライザの__init__
を実装すると、クラスの評価時にメンバの値は__init__
に渡されます。そして、タプルを使えば複数の変数を渡すことができます。
ただし、__init__
はメンバの値をカスタマイズすることはできません。メンバをカスタマイズしたい時は、コンストラクタの__new__
を実装する必要があります。
importenumclassCoordinate(bytes,enum.Enum):def__new__(cls,value,label,unit):obj=bytes.__new__(cls,[value])obj._value_=valueobj.label=labelobj.unit=unitreturnobjPX=(0,'P.X','km')PY=(1,'P.Y','km')VX=(2,'V.X','km/s')VY=(3,'V.Y','km/s')print(Coordinate.PY.label,Coordinate.PY.value,Coordinate.PY.unit)# 実行結果:P.Y 1 kmprint(Coordinate.PY)# 実行結果:Coordinate.PY
上記は公式ドキュメントに載っている例で、バイナリオブジェクトでメンバの値とその他の情報をまとめて格納する列挙型になります。
列挙型はクラスなので、内部でメソッドまたはダンダーを実装することができます。しかし、列挙型は普通のクラスと違うところがたくさんあります。まず、列挙型は特殊なメタクラスで実現されていて、メンバ(クラス変数)はクラスのインスタンスになります。そのため、__new__
と__init__
はインスタンス化のタイミングではなく、クラス評価時に機能します。それから、列挙型はいくつか特殊な属性を持っています。
__members__
:member_name:member
のマッピングで、読み出し専用です。_name_
: メンバ名_value_
:メンバの値、__new__
で設定や変更できます。_missing_
:値が見つからなかった時に使われる検索関数です;オーバーライドできます。_ignore_
:リストまたは文字列で、その中身の要素と一致するクラス変数はメンバに変換されなくなります。_order_
:メンバの順番を維持するためのクラス属性です;例えば、_order_ = 'red green blue'
で定義すると、この順番と違う形でメンバを定義したら、エラーを起こします。_generate_next_value
:Functional API
やenum.auto
に使用され、あるメンバの適切な値を取得します;オーバーライドできます。
ちなみに、前後に1つのアンダースコアの付いた属性のことを_sunder_
というらしいです。
5-7. データクラス
ここまで、Pythonのオブジェクト指向プログラミングのほとんどを見てきました。Pythonだけの問題ではなく、オブジェクト指向プログラミング言語全般の問題ではありますが、クラスの定義は非常に煩雑です。Pythonでは大体の場合、クラスの作成する時に、__init__
の定義は最低限必要です。場合によっては他の特殊メソッドも実装しなければなりません。それに加えて、似たようなクラスを大量に作らなければならない時もあります。このようなコードの冗長性をボイラープレート・コードと呼びます。
簡単にクラスを作る1つの手として、types.SimpleNamespace
を使うことができます。
importtypesbbox=types.SimpleNamespace(x=100,y=50,w=20,h=20)print(bbox)# 実行結果:namespace(h=20, w=20, x=100, y=50)print(bbox==bbox)# 実行結果:True
Pythonでtypes.SimpleNamespace
と同じ機能を実装すると以下のようになります。
classSimpleNamespace:def__init__(self,/,**kwargs):self.__dict__.update(kwargs)def__repr__(self):items=(f"{k}={v!r}"fork,vinself.__dict__.items())return"{}({})".format(type(self).__name__,",".join(items))def__eq__(self,other):returnself.__dict__==other.__dict__
__init__
、__repr__
、__eq__
は実装してくれましたが、__hash__
や他の比較演算用特殊メソッドを実装したい時は結構面倒で、逆にコードの可読性が下がってしまう可能性があります。types.SimpleNamespace
はあくまでもシンプルなクラス(名前空間)を作るためのものです。
ボイラープレート・コードの問題に気づいたサードパーティライブラリの開発者たちがいて、クラスの定義をシンプルにするツールを開発してくれました。attrsはその1つです。基本的な使い方は以下のようになります。
fromattrimportattrs,attrib@attrsclassPerson:name=attrib(type=str)sex=attrib(type=str)age=attrib(type=int,default=0)mary=Person('Mary','F',18)print(mary)# 実行結果:Person(name='Mary', sex='F', age=18)print(mary==mary)# 実行結果:True
attrs
はクラスデコレーターの形で使用できます。ここでは、3つのインスタンス変数を定義し、age
にデフォルト引数を設定しました。attrs
は、__init__
と__repr__
を自動的に定義してくれます。また、__eq__
、__ne__
、__lt__
、__le__
、__gt__
、__ge__
も定義してくれて、比較対象はインスタンス変数のタプルになります。
詳しい紹介を省きますが、attrs
はtypes.SimpleNamespace
より高度なツールで、様々な機能があり、Pythonのオブジェクト指向プログラミングにとっては強力なツールになります。
ボイラープレート・コードを解消するために、公式の動きとしてPython 3.7から導入されたデータクラス(dataclasses)というモジュールがあります。このモジュールは特殊メソッドの生成など、attrs
と似たような機能を提供しています。
fromdataclassesimportdataclassfromtypingimportClassVarfromfunctoolsimportcached_propertyimportboto3@dataclassclassS3Image:bucket:strkey:strimg_id:int=1client:ClassVar=boto3.client('s3')# クラス変数@cached_propertydefimage_url(self,http_method:str)->str:returnself.client.generate_presigned_url(...)item_image_1=S3Image('Image','ItemImage')print(item_image_1)# 実行結果:S3Image(bucket='Image', key='ItemImage', img_id=1)print(item_image_1==item_image_1)# 実行結果:True
データクラスはPythonの型アノテーションの形式でクラスのメンバ変数を定義できます。そして、__init__
、__repr__
を実装してくれます。attrs
と違うのは、比較演算用の特殊メソッドはデフォルトでは__eq__
しか実装してくれません。そして、実装してほしいメソッドはdataclass
クラスデコレーターの引数として定義できます。デフォルトでは以下のようになります。
@dataclass(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False)classC:...
それぞれの引数の役割はほぼ自明です。
order
は__eq__
以外の比較演算用特殊メソッドの__ne__
、__lt__
、__le__
、__gt__
、__ge__
の自動追加のフラグです。Trueにするにはeq
引数もTrueにしなければなりません。unsafe_hash
は__hash__
の挙動を制御するフラグです。unsafe_hash=True
にすると、__hash__
メソッドが自動的に追加されますが、不変(immutable)オブジェクトではない時に問題が生じるかもしれません。デフォルトではeq
とfrozen
両方がTrueの時に、__hash__
メソッドは自動的に追加されるので、フラグによる制御は不要です。eq=True
、frozen=False
の場合は親クラスの__hash__
を継承し、eq=False
、frozen=True
の場合は__hash__ = None
に設定されます。また、__hash__
を定義した状態ではunsafe_hash=True
にすることはできません。frozen
は、フィールド(メンバ)に対する値の代入をコントロールするフラグです。True
の場合は、代入を阻止し、読み出し専用になります。
また、データクラスにはfield
という関数が存在し、フィールドごとのコントロールができます。
fromdataclassesimportdataclass,fieldfromtypingimportList@dataclassclassC:mylist:List[int]=field(default_factory=list)c=C()c.mylist+=[1,2,3]
field
は以下の引数を受け取り、Field
オブジェクトを返します。
default
はフィールドのデフォルト値を提供する引数です。default
とdefault_factory
は共存できません。default_factory
は引数なしで呼び出せるオブジェクトを受け取り、フィールドのデフォルトのファクトリーを提供します。例えば、上記の例ではlist
が提供され、それでフィールドのデフォルトのデータ構造を作成します。init
は__init__
にフィールドを含むかどうかのフラグです。repr
は__repr__
にフィールドを含むかどうかのフラグです。compare
は__eq__
や__gt__
などの比較演算用特殊メソッドにフィールドを含むかどうかのフラグです。hash
は__hash__
にフィールドを含むかどうかのフラグです。hash=None
の場合はcompare
を使ってハッシュ値を計算します。公式では、hash=None
が推奨されています。metadata
はマッピングまたはNone
を受け取り、マッピングの場合は読み出し専用の辞書types.MappingProxyType
にラップされます。主に、サードーパーティーのモジュールなどが使用するものです。
fields
関数で、データクラスのフィールドオブジェクトをタプルの形で全部取得することができます。
fromdataclassesimportfieldsprint(fields(c))# 実行結果:# (Field(name='mylist',type=typing.List[int],default=<dataclasses._MISSING_TYPE object at 0x7f8aa098a9a0>,# default_factory=<class 'list'>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD),)
asdict
とastuple
はデータクラスのインスタンスを辞書とタプルに変換します。
@dataclassclassPoint:x:inty:int@dataclassclassPoints:pl:List[Point]p=Point(10,20)assertasdict(p)=={'x':10,'y':20}assertastuple(p)==((10,20))ps=Points([Point(0,0),Point(10,4)])assertasdict(ps)=={'pl':[{'x':0,'y':0},{'x':10,'y':4}]}# 再帰的に処理される
make_dataclass
という動的にデータクラスを作る関数もあります。
fromdataclassesimportdataclass,field,make_dataclassC=make_dataclass('C',[('x',int),'y',('z',int,field(default=5))],namespace={'add_one':lambdaself:self.x+1})# 下の書き方と同様@dataclassclassC:x:inty:'typing.Any'z:int=5defadd_one(self):returnself.x+1
データクラスのインスタンスに修正を加え、同じ型のオブジェクトを新しく作るための関数replace
も提供されています。
fromdataclassesimportdataclass,replace@dataclassclassPoint:x:inty:intdef__post_init__(self):print('__post_init__')p1=Point(1,2)# 実行結果:__post_init__p2=replace(p1,x=2)# 実行結果:__post_init__print(p2)# 実行結果:Point(x=2, y=2)
replace
は__init__
を呼び出して新しいインスタンスを作成するのですが、__post_init__
が定義されている場合は__init__
の後に呼び出します。
他に、データクラスを判別するための関数is_dataclass
も提供されています。データクラスかデータクラスのインスタンスに使用する場合のみ、True
が返ってきます。
5-8. MRO
MRO(Method Resolution Order、メソッド解決順序)は、多重継承において、継承チェーン上でメソッドを探索する時の順序です。PythonのMROアルゴリズムは、「Python 2.2の古いクラス」、「Python 2.2の新しいクラス」、「Python 2.3以降の新しいクラス」の3種類存在します。
なぜMROが必要かというと、以下の例を見てください。
classD:defmethod(self):...classC(D):defmethod(self):...classB(D):passclassA(B,C):pass
上記のコードは「菱形継承問題」と呼ばれる現象です。クラスAはmethod
をどのクラスから継承すれば良いのかを探索する必要があります。
Python 2.2の古いクラスはDFS(深さ優先探索)を採用しています。イメージとしては以下の図のようになります。
図の左は継承関係を表すもので、矢印は子クラスを指します。右はMROの探索順序です。DFSでは、クラスA
はまず(B, C)
の左にあるB
から、D
まで辿り着き、D
はmethod
を実装しているので、それを継承して探索を終えます。継承関係を見ると、C
のほうがA
に近いので、本来はC
のmethod
を継承するのが正しいであるため、DFSはうまく菱形継承問題を解決できないですね。
Python 2.2の新しいクラスはobject
を継承するクラスです。つまり、全ての新しいクラスは共通の祖先クラスobject
を持ちます。そのため、Mixin継承をする時にDFSではメソッドを正しく継承できないので、その対応としてMROアルゴリズムはDFSからBFS(幅優先探索)に変えられました。
BFSでは、クラスA
はB
から、横にあるC
を先に探索しますので、菱形継承の場合は正しくメソッドを継承できます。しかし、通常の継承パターンは少し問題があります。
上記の図は通常のMixin継承です。右のMROは一見問題ないように見えますが、実は違いますね。
classE:passclassD:defmethod(self):...classC(E):defmethod(self):...classB(D):passclassA(B,C):pass
例えば、上記のような場合、本来ならA
はB
からD
に辿り着き、そのmethod
を継承するのです。BFSになると、C
のmethod
が先に探索されるため、D
までは辿り着かなくなります。B
とB
の親のD
から探す順序は単調写像と呼びます。
BFSは単調写像の性質を違反しているため、Python 2.3以降はC3というアルゴリズムでMROを解決するようになりました。C3は「A Monotonic Superclass Linearization for Dylan」という論文で公開されたもので、BFSとDFSの問題点を解決し、単調写像を満たした完璧なアルゴリズムになります。
C3は以下の3つの性質を持っています。
- precedence graph(有向非巡回グラフ)
- 局所の優先順位を保持
- 単調写像
C3アルゴリズムの計算については、「The Python 2.3 Method Resolution Order」の記事に詳しく書かれています。C3は継承チェーンを有向非巡回グラフとして扱うため、循環継承だとエラーになることを注意してください。
また、クラスのMROを参照したい時は、mro()
関数や__mro__
属性の他に、inspect.getmro()
関数を使うこともできます。
まとめ
オブジェクト指向の歴史から、OOPの関連概念、Pythonのオブジェクト指向プログラミングについて隅々まで見てきました。しかし、オブジェクト指向プログラミングは奥深いので、まだまだ学ぶことが多いです。今回はデザインパターンについて詳しく説明できませんでしたが、また別の記事で紹介したいと思います。
参考
Data Model
Built-in Functions
inspect — Inspect live objects
types — Dynamic type creation and names for built-in types
Descriptor HowTo Guide
abc — Abstract Base Classes
collections.abc — Abstract Base Classes for Containers
enum — Support for enumerations
dataclasses — Data Classes
What are metaclasses in Python?
Python Types and Objects(リンク切れ)
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme