Python の identity って何?


次の4つを通して identity とは何かについて説明します。

  1. identity って何?
  2. 変数, 属性って何?
  3. 代入って何?
  4. copy と deepcopy って何?

0. identity って何?

f:id:domodomodomo:20171022155650j:plain f:id:domodomodomo:20171022155655j:plain f:id:domodomodomo:20171022155658j:plain f:id:domodomodomo:20171022155701j:plain f:id:domodomodomo:20171022155704j:plain f:id:domodomodomo:20171022155706j:plain

◯ identity

変数は、オブジェクトを参照しています。 どのように参照しているかというと identity を使っています。実は、各オブジェクトには identity という背番号のような数字が割り当てられています。変数には、オブジェクトそのものを保存しているのではなく、identity という数字、背番号を保存して、オブジェクトを操作できるようにしています。

名前 (name) は、オブジェクトを参照します。
4.2.1. 名前の束縛

◯ id 関数

オブジェクトの identity を調べたい時は id 関数を使います。

>>> id(yaruo)
4479618016
>>>

id(object)
オブジェクトの “identity” を返します。(中略)
CPython 実装の詳細: identity は、メモリ空間上のオブジェクトのアドレスです。

id(object)
Return the “identity” of an object. (Omitted)
CPython implementation detail: This is the address of the object in memory.
2. Built-in Functions

CPython では identity は、ポインタになります。実際 CPython のコードを見てみるとポインタを使ってオブジェクトを操作していることがわかります。identity からさらに特殊な値に変換して、色々ごちゃごちゃしたりというようなことはしてなさそうでした。




1. 変数と属性って何?





2. 代入って何?

◯ いろいろな代入の仕方

◯ 1つの変数に代入

a = 1


◯ 2つ以上の変数に代入

#
a = b = c = 1


◯ 2つ以上の変数に異なるオブジェクトを代入

#
a = 0; b = 1; c = 2

#
a, b, c = 0, 1, 2


# tuple
a, b, c = (0, 1, 2)


# list
a, b, c = [0, 1, 2]


# range
a, b, c = range(3)


# function
def f():
    return 0, 1, 2

a, b, c = f()


# iterator
a, b, c = (i for i in range(3))


# stdin 1
# 標準入力から 0, 1, 2 を入力
a, b, c = eval(input())
0, 1, 2


# stdin 2
# 標準入力から 0, 1, 2 を入力
# stdin 1 に比べて長いけど、確実に int が代入される安全な書き方
a, b, c = map(int, input().replace(' ', '').split(',')) 
0, 1, 2


for 文の in の中に書けるものを右辺において複数同時に代入することができます。ちなみに 0, 1, 2 も 関数 f も tuple を生成しています。

>>> 0, 1, 2
(0, 1, 2)
>>> 
>>> f()
(0, 1, 2)
>>>









f:id:domodomodomo:20171023175156j:plain






◯ 代入とは

f:id:domodomodomo:20171022155650j:plain f:id:domodomodomo:20171022155655j:plain f:id:domodomodomo:20171022155658j:plain f:id:domodomodomo:20171022155701j:plain f:id:domodomodomo:20171022155704j:plain f:id:domodomodomo:20171022155706j:plain

identity と名前を対応づけることです。 変数に代入してもオブジェクトは変化しませんが、属性に代入するとオブジェクトは変化します。 代入しても identity が渡されるだけでオブジェクトがコピーされる訳ではありません。

◯ 束縛

このようにある値(ここではオブジェクトの id)を識別子(変数)に対応づけることを束縛と言います。

名前束縛 (Name binding) あるいは名前結合とは、値を識別子に対応付けることを意味する。
名前束縛


ここからは、すこし込み入った話をします。

◯ 2つのポイント


次の2つを違いを明確にすることは、大切なことです。

1 変数への代入オブジェクトの属性への代入
2 変数への代入コピー


◯ だから何?

こんなことを知って一体なんのためになるのでしょうか。実は、このことについて知らないと、思わぬところででつまずいてしまいます。 いましばらくお待ちください。

1-1. for 文内で代入しても代入できないのは何故 (´;ω;`)ブワッ
1-2. 関数, メソッド内で代入しても代入できないのは何故 ヽ( ^ω^ )7
2-1. 変数に代入してもオブジェクトが copy できないのは何故 (´;ω;`)ブワッ
2-2. list を copy して pop したら copy 元も pop された (´;ω;`)ブワッ


2.1. 変数への代入とオブジェクトの属性への代入は違う。


操作
結果
①変数への代入オブジェクトの属性は変化しない
②オブジェクトの属性への代入オブジェクトの属性が変化する
③シーケンスへの代入シーケンスの要素が変化する


① 変数への代入

# 変数 = オブジェクト
# a    = 1
a = 1


② オブジェクトの属性への代入

# オブジェクト. 属性 = オブジェクト
# obj       . otr = オブジェクト
obj.attr = 1


シーケンス の要素への代入

# シーケンスの要素 = オブジェクト
# シーケンス[番号] = オブジェクト
# sequence[ n ] = 1
sequence[n] = 1



2.1.1. for 文内での代入

こんなことで困ったことはありません?

for 文内で代入しても代入できないのは何故 (´;ω;`)ブワッ

問題

iterable なオブジェクト list の扱いについて見てみましょう。実行結果1, 2 には何が出力されるでしょうか?

# ① 変数への代入
list1 = ['y', 'a', 'r', 'u', 'o']
for element in list1:
    element = ''

print(list1)  # 実行結果 1

# ③  シーケンスへの代入
list3 = ['y', 'a', 'r', 'u', 'o']
for index in range(len(list3)):
    list3[index] = ''

print(list3)  # 実行結果 2
答え
>>> # ① 変数への代入
... list1 = ['y', 'a', 'r', 'u', 'o']
>>> for element in list1:
...     element = ''
... 
>>> print(list1)  # 実行結果 1
['y', 'a', 'r', 'u', 'o']
>>> 
>>> # ③  シーケンスへの代入
... list3 = ['y', 'a', 'r', 'u', 'o']
>>> for index in range(len(list3)):
...     list3[index] = ''
... 
>>> print(list3)  # 実行結果 2
['', '', '', '', '']
>>> 
解説
  1. element = ''
    変数への代入はオブジェクトを変化させません。 変数 element が格納している identity を変えています。 ひたすら変数 element を書き換えてるだけです。

  2. list3[index] = ''
    シーケンスの要素への代入はオブジェクトを変化させます。 リストオブジェクト list3 の index 番目に束縛されている identity を別の identity に書き変えています。



2.1.2. 関数の引数への代入

こんなことで困ったことはありませんか?

関数, メソッド内で代入しても代入できないのは何故 ヽ( ^ω^ )7

問題

次のような Person クラスについて考えてみましょう。実行結果 1, 2 には何が出力されるでしょうか?

class Person():
    def __init__(self, name):
        self.name = name


person = Person('やる夫')
print(person.name)
print(id(person.name))


#
# 変数への代入
#
def change_name(name):
    # 変数 = オブジェクト
    name = '岩倉玲音'
    print(name)
    print(id(name))


change_name(person.name)
print(person.name)  # 実行結果 1
print(id(person.name))


#
# 属性への代入
#
def change_person_name(person):
    # オブジェクト.属性 = オブジェクト
    person.name = 'サーバルちゃん'
    print(person.name)
    print(id(person.name))


change_person_name(person)
print(person.name)  # 実行結果 2
print(id(person.name))
答え
>>> # 抜粋したもの
>>> print(person.name)  # 実行結果 1
やる夫
>>> print(person.name)  # 実行結果 2
サーバルちゃん
>>>
解説

実行結果 1 では変化しましたが、実行結果 2 では変化しませんでした。これは change_name が変数 name への代入なので、オブジェクトは変化しませんでした。change_person_name は、属性 person.name への代入なので、オブジェクトが変化しました。

評価戦略

Python は値渡しです。これは identity という値を渡しているからです。参照渡しではありません。
Wikipedia > 引数 > 評価戦略

引数は 値渡し (call by value) で関数に渡されることになります(ここでの 値 (value) とは常にオブジェクトへの参照(reference) をいい、オブジェクトの値そのものではありません) [1]。

脚注
[1] 実際には、オブジェクトへの参照渡し (call by object reference) と書けばよいのかもしれません。というのは、変更可能なオブジェクトが渡されると、関数の呼び出し側は、呼び出された側の関数がオブジェクトに行ったどんな変更 (例えばリストに挿入された要素) にも出くわすことになるからです。
4.6. 関数を定義する

束縛

Python では代入するときだけではなく、以下の場合に束縛を行なっています。以下、太字は本稿で取り扱ったもの。

以下の構造で、名前が束縛されます:
 関数の仮引数 (formal parameter) 指定
 import 文、
 クラスや、
 関数の定義、
 代入が行われるときの代入対象の識別子、
 for ループのヘッダ
 with 文や except 節の as の後ろ。
名前束縛について


2.2. 代入とコピー

代入してもオブジェクはコピーされない

2.2.1. 変数への代入

こんなことで困ったことはありませんか?

変数に代入してもオブジェクトが copy できないのは何故 (´;ω;`)ブワッ

問題

GirlFriend クラスについて、考えて見ます。 変数に代入するとパッと見なんだか値がコピーされたように見えます。 実行結果 1, 3 には、何が表示されるでしょうか?

class GirlFriend():
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return "GirlFriend('" + self.name + "')"


x = GirlFriend('サーバルちゃん')
y = x
z = x

# パッと見、3人のサーバルちゃんが
# コピーされたように見えます...
x  # GirlFriend('サーバルちゃん')
y  # GirlFriend('サーバルちゃん')
z  # GirlFriend('サーバルちゃん')



#
# いま新しい GirlFriend が欲しくて y の名前を書き換えて見ました。
# さて x, z の名前はどうなるでしょうか?
#

y.name = 'かばんちゃん'

x  # 実行結果 1
y  # GirlFriend('かばんちゃん')
z  # 実行結果 3


解答

全員かばんちゃんになります。

>>> class GirlFriend():
...     def __init__(self, name):
...         self.name = name
...     
...     def __repr__(self):
...         return "GirlFriend('" + self.name + "')"
... 
>>> 
>>> x = GirlFriend('サーバルちゃん')
>>> y = x
>>> z = x
>>> 
>>> # パッと見、3人のサーバルちゃんが
... # コピーされたように見えます...
... x  # GirlFriend('サーバルちゃん')
GirlFriend('サーバルちゃん')
>>> y  # GirlFriend('サーバルちゃん')
GirlFriend('サーバルちゃん')
>>> z  # GirlFriend('サーバルちゃん')
GirlFriend('サーバルちゃん')
>>> 
>>> 
>>> 
>>> #
... # いま新しい GirlFriend が欲しくて y の名前を書き換えて見ました。
... # さて x, z の名前はどうなるでしょうか?
... #
... 
>>> y.name = 'かばんちゃん'
>>> 
>>> x  # 実行結果 1
GirlFriend('かばんちゃん')
>>> y  # GirlFriend('かばんちゃん')
GirlFriend('かばんちゃん')
>>> z  # 実行結果 3
GirlFriend('かばんちゃん')
>>> 


解説(どうしてこんな動作をするの?)

答え: 代入は変数にオブジェクトへの identity を渡しているだけだから。

次のコードを実行して見ると、実行結果 1, 2, 3 は、どのようになるでしょうか。全て同じ数字でしょうか?それとも全てバラバラの数字でしょうか?

class GirlFriend():
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return "GirlFriend('" + self.name + "')"


x = GirlFriend('サーバルちゃん')
y = x
z = x

# パッと見、3人のサーバルちゃんが
# コピーされたように見えます...
x  # GirlFriend('サーバルちゃん')
y  # GirlFriend('サーバルちゃん')
z  # GirlFriend('サーバルちゃん')

#
# 本当にそうなのでしょうか?
#
id(x)  # 実行結果 1
id(y)  # 実行結果 2
id(z)  # 実行結果 3


全て同じ数字が表示されました。 このことを踏まえると x, y, z は同じオブジェクトを指していた、参照していたと言うわけです。

>>> id(x)  # 実行結果 1
4343896048
>>> id(y)  # 実行結果 2
4343896048
>>> id(z)  # 実行結果 3
4343896048
>>> 


オブジェクトを作りたいなら、このように都度、インスタンスを生成する必要があります。

# 都度 instance を生成
x = GirlFriend('サーバルちゃん')
y = GirlFriend('かばんちゃん')
z = GirlFriend('岩倉玲音')


2.2.2. リストの代入

こんなことで困ったことはありませんか?

list を copy して pop したら copy 元も pop された (´;ω;`)ブワッ

問題 a

list の代入 いままでのことを踏まえて具体例を見て見ましょう。

l1 = m1 = n1 = ['y', 'a', 'r', 'u', 'o']

l1.pop()

# 実行結果
l1
m1
n1
id(l1) == id(m1) == id(n1)
解答 a
>>> # 実行結果
... l1
['y', 'a', 'r', 'u']
>>> m1
['y', 'a', 'r', 'u']
>>> n1
['y', 'a', 'r', 'u']
>>> id(l1) == id(m1) == id(n1)
True
>>> 


変数に代入しても、オブジェクトはコピーされません。 代入は、オブジェクトをコピーしているわけではなくて identity を渡しているだけ です。

id 関数を使うと True が返ってきていることから、 l1, m1, n1 には同じ identity が束縛されていることがわかります。

問題 b
l2 = ['y', 'a', 'r', 'u', 'o']
m2 = ['y', 'a', 'r', 'u', 'o']
n2 = ['y', 'a', 'r', 'u', 'o']

l2.pop() 

# 実行結果 2
l2
m2
n2
id(l2) == id(m2) == id(n2)
解答 b
>>> # 実行結果 b
... l2
['y', 'a', 'r', 'u']
>>> m2
['y', 'a', 'r', 'u', 'o']
>>> n2
['y', 'a', 'r', 'u', 'o']
>>> id(l2) == id(m2) == id(n2)
False
>>>


l2, m2, n2 には、それぞれ別々のリストオブジェクトを代入しました。
Python Copy Through Assignment? - Stack Overflow

3. copy と deepcopy の違い

Python には copy モジュールがあります。これを使ってオブジェクトをコピーすることができます。copy モジュールの中には copy 関数と deepcopy 関数があります。

deepcopy 関数は、わかりやすいです。全てをコピーしてくれます。ただし、str や int などの immutable なオブジェクトは、singleton として取り扱い、コピーしません。
Python で mutable と immutable の違い

copy 関数は、わかりにくいです。オブジェクトのあたまの部分だけを新しく生成して、属性に代入された値はコピーせず、identity だけ格納します。
8.10. copy — 浅いコピーおよび深いコピー操作

変数と属性に格納された identity を表示する structurerize という関数を作って、実際にどのようにコピーされているかを確認して見ました。

# original
imac = Computer(
    Cpu('2.3GHz', 5),
    Memory('8GB', '2133MHz', 'DDR4'),
    Ssd('256GB'))
pprint.pprint(structurerize(imac))

# copy
copy_imac = copy.copy(imac)
pprint.pprint(structurerize(copy_imac))

# deepcopy
deepcopy_imac = copy.deepcopy(imac)
pprint.pprint(structurerize(deepcopy_imac))
>>> imac = Computer(
>>>     Cpu('2.3GHz', 5),
>>>     Memory('8GB', '2133MHz', 'DDR4'),
>>>     Ssd('256GB'))
>>>
>>>
>>> # 4. copy VS deepcopy
>>> # 4.0. original
>>> pprint.pprint(structurerize(imac))
{'imac': (4359299312,
          {'auxiliary_memory': (4359299256, {'volume': 4359235264}),
           'cpu': (4359299144, {'clock': 4359234872, 'core': 4355374560}),
           'primary_memory': (4359299424,
                              {'clock': 4359235880,
                               'type_': 4359235208,
                               'volume': 4359235656})})}
>>>
>>> # 4.1. copy
>>> # 4.0. original と比べて 変数 copy_original に格納された identity だけ違います。
>>> copy_imac = copy.copy(imac)
>>> pprint.pprint(structurerize(copy_imac))
{'copy_imac': (4359237584,
               {'auxiliary_memory': (4359299256, {'volume': 4359235264}),
                'cpu': (4359299144, {'clock': 4359234872, 'core': 4355374560}),
                'primary_memory': (4359299424,
                                   {'clock': 4359235880,
                                    'type_': 4359235208,
                                    'volume': 4359235656})})}
>>> 
>>> # 4.2. deepcopy
>>> # 4.0. original と比べて全ての identity が異なります。
>>> # ただし、immutable な str, int のオブジェクトはコピーされません。 
>>> deepcopy_imac = copy.deepcopy(imac)
>>> pprint.pprint(structurerize(deepcopy_imac))
{'deepcopy_imac': (4359236072,
                   {'auxiliary_memory': (4359237416, {'volume': 4359235264}),
                    'cpu': (4359235344,
                            {'clock': 4359234872, 'core': 4355374560}),
                    'primary_memory': (4359235512,
                                       {'clock': 4359235880,
                                        'type_': 4359235208,
                                        'volume': 4359235656})})}
>>>


また、list, dict, set には専用の copy メソッドがあります。もし copy(list_) と書いたらどうなるのだろうと思われるかもしれませんが、list_.copy() が呼び出されているだけです。copy(list_) と list_.copy() は、同じことをしています。