今回の記事は、PythonのClassに関するものです。
前回の記事:
でClassを作成してまとめを作ると記載しましたが、その前にPythonのClassについて復習しておきたいと考えて記事を作成。
主に、Pythonの公式ドキュメントを参考にし、その概要の紹介となります。
- Classとは
- Scopeについて
- Classのinstanceについて
- Classの継承
- Private変数
- Iteratorについて
1. Classとは
クラスはデータと機能を組み合わせる方法を提供します。
クラスを作成することで:
- 新しいオブジェクトの型を作成、その型を持った新しいインスタンスを作成することができる。
- インスタンスは自身の状態を保持する属性を持てる
- インスタンスは自身の状態を変更するためのメソッドを持てる
といったことが可能となります。
Python は、他のプログラミング言語と比較して、最小限の構文と意味付けを使ってクラスを言語に追加しています。Python のクラスは、C++ と Modula-3 のクラスメカニズムを混ぜたものです。Python のクラス機構はオブジェクト指向プログラミングの標準的な機能を全て提供しています。
ここでオブジェクト指向プログラミングについてはこちらを参照していただき、基本概念
- カプセル化 (Encapsulation)
- 継承 (Inheritance)
- ポリモーフィズム (Polymorphism)
のみを紹介します。以下はかなりざっくりとした説明になりますのでご注意ください。
-
カプセル化とは:
オブジェクトの内部データへのアクセスを制限することで、外部からのデータの変更や誤用を防ぐ。カプセルというのはオブジェクトの内部と外部を隔てるイメージからきている(と思われる)。 -
継承とは:
共通する性質を持ったオブジェクトを記述する際に使用されるもので、コードの再利用という点においてメリットがある。(イメージとしては共通の性質で絞り込んだ抽象的なクラスを作成し、そのクラスを拡張して具体的なクラスを作成していく。) -
ポリモーフィズムとは:
クラスの継承が行われたクラスについて、そのクラスが有するメソッドを使用可能でありながら、ほかの異なるメソッドを作成することができる仕組み。
なお、ほかの言語で使用されるようなデータアクセスを制限するprivate
といった予約語は持っておらず、そのような仕組みがない点に注意。(慣習としてprivate
変数はアンダースコアを付けた_varName
で表現されている)
2. Scopeについて
スコープ (scope) とは、ある名前空間が直接アクセスできるような、 Python プログラムのテキスト上の領域です。 "直接アクセス可能" とは、修飾なしに (訳注: spam.egg ではなく単に egg のように) 名前を参照した際に、その名前空間から名前を見つけようと試みることを意味します。
ここで名前空間(namespace)とは、名前によるオブジェクトのマッピングであり、現状ではPythonの辞書として実装されている。
名前空間の寿命については
- モジュール:モジュール定義が読み込まれたときに名前空間を作成、インタプリタ終了時まで残る。
- 関数内:関数が呼び出されたときに作成、関数がreturnまたはExceptionの送出かつ関数内で処理されなかった場合削除。
となっている。
スコープに関連する、予約語として重要な:
-
global
: 特定の変数がグローバルスコープに存在し、そこで再束縛されることを指示する。 -
nonlocal
: 特定の変数が外側のスコープに存在し、そこで再束縛されることを指示する。
上記予約語を用いて、サンプルコードが以下の通り:
def scope_test():
"""Outer func scope"""
def do_local():
"""Inner func scope"""
spam = "local spam"
def do_nonlocal():
"""Inner func scope"""
nonlocal spam
spam = "nonlocal spam"
def do_global():
"""Inner func scope"""
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam) # not assigned
do_nonlocal()
print("After nonlocal assignment:", spam) # assigned
do_global()
print("After global assignment:", spam) # not assigned
"""Module scope"""
scope_test()
print("In global scope:", spam) # assigned
上記コードの出力は次の通り:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
出力結果から
- "Inner func scope"で変数の束縛が実施されない(予約語なし)
- "Inner func scope"かつ"nonlocal"予約語ありの場合、外側の"Outer func scope"において変数の束縛を実施
- "Inner func scope"かつ"global"予約語ありの場合、"Outer func scope"の外側の"Module scope"において変数の束縛を実施
となっている。
3. Classのinstanceについて
Pythonにおいて、Classは予約語class
を用いて、
class ClassName:
<statement-1>
...
...
...
<statement-N>
により定義される。(クラスオブジェクトの作成)クラス定義の内側では新たな名前空間が作成され、ローカルな名前空間として使用される。
クラスオブジェクトは属性参照とインスタンス生成をサポートしている。例えば、次のようなクラスオブジェクトがあったとする:
class MyClass:
"""A simple example class"""
data = []
def __init__(self, name):
self.name = name
self.data.append(self)
def say_hello(self):
return f"Hello {self.name}!"
def __repr__(self):
return f"\"{self.__class__} : ({self.name})\""
ここで__repr__
メソッドは例えば、MyClass
をprint
した時の表現を与える特殊メソッドです。
特殊メソッドについては、こちらを参照してください。
属性参照(attribute reference)
クラスのメンバーにアクセスするためには
Pythonにおけるすべての属性参照で使われている標準的な構文:obj.name
を使用する。
上記例では、MyClass.data
やMyClass.say_hello
、さらにはMyClass.__doc__
(クラスに属しているdocstringを返す)のようにアクセスする。
インスタンス化(instantiation)
クラスのインスタンス化には関数のような表記法を使用する:
class1 = MyClass("Sample")
これによりclass1のインスタンスオブジェクト
が作成される。ここで、インスタンス生成時には自動的に__init__(self, name)
が呼び出されており、引数name
にMyClass("Sample")
の引数が渡されています。
インスタンスオブジェクトは属性参照が可能で、有効な属性名はデータ属性およびメソッドです。例えば、次のようにアクセスする:
class1_data = class1.data
class1_hello = class1.say_hello
class1.data
はlist
であり、class1.say_hello
はmethod
です。通常、メソッドはバインドされた直後に呼び出されますが、後者のメソッドはすぐに呼び出されるわけではなく、メソッドオブジェクト
として変数に格納しておき、class1_hello()
によって呼び出すことが可能です。
ここで、class1_hello()
で呼び出すことが可能であると説明しましたが、MyClass
では引数self
が与えられています。引数がない場合は例外を送出するはずですが、正常に実行されます。
この理由はメソッドが第1引数にインスタンスオブジェクトを渡しているためです。つまり、次の2つの表現が等価です:
- class1_hello()
- MyClass.say_hello(class1)
実際に以下のコードを実行して確認してみて下さい:
class MyClass:
"""A simple example class"""
data = []
def __init__(self, name):
self.name = name
self.data.append(self)
def say_hello(self):
return f"Hello {self.name}!"
def __repr__(self):
return f"\"{self.__class__} : ({self.name})\""
class1 = MyClass("Sample")
class1_hello = class1.say_hell
print(f"type1: {class1_hello()}")
print(f"type2: {MyClass.say_hello(class1)}")
すると、以下の出力がターミナル上に表示されているはずです:
type1: Hello Sample!
type2: Hello Sample!
なお、クラスのメソッドに渡されている、引数self
という名前は慣習によるものです。
クラスとインスタンス変数
一般的にインスタンス変数はそれぞれのインスタンスについて固有のデータのためのもので、クラス変数はそのクラスのすべてのインスタンスによって共有される属性やメソッドのためのものです:
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
この例では、kind
はDog
クラスオブジェクトの変数、name
はインスタンスオブジェクトの変数となっています。
MyClass
クラスではクラス変数data
が定義されているので、インスタンス生成されたオブジェクトの数だけリストにデータがあり、この情報はクラスオブジェクトまたはインスタンスオブジェクトの両方で取得することができます。例えば、
class MyClass:
略
class1 = MyClass("Sample")
class2 = MyClass("Python")
print(MyClass.data)
print(class2.data)
とすると、次のような出力が得られるはずです:
["<class '__main__.MyClass'> : (Sample)", "<class '__main__.MyClass'> : (Python)"]
["<class '__main__.MyClass'> : (Sample)", "<class '__main__.MyClass'> : (Python)"]
出力の表示を変更するためには、__repr__
メソッドの返し値の形式を変更します。例えば、
class MyClass:
中略
def __repr__(self):
return f"\"{self.__class__.__name__} : ({self.name})\""
このようにすると、出力はモジュール名(今回は__main__
でした)を除いたクラス名が表示されるはずです:
["MyClass : (Sample)", "MyClass : (Python)"]
["MyClass : (Sample)", "MyClass : (Python)"]
こちらはあくまで例です。
属性名の注意点
インスタンスとクラスの両方で同じ属性名が使用されている場合、属性検索はインスタンスが優先されます。
例えば:
class Warehouse:
purpose = "storage"
region = "west"
というクラスに対して
w1 = Warehouse()
print(f"{w1.purpose} {w1.region}")
w2 = Warehouse()
w2.region = "east" # インスタンス変数の値を再設定
print(f"{w2.purpose} {w2.region}")
とすると、出力は次のようになります:
storage west
storage east
4. Classの継承
継承という表現を使う際、ほかの言語ではもととなるクラスを親クラス
、継承するクラスを子クラス
と表現しますが、Pythonではそれぞれ基底クラス(base class)
、派生クラス(derived class)
と呼びます。
派生クラスは次のように定義される:
class DerivedClassName(BaseClassName):
<statement-1>
...
<statement-N>
または、引数の基底クラスをモジュール(modname
)を指定した
class DerivedClassName(modname.BaseClassName):
で定義する。今回はMyClass
を基底クラスとした派生クラスSubClass
を定義します。
class SubClass(MyClass):
def __init__(self, name, age):
super().__init__(name)
self.age = age
def self_introduce(self):
return f"(His/Her) name is {self.name}. (He/She) is {self.age} years old."
おおざっぱですが、年齢age
と自己紹介self_introduce
の2つのメンバーを追加しました。
ここでsuper()
関数によって親オブジェクト、今回の例では基底クラスの型を持った代替オブジェクトを返し、そのオブジェクトから基底クラスの__init__
メソッドを呼び出しています。このことから派生クラスの__init__
が呼び出された際に、基底クラスの初期化を同時に行うことができています。super()
を用いることでメソッドのオーバーライド(override)を行うことが可能なことも注目の点です。
ちなみに、ドキュメントによると、
D -> B -> C -> A -> object
のように継承されていた時、仮にB
のクラスでsuper()
を使用した場合、C -> A -> object
の順にオブジェクトまたはメソッドが探索されるようです。
なお、Pythonは多重継承の形式もサポートしており、複数の基底クラスをもつクラス定義は次の通り:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
...
<statement-N>
5. Private変数
オブジェクトの中からしかアクセスすることのできない、いわゆる"プライベート"インスタンス変数は、Pythonには存在しません。ただし、プライベート変数に相当する変数を定義する慣習として、変数の前にアンダースコア(_)
を付与した
_privateVar
をプライベートなAPIとみなして使用します。
クラスのプライベートメンバについて適切なユースケースがあるため、名前マングリング(name mangling)という、限定されたサポートも存在しています。__privateMem
という形式の識別子を与えた場合、_classname__privateMem
というテキストに置換されるというものです。これによりサブクラスで定義された名前との衝突を避けることができます。
名前マングリングは、サブクラスが内部のメソッド呼び出しを壊さずにメソッドをオーバーライドするのに便利です:
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
update
というメソッドが基底クラスで定義され、派生クラスにおいてオーバーライドが行われています。例えば、次のようにインスタンス生成してみます:
mapping = Mapping([1,2,3,4,5])
sub_mapping = MappingSubclass(["item1","item2","item3"])
sub_mapping.update( ["sample1","sample2","sample3"], [1,2,3])
print(mapping.__dict__)
print(sub_mapping.__dict__)
こちらのコードを実行すると次のように表示されるはずです:
{'items': [1, 2, 3, 4, 5]}
{'items': ['item1', 'item2', 'item3', ('sample1', 1), ('sample2', 2), ('sample3', 3)]}
この表示から、update
メソッドはiterable
を与えた場合とkeys,values
を与えた場合の両方を受け付けていることがわかります。これは内部的に_Mapping__update
と_MappingSubClass__update
の2つのメソッドが共存していることに依ります。
なお、名前マングリングが機能していることを確認するために__update = update
をコメントアウトし、上記コードを再実行してみると、次のようなエラーが送出されるはずです:
AttributeError: 'Mapping' object has no attribute '_Mapping__update'
これより、__update
が_classname__privateMem
のように置換されていることが確認できます。
カプセル化について
Javaなどの言語ではgetter/setter
を定義したクラスを作成していました。これをPythonで記述する場合を紹介します。そのために、まずはデコレータ
を紹介する必要があります。デコレータについては、英語の文献にはなりますが、こちらに詳しく記載されていました。
デコレータとは
別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。
とあります。具体的な例で確認していきますが、ざっくりいいますとデコレータは「関数を引数として受け取り、関数を返り値として返す関数」のことで、使用するためには@
記号を使用するということに着目してください。
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def return_greeting(name):
print(f"Hello {name}!")
上記コードではdo_twice
というメソッドは引数にfunc
、つまり関数を受け取り、それを2回呼び出します。次に、return_greeting
というメソッドには@do_twice
というデコレータが付与されています。
この場合にreturn_greeting("Python")
を呼び出すと
Hello Python!
Hello Python!
と出力されるはずです。これはdo_twice
の内側でreturn_greeting
が2度呼び出されたことに依ります。
このようにデコレータは関数を引数として受け取り、関数を返り値として返すことができます。(復習するまでは@
はJavaでいうアノテーションかなんかだろうと考えていました...w)
さて、getter/setter
の話に戻りますと、Pythonにはproperty
というクラスがあります:
class property(
fget: ((Any) -> Any) | None = ...,
fset: ((Any, Any) -> None) | None = ...,
fdel: ((Any) -> None) | None = ...,
doc: str | None = ...
)
引数はそれぞれ次のようになっています:
- fget : 属性値を取得する関数
- fset : 属性値をセットする関数
- fdel : 属性値を削除する関数
- doc : docstring
こちらを使用した実装は例えば次のような感じです:
class PropTest:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
こちらをインスタンス生成して使用する場合は次の通り:
test = PropTest()
test.setx(12345)
print(test.getx()) # 12345
test.delx()
test.setx(123)
print(test.getx()) # 123
property
とデコレータを組み合わせると次のようなクラスを定義することができます:
class DecoTest:
def __init__(self):
self._x = None
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
self._x = None
こちらのクラスをインスタンス生成後、
- メンバにアクセスするには :
InstanceObj.x
- メンバに値をセットするには :
InstanceObj.x = "Value"
- メンバにセットされた値を削除するには :
del(InstanceObj.x)
実際に使用した例は次の通り:
deco_test = DecoTest()
deco_test.x = "Python"
print(deco_test.x) # Python
del(deco_test.x)
print(deco_test.x) # None
このようにしてgetter/setter
を定義することができます。最後に、少しコメント:
-
x
というメソッド(getterのこと)は@property
を使用しているため、返し値はproperty
クラス。 -
@x.setter
の部分をコメントアウトすると、メンバx
の値を設定することができない(エラーが生じる)、つまりこの場合はメンバの値についてRead-Onlyとなる。 -
@x.deleter
は実際、使う機会はあまりなさそう。 -
property
にはgetter/setter/deleter
というメソッドはあるがdocstring
に関するメソッドはなかった。また、x.
の後ろのsetter
およびdeleter
はこのメソッド名からきている。
6. Iteratorについて
公式ドキュメントにあった、クラスにiteratorの振る舞いを追加する方法をこちらで紹介。
その前に、for文
を使うと
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for key, value in zip(["key1","key2","key3"],["value1","value2","value3"]):
print(f"{key} : {value}")
ほとんどのコンテナオブジェクトにわたってループを行うことが可能です。ここでPythonにおけるコンテナにはdict
やlist
、set
、tuple
があり、さらに特殊なコンテナ型が実装されたcollections
モジュールがあります。
for文
のメカニズムは
- コンテナオブジェクトに対して
iter()
関数を呼び出す - 呼び出された関数は、コンテナの中の要素に1つずつアクセスする
__next()__
メソッドが定義されているiteratorオブジェクトを返す - アクセスできる要素がない場合、
__next__()
メソッドはStopIteration
を送出 -
for
ループが終了
となっています。ここで、iter
関数を紹介:
iter(object, sentinel)
iteratorオブジェクトを返す。第2引数sentinel
の有無によって関数の挙動が変わるので注意です。例えば、第1引数のみであれば
for i in iter([1,2,3,4,5]):
print(i)
のようにリストを渡すことが可能です。しかし、第2引数に例えばsentinel(番兵)
として3
を渡してみると
TypeError: iter(v, w): v must be callable
といったエラーが生じます。このエラーはリストが関数のような呼び出し可能なオブジェクトでないことから生じています。ちなみに、番兵
は与えられた値が渡されたときに、for
ループを終了させるように働きます。
番兵
を使用した例が次の通り(参照にしたのはこちら):
from functools import partial
from random import randint
for i in iter(partial(randint,1,6), 6):
print(i)
print("Stop for-loop by sentinel(6)")
partial(func,/,*args,**kwargs)
オブジェクトは呼び出されたときに、args
とkwargs
付きで呼び出されたfunc
のように振る舞う、つまり今回のコードではrandint(1,6)
を呼び出します。
iteratorの仕組みから、クラスにiteratorの振る舞いを追加するには次の2つのメソッドを定義します:
-
__next__()
メソッド -
__iter__()
メソッド:__next__()
メソッドをもつオブジェクトを返す
上記メソッドを定義したクラスが以下の通り:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self # __next__()を持つReverseを返す
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
こちらのクラスは
for char in Reverse("Python"):
print(char)
のように使用することができます。
Generator
generatorを用いると上記クラスについて、もっとコンパクトに実装が可能です。
ジェネレータ は、イテレータを作成するための簡潔で強力なツールです。ジェネレータは通常の関数のように書かれますが、何らかのデータを返すときには yield 文を使います。そのジェネレータに対して next() が呼び出されるたびに、ジェネレータは以前に中断した処理を再開します (ジェネレータは、全てのデータ値と最後にどの文が実行されたかを記憶しています)。
generatorは次のような機能を有しています:
-
__iter__()
メソッドと__next__()
メソッドが自動で作成される - 呼び出しごとにローカル変数と実行状態が自動的に保存される
- 終了時に自動的にStopIterationを送出する
この機能を使用すると、Reverse
クラスと同じようなiteratorを次のように実現可能です:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index] # 関数定義内で`yield`を使用することで、`reverse`関数はgenerator関数となる
出力はReverse
クラスと同じになるように次のようにしました:
chars = reverse("Python")
for char in chars:
print(char)
print(type(chars)) # <class generator>
7. まとめ
今回は、PythonのClassについて復習したものに関する記事でした。Python特有の事情や定義方法があるため調べる対象として非常に捗りました。
そのせいもあって、思ったよりも寄り道が多くなってしまいました。
恐らく、一通りは復習できたと思います。
次回は、現在放置中のFletのクラス化に取り組んでいきます。