Edited at

~Pythonistaより愛をこめて贈るPython入門者のためのTips②~

More than 1 year has passed since last update.

Tips①の続きです

さっくり書いていくので厳密でないところが多いと思いますが、間違いがあれば是非コメントお願いします〜


リスト,タプル,集合,辞書のTips


リスト型

リスト型はもう何度か使ってるのでわかってると思いますが、サイズ指定のない配列ですね。listオブジェクトですので、a = []の他にa = list()と書いても大丈夫です。

リスト型では要素の順序が保証されています。


Python3.5.0

>>> a = [1,2,3,4,5,6,7,8,9]

>>> a.append(0) #後ろに要素0を追加
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
>>> a.insert(1,0) #指定のインデックス番号に要素0を追加
>>> a
[1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 0]
>>> a[0] #インデックスが0番目の要素にアクセス
1
>>> a[2] #インデックスが2番目の要素にアクセス
2
>>> a[-1] #インデックスが後ろから1番目の要素にアクセス(0はない)
0
>>> a[2:5] #インデックスが2番めから5番目の要素にアクセス
[2, 3, 4]
>>> a[2:-2] #インデックスが先頭2番から後方2番目の要素にアクセス
[2, 3, 4, 5, 6, 7, 8]
>>> a[2:-2:2] ##インデックスが先頭2番から後方2番目の要素に2ステップずつアクセス
[2, 4, 6, 8]
>>> b = [1.1, 1.2, 1.3, 1.4, 1.5]
>>> a.extend(b) #リスト同士の結合
>>> a
[1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1.1, 1.2, 1.3, 1.4, 1.5]
>>> a.pop() # リストの一番後ろの要素を取り出し削除
1.5
>>> a
[1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1.1, 1.2, 1.3, 1.4]
>>> sorted(a) #ソート(昇順)
[0, 0, 1, 1.1, 1.2, 1.3, 1.4, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sorted(a, reverse=True) #ソート(降順)
[9, 8, 7, 6, 5, 4, 3, 2, 1.4, 1.3, 1.2, 1.1, 1, 0, 0]
>>> len(a) #リストの長さ
16


リスト内包表記

やっとここまで来た…。

これぞPython特有なのでは!!

先程、for文で出た例


Python3.5.0

>>> for i in range(1,10):

... a.append(i)
...
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

これなんと1行で書けるんです。


Python3.5.0

>>> a = [i for i in range(1,10)]

>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

初見はちょっと気持ち悪いかもしれないですが、慣れると相当便利です。ちなみに普通にfor文で回すより実行速度が速いそうです。


Python3.5.0

>>> a = [i for i in range(1,10) if i % 2 == 0]

>>> a
[2, 4, 6, 8]
>>> a = [i if i % 2 == 0 else i*i for i in range(1,10)]
>>> a
[1, 2, 9, 4, 25, 6, 49, 8, 81]
>>> a = [[j for j in range(i)] for i in range(1,10)]
>>> a
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7, 8]]
>>>

いくらでも難読化出来てしまうので程々に…。


オブジェクトは基本的に参照渡し

注意点として、Pythonではオブジェクトは参照渡しである点です。

int/float/str/unicode(2.x系)は値渡しに見えますが、不変オブジェクトの参照渡しです(間違っていないよね...)。

まぁ意識すべきは、リスト型は参照渡しだってことですかね(タプル型、集合型、辞書型も同様)。

なので、他の変数や関数に引数として値渡しする場合には明示的にコピーする必要があります。


Python3.5.0

>>> a = [i for i in range(10)]

>>> b = a
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a.pop()
9
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> b #bも消えてる!!
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> c = list(a) # これでコピーになる(異なるインスタンスが生成される)
>>> from copy import copy
>>> d = copy(c) #copyモジュールも使える
>>> a.pop()
8
>>> a
[0, 1, 2, 3, 4, 5, 6, 7]
>>> b
[0, 1, 2, 3, 4, 5, 6, 7]
>>> c
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> d
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>>

ただ注意が必要なのは、list(a)copy(a)は浅いコピーです。

浅いコピーとは、リストの中にリストがネストされているような深いオブジェクトはコピーされず、もっとも浅い部分のオブジェクトのみコピーされそれより深いオブジェクトは参照渡しされます(説明難しい)。

これは、copyモジュールのdeepcopyを使うことで解決されます。


Python3.5.0

>>> a = [[j for j in range(4)] for i in range(4)]

>>> a
[[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> from copy import copy
>>> b = copy(a) #浅いコピー
>>> a[0].pop() #要素のリストからpop
3
>>> a #要素のリストからpopしたものが消える
[[0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> b #コピーしたリストの要素のリストからpopしたものが消える!!
[[0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> from copy import deepcopy
>>> c = deepcopy(a) #深いコピー
>>> a[0].pop()
2
>>> a
[[0, 1], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> b
[[0, 1], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> c #消えない!!
[[0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> a is b #最も浅いオブジェクトのインスタンスは異なる
False
>>> a[0] is b[0] #それよりも深いところに格納されているオブジェクトは参照渡しで同じインスタンスになる
True
>>> a[0] is c[0] #deepcopy↓オブジェクトは深いオブジェクトも値渡しになっている
False
>>>


タプル型

タプル型は、tupleで表現されます。

タプル型は基本的にリストと同じように要素を並べたものですが、一度値を格納するとその値は変更できません。


Python3.5.0

>>> a = tuple() #空のタプル

>>> a = (1,2,3,4,5) #1~5を順に並べたタプル
>>> a[1] #インデックスが1番目の要素にアクセス
2
>>> a[1:3] #インデックスが1番目から3番目までの要素にアクセス
(2, 3)
>>> a[0:5:2] #インデックスが0番目から5番目までの要素にステップ2でアクセス
(1, 3, 5)
>>> a[0] = 1 #代入するとエラーになる
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> a = (1,) #タプルを生成
>>> a
(1,)
>>> a = (1) #,がないとタプルにならない
>>> a
1
>>>

注意点としては、カンマがないとタプルにならないってことくらいですかね。


集合型

集合型は、setで表現されます。

集合型ではリスト型のように要素の集まりですが、要素の重複を許さないのと、順序が保証されていません。


Python3.5.0

>>> a = set() #空の集合

>>> a
set([])
>>> a = {i for i in range(10)} #内包表記
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.add(10) #10を追加
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> a.add(10) #10を追加(既にあるので変わらない)
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> a = set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> a.remove(10) #10を削除
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.remove(10) #10を削除(要素にないのでエラーになる)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 10
>>> a.add(10)
>>> a.discard(10) #10を削除
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a.discard(10) #10を削除(なければ何もしない)
>>> a
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b = {1,2,3,4,5}
>>> b.issubset(a) #bはaの部分集合かどうか
True
>>> b.issuperset(a) #aはbの部分集合かどうか
False
>>> c = {2,4,6,8,10,12,14,16,18,20}
>>> a.union(c) #2つの集合の和集合
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20])
>>> a.intersection(c) #2つの集合の積集合
set([8, 2, 4, 6])
>>> a.difference(c) #aの要素かつcの要素でない(a-c)集合
set([0, 1, 3, 5, 7, 9])
>>> a.symmetric_difference(c) #2つの集合の排他的論理和
set([0, 1, 3, 5, 7, 9, 10, 12, 14, 16, 18, 20])
>>> a_copy = a.copy() #集合のコピー(値渡し)
>>> a is a_copy #値渡しにより違うインスタンスになっている
False
>>> a == a_copy #値は同じ
True
>>> a_copy.update(c) #対象の集合に他の集合の要素を追加
>>> a_copy
set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20])
>>> a_copy.pop() #任意の要素を取り出して削除
0
>>> a_copy.clear() #すべての要素を削除
>>> a_copy
set([])
>>>

@zakuro9715 さんのご指摘をうけ、ハッシュ化に関して追記と修正しました

Twitterでご指摘受けハッシュ化に関して修正しました

集合の使いどころとして、要素の重複を避けたい場合もそうですが要素がハッシュ化とソートされている点が重要です(要素の重複をなくすためにそうしたのかな。多分そう)。

これは何かというと、要素を走査するときにハッシュテーブルを使っているので平均でO(1)で行ける※が、そうでないとO(N)になるためです。つまり、a in bのようにしたときに能力を発揮します。

リスト型は、要素がハッシュ化されていないためN回のループでa in bのようにすると下手するとN×len(b)回の走査が必要です。

一方で、集合型(辞書型のキーも)ではハッシュ化されているためそのような走査は行われません。

なので、ループ回数が多く要素も多い場合は集合型にキャストしてからa in bのような条件は判断をすることをオススメします。

※この件でいくつかご指摘いただきましたが正しくはハッシュテーブルのようです(リンク)。


辞書型

辞書型は、dictで表現されます。

辞書型ではキーと値のペアで格納されます。

@zakuro9715 さんのご指摘をうけ、キーの設定に関して追記と修正しました

キー(または値)はどんなオブジェクトでも設定できます。

キーには hashableなオブジェクトであれば 設定可能です。

(例えば、dictをキーに持つdictは設定できません)

値に関しては任意のオブジェクトを設定可能です。


Python3.5.0

>>> a = dict() #空の辞書型を生成

>>> a = {str(i)+u"番目": i*i for i in range(10)}
>>> a
{'1番目': 1, '0番目': 0, '7番目': 49, '4番目': 16, '8番目': 64, '6番目': 36, '9番目': 81, '5番目': 25, '2番目': 4, '3番目': 9}
>>> a["0番目"] #"0番目"というキーの値を取得
0
>>> a["5番目"] #"5番目"というキーの値を取得
25
>>> a["10番目"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: '10番目'
>>> a.get(u"10番目") #"10番目"というキーの値を取得(ないがエラーではなくNoneを返す)
>>> a.get(u"10番目", 100) #"10番目"というキーの値を取得。なければ第2引数に設定した値を返す
100
>>> a.get(u"9番目", "100") #"9番目"というキーの値を取得。なければ第2引数に設定した値を返す
81
>>> a.keys() #キーをdict_keysというオブジェクトで返す(2系ではリスト)
dict_keys(['4番目', '3番目', '5番目', '2番目', '8番目', '0番目', '9番目', '1番目', '7番目', '6番目'])
>>> a.values() #値をdict_valuesというオブジェクトで返す(2系ではリスト)
dict_values([16, 9, 25, 4, 64, 0, 81, 1, 49, 36])
>>> a.items() #キーと値のタプルをdict_itemsというオブジェクトで返す(2系ではリスト)
dict_items([('4番目', 16), ('3番目', 9), ('5番目', 25), ('2番目', 4), ('8番目', 64), ('0番目', 0), ('9番目', 81), ('1番目', 1), ('7番目', 49), ('6番目', 36)])
>>> "1番目" in a #キーが有るか確認
True
>>> del a["0番目"] #該当のキーの値を削除
>>> a.pop(u"1番目") #該当のキーの値を取得し削除
1
>>> a.popitem() #任意のitemを取得し削除
('4番目', 16)
>>> a
{'3番目': 9, '5番目': 25, '2番目': 4, '8番目': 64, '9番目': 81, '7番目': 49, '6番目': 36}
>>> b = a #参照渡し
>>> c = a.copy() #値渡し
>>> a is b #同じインスタンス
True
>>> a is c #異なるインスタンス
False
>>> a == c #値は同じ
True
>>> a.clear() #すべて削除
>>> a
{}
>>> b #同じインスタンスなので削除される
{}
>>> c #異なるインスタンスなので変わらない
{'9番目': 81, '5番目': 25, '2番目': 4, '8番目': 64, '3番目': 9, '7番目': 49, '6番目': 36}
>>>
>>> a = {str(i)+"番目": i*i for i in range(10)} #辞書内包表記
>>> a_copy = a.copy()
>>> a_copy.update({"0番目": 10})
>>> a_copy["0番目"]
10
>>>


リストと辞書のTips

特にリストと辞書においてカウントしたりイテレーションするときに色々便利な方法があるので、独断と偏見でよく使いそうなものを紹介します。


便利なイテレーション


Python3.5.0

>>> a = [i for i in range(1,6)]

>>> #インデックス番号と一緒に出力
>>> for i, num in enumerate(a):
... print "{}番目のインデックスの要素:{}".format(i, num)
...
0番目のインデックスの要素:1
1番目のインデックスの要素:2
2番目のインデックスの要素:3
3番目のインデックスの要素:4
4番目のインデックスの要素:5
>>> #2つのリストを同時にイテレーションする
>>> b = [str(i)+u"番目のインデックス" for i in range(5)]
>>> for a_num, b_num in zip(a, b):
... print(b_num+u":"+str(a_num))
...
0番目のインデックス:1
1番目のインデックス:2
2番目のインデックス:3
3番目のインデックス:4
4番目のインデックス:5
>>> c = [[j**i for j in range(1,5)] for i in range(1,5)]
>>> c
[[1, 2, 3, 4], [1, 4, 9, 16], [1, 8, 27, 64], [1, 16, 81, 256]]
>>> #転置にも使える
>>> c_t = [[i, j, k, l] for i, j, k, l in zip(*c)]
>>> c_t
[[1, 1, 1, 1], [2, 4, 8, 16], [3, 9, 27, 81], [4, 16, 64, 256]]
>>>
>>> from itertools import product, permutations, combinations, combinations_with_replacement
>>> #for文をネストした場合の動作(repeatでネストの深さを指定できる)
>>> for pear in product(a, repeat=2):
... print(pear)
...
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(2, 5)
(3, 1)
(3, 2)
(3, 3)
(3, 4)
(3, 5)
(4, 1)
(4, 2)
(4, 3)
(4, 4)
(4, 5)
(5, 1)
(5, 2)
(5, 3)
(5, 4)
(5, 5)
>>> #被りを許さない順列
>>> for pear in permutations(a, 2):
... print(pear)
...
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 1)
(2, 3)
(2, 4)
(2, 5)
(3, 1)
(3, 2)
(3, 4)
(3, 5)
(4, 1)
(4, 2)
(4, 3)
(4, 5)
(5, 1)
(5, 2)
(5, 3)
>>> #被りを許さない組み合わせ
>>> for pear in combinations(a, 2):
... print(pear)
...
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 3)
(2, 4)
(2, 5)
(3, 4)
(3, 5)
(4, 5)
>>> #被りを許す組み合わせ
>>> for pear in combinations_with_replacement(a, 2):
... print(pear)
...
(1, 1)
(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 2)
(2, 3)
(2, 4)
(2, 5)
(3, 3)
(3, 4)
(3, 5)
(4, 4)
(4, 5)
(5, 5)
>>>


特殊な辞書(defaultdict, Counter)


Python3.5.0

>>> from itertools import combinations

>>> from collections import defaultdict, Counter
>>>
>>> #辞書がネストされている辞書の作成
>>> a = {}
>>> for key, val in combinations([i for i in range(6)], 2):
... if key not in a:
... a[key] = {}
... a[key][val] = key*val
...
>>> a
{0: {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}, 1: {2: 2, 3: 3, 4: 4, 5: 5}, 2: {3: 6, 4: 8, 5: 10}, 3: {4: 12, 5: 15}, 4: {5: 20}}
>>>
>>> a = defaultdict(dict)
>>> for key, val in combinations([i for i in range(6)], 2):
... a[key][val] = key*val
...
>>> a
defaultdict(<type 'dict'>, {0: {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}, 1: {2: 2, 3: 3, 4: 4, 5: 5}, 2: {3: 6, 4: 8, 5: 10}, 3: {4: 12, 5: 15}, 4: {5: 20}})
>>>
>>>
>>> #あるキーに対するカウントを辞書に格納する場合
>>> a = {}
>>> for key, val in combinations([i for i in range(6)], 2):
... if key in a:
... a[key] += val
... else:
... a[key] = val
...
>>> a
{0: 15, 1: 14, 2: 12, 3: 9, 4: 5}
>>> a = Counter()
>>> for key, val in combinations([i for i in range(6)], 2):
... a[key] += val
>>> a
Counter({0: 15, 1: 14, 2: 12, 3: 9, 4: 5})
>>>

他にもあるので気になる人はリファレンスをどうぞ。


関数

では関数の書き方について。

ここでは基本的な関数の書き方として、関数の定義文/引数の書き方(デフォルトの設定)/戻り値(return,yield)について書こうかと思います。


Python3.5.0

>>> #最も基本的な書き方

>>> def hello():
... print("Hello world!!")
...
>>> hello()
Hello world!!
>>>
>>> #引数がある場合の書き方
>>> def hello_name(name):
... print("Hello {}!!".format(name))
...
>>> hello_name("Guido")
Hello Guido!!
>>>
>>> #引数にデフォルト値がある場合の書き方
>>> def hello_name(name="world"):
... print("Hello {}!!".format(name))
...
>>> hello_name()
Hello world!!
>>> hello_name("Guido")
Hello Guido!!
>>>
>>> #戻り値がある場合の書き方
>>> def hello_return(name):
... return "Hello {}!!".format(name)
...
>>> result = hello_return("Guido")
>>> print(result)
Hello Guido!!
>>>
>>> #戻り値(ジェネレータ式)がある場合の書き方
>>> def hello_yield(name):
... for s in name:
... yield s
...
>>> generater = hello_yield("Hello Guido!!")
>>> generater.next()
'H'
>>> generater.next()
'e'
>>> generater.next()
'l'
>>> generater.next()
'l'
>>> generater.next()
'o'
>>> generater.next()
' '
>>> generater.next()
'G'
>>> generater.next()
'u'
>>> generater.next()
'i'
>>> generater.next()
'd'
>>> generater.next()
'o'
>>> generater.next()
'!'
>>> generater.next()
'!'
>>> generater.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

入門者としてはジェネレータ式が分かりづらいかもしれませんが、難しく考える必要はなく「前の呼び出し状態を記憶してnext()で次の状態を返す」と考えておけば大丈夫です。

次の状態がない場合にはStopIterationクラスの例外を出すので、実際に使う場合にはtry/exceptで制御するといいでしょう。


Python3.5.0

>>> generater = hello_yield("Hello Guido!!")

>>> while True:
... try:
... result = generater.next()
... print(result)
... except StopIteration:
... break
...
H
e
l
l
o

G
u
i
d
o
!
!
>>>



クラス


2.x系と3.x系の違い

まずクラスの基本的な書き方について、2系と3系だとちょっと違うのでそこ書いておきます。


Python2.7.9

>>> class Hello(object):

... prefix = "Hello "
... def hello(self, name="world"):
... print(self.prefix+name+"!!")
...
>>> HelloInstance = Hello()
>>> HelloInstance.hello()
Hello world!!
>>>

これは2系の書き方で、objectという基底クラスを継承してるのですがこれは新クラススタイルです。

helloというインスタンスメソッドの第1引数にあるselfというのは予約語で必ず書いて下さい。

これは自分自身(インスタンス)を指すためのもので、PHPでいうthisです。

つまり、Instance.method(val)と書くとClass.method(self, val)に読み替えられます。

また、旧クラススタイルもありobjectを書かずにclass Hello:のみで書けます。

しかし、新スタイルと旧スタイルでは仕様が異なる(継承の優先順位/クラスメソッド、スタティックメソッド/プロパティ)ので推奨されるのは新クラススタイルです。

これも古い仕様を壊さずに改良を加えたため、このような形になりました。

一方、3系ではclass Hello:とかくと2系でいう新クラススタイルになります。

動作は変わりませんが一応書いておきます。


Python3.5.0

>>> class Hello:

... prefix = "Hello "
... def hello(self, name="world"):
... print(self.prefix+name+"!!")
...
>>> HelloInstance = Hello()
>>> HelloInstance.hello()
Hello world!!
>>>


コンストラクタ

厳密に言うとコンストラクタではないのですが、Pythonには__init__というインスタンスを生成するときに呼び出されるメソッドがありここにコンストラクタ的な処理を書ける。


Python3.5.0

>>> class Hello:

... def __init__(self, prefix="Hello "):
... self.prefix = prefix
... def hello(self, name="world"):
... print(self.prefix+name+"!!")
...
>>> HelloInstance = Hello()
>>> HelloInstance.hello()
Hello world!!
>>> HelloInstance.hello("Guido")
Hello Guido!!
>>> HelloInstance = Hello("Hey ")
>>> HelloInstance.hello("Guido")
Hey Guido!!
>>>

ちなみに、__new__という__init__よりも先に呼ばれるメソッドがあります。

これはそれ自身のインスタンスを返し、自分自身のインスタンスが返されると__init__が実行されます。

__new__をカスタマイズして自分以外のインスタンスを返すことも出来ますがメリットはない。。。

まぁ全く無いわけではなく、メタクラスを書けるわけですがそれは入門者に向けて書くことか?という気もするので割愛します。

(入門者から脱するためのTips/中級者のためのTips等も別記事で書いていこうと思います)


メソッドの引数(インスタンス、クラス、スタティック)

Pythonではクラス内でメソッドを定義した場合は、特に断りがないかぎりインスタンスメソッドです。

既に説明したように、インスタンスメソッドの第一引数はselfであり、これは自分自身のインスタンスを指しています。

しかし、その他にもクラスメソッドやスタティックメソッドも書けます。

インスタンスメソッド、クラスメソッド、スタティックメソッドの書き方とともに動作違いも以下に示しますが、そもそもオブジェクト指向をなんとなくでもわかってないとちょっとわからないかもです。


Python3.5.0

>>> class Hello:

... prefix = "Hello "
... def __init__(self, prefix=None):
... if prefix:
... self.prefix = prefix + " "
... def hello(self, name="world"):
... """
... インスタンスメソッド:第一引数はself
... """

... print(self.prefix+name+"!!")
... @classmethod
... def hello_cls(cls, name="world"):
... """
... クラスメソッド:第一引数はcls
... """

... print(cls.prefix+name+"!!")
... @staticmethod
... def hello_stc(name="world"):
... """
... スタティックメソッド:第一引数は予約語ではない
... """

... print(name+"!!")
... @classmethod
... def set_class_prefix(cls, prefix):
... cls.prefix = prefix + " "
...
>>> HelloInstance1 = Hello("Hey") #インスタンスのprefix(self.prefix)をHeyに書き換え
>>> HelloInstance1.hello("Guido!!")
Hey Guido!!!!
>>> HelloInstance1.hello_cls("Guido!!") #クラスメソッドなのでアクセスするprefixはクラスのprefix
Hello Guido!!!!
>>>
>>> HelloInstance2 = Hello("Hi") #インスタンスのprefix(self.prefix)をHeyに書き換え
>>> HelloInstance2.hello("Guido!!")
Hi Guido!!!!
>>> HelloInstance2.hello_cls("Guido!!")
Hello Guido!!!!
>>>
>>> HelloInstance2.set_class_prefix("I'm") #HelloInstance2からクラス変数を書き換え
>>> HelloInstance2.hello("Guido!!") #インスタンス変数は書き変わらない
Hi Guido!!!!
>>> HelloInstance2.hello_cls("Guido!!") #クラス変数は書き変わる
I'm Guido!!!!
>>> #別のインスタンス
>>> HelloInstance1.hello("Guido!!") #インスタンス変数は書き変わらない
Hey Guido!!!!
>>> HelloInstance1.hello_cls("Guido!!") #クラス変数なので書き換わる!!
I'm Guido!!!!
>>>
>>> Hello.hello_stc() #スタティックメソッドはクラスから呼び出せる
world!!
>>>

注目すべき点は、クラスメソッドからクラス変数を書き換えると別インスタンスのクラス変数も変わることろですかね(インスタンス変数ではなくクラス変数なのでまぁ当たり前っちゃ当たり前)。


アンダースコア地獄と特殊メソッド

Pythonを使っているとよく見る__name__,__init__,__new__,__call__などの__(アンダースコア2つ)ですが、特殊メソッドなどに用いられます。

以下によく出る特殊メソッドを示します。

特殊メソッド
機能
使用例

__doc__
ドキュメント文字列
オブジェクトに記載されたドキュメントを見る。object.__doc__

__name__
importされたファイル名または実行ファイル
ファイルを実行する際。if __name__ == '__main__'

__dict__
オブジェクトの属性が格納されている
文字列からオブジェクトの属性を取得等

__init__
インスタンス生成時に呼び出されるメソッド
X = Class()

__new__
ラスの新たなインスタンスを生成する際のファクトリクラス
オーバライドしてmetaクラスを作ることも可能

__call__
関数実行時に呼び出されるメソッド
X()

__del__
オブジェクトを破棄する際に呼び出されるメソッド
del object

__add__
加算(+)演算子を使用する際に呼び出されるメソッド
オーバライドしてカスタマイズ可能

__iter__
ループ時に呼び出されるメソッド
forループ。内包表記

__getattr__
属性へのアクセス
X.attr

__setattr__
属性への代入
X.attr = x


多重継承とネームマングリング

Pythonでは多重継承可能です。

念のため多重継承の例を示します。


Python3.5.0

>>> class Hello:

... prefix = "Hello "
... def say(self, name):
... return self.prefix + name
...
>>>
>>> class Hey:
... prefix = "Hey "
... def say(self, name):
... return self.prefix + name + "!"
...
>>>
>>> class HowAreYou(Hello, Hey):
... def __init__(self, name, *args, **kargs):
... #親クラスの初期化
... #この例では親クラスに__init__が定義されていないのでなにも実行されない
... super(HowAreYou, self).__init__(*args, **kargs)
... self.name = name
... def output(self):
... greet = self.say(self.name)
... print(greet, "How are you?")
...
>>>
>>> X = HowAreYou("Guido")
>>> X.output()
('Hello Guido', 'How are you?')
>>>

上記のように、クラスを2つ以上継承してクラスを作るようなものですね。まぁここ説明するならオブジェクト指向から説明しないと行けないんですがね。それは他の記事にお任せします。

しかしここで注目すべきは、HowAreYou.output()を実行した時の動き。多重継承の場合、属性同士が衝突する可能性があります。

この例でも(意図的ですが)、sayという属性同士が衝突しています。

Pythonでは多重継承をした場合にどの親クラスを優先するか決まっています。

基本的に、①「左から右」②「下から上」という順で①が②よりも優先されます(旧スタイルクラスでは②が①より優先される)。

なので今回の場合は、HowAreYou.output()Hello.say()を優先しています。同じようにself.prefixHello.prefixが優先されます。

もちろん、多重継承はしたいがどうしても衝突してほしくない属性があると思います(もちろん多重継承しないように設計するのがいいと思いますが)。

そのとき使う機能が ネームマングリング です。

具体的には、アンダースコアを2つ属性名の前に付けます。

そうすると自動的に_Class__attrという風に解釈されます。


Python3.5.0

>>> class Hello:

... __prefix = "Hello "
... def __say(self, name):
... return self.__prefix + name
...
>>> class Hey:
... __prefix = "Hey "
... def __say(self, name):
... return self.__prefix + name + "!"
...
>>> class HowAreYou(Hello, Hey):
... def __init__(self, name, *args, **kargs):
... #親クラスの初期化
... #この例では親クラスに__init__が定義されていないのでなにも実行されない
... super(HowAreYou, self).__init__(*args, **kargs)
... self.name = name
... def hello_output(self):
... greet = self._Hello__say(self.name) #Helloの__sayを呼び出す
... print(greet, "How are you?")
... def hey_output(self):
... greet = self._Hey__say(self.name) #Heyの__sayを呼び出す
... print(greet, "How are you?")
...
>>> X = HowAreYou("Guido")
>>> X.hello_output() #Helloのsayを使用
('Hello Guido', 'How are you?')
>>> X.hey_output() #Heyのsayを使用
('Hey Guido!', 'How are you?')
>>> dir(X) #Xの属性を表示
['_Hello__prefix', '_Hello__say', '_Hey__prefix', '_Hey__say', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'hello_output', 'hey_output', 'name']
>>>

上記のように、アンダースコアを2つ(__)付けるだけで属性名の前に_クラス名が付与されています。

書いて思ったのですが、これ入門者に教えることじゃないかもʕº̫͡ºʔ


隠蔽はあるのか問題

PythonではJavaのように属性の隠蔽が出来ません。

しかし、アンダースコアを使うことでそれっぽい事はできます。

すでに説明したように、ネームマングリングを用いることで異なるクラスで同じ名前の属性同士が衝突しないようにできます。

本来の使い方ではないですが、この機能を使うことでどうしても隠蔽したい属性を隠蔽する(ようなこと)事ができます。

また、_x__xで少し意味が異なる(使い分けができる)ので説明しておきます。


Python3.5.0

>>> class Hello:

... def __init__(self):
... self._prefix = "Hello" #アンダースコア1つ
... self.__prefix = "Hey" #アンダースコア2つ
...
>>> HelloInstance = Hello()
>>> HelloInstance._prefix #アンダースコア1つの属性にはアクセスできる
'Hello'
>>> HelloInstance.__prefix #アンダースコア2つの属性にはそのままではアクセス出来ない
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Hello' object has no attribute '__prefix'
>>> HelloInstance._Hello__prefix #ネームマングリングに従いアクセスはできる
'Hey'
>>>

アンダースコア1つの場合は、アクセスは出来てしまうのであくまでコーディング規約としてのローカル変数を示すためのプレフィックスです。

しかしアンダースコア2つの場合は、ネームマングリングにより自動的に_クラス名が付与されるため、そのままの名前ではアクセス出来ません。

このように完全には隠蔽することは出来ませんが、擬似的に隠蔽することは可能です。


疲れた②

さっくり書いたが疲れたので続きは③で

Tips①はこちら

Tips③はこちら