Python の変数と属性、代入とコピー



この文章の目的は、次の2つの違いを明確にすることです。

1 章 代入コピーの違い
2 章 変数への代入属性への代入の違い




1 章では、代入してもオブジェクトは、コピーされないことを説明します。

2 章では、変数に代入してもオブジェクトは、変化しませんが、属性に代入するとオブジェクトが変化することを説明します。


◯ だから何?

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

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


◯ identity とは何か?

identity とは何かについて、簡単にイメージを押さえておいてください。


1. 代入とコピー

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

1.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('岩倉玲音')


1.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

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


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


① 変数への代入

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


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

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


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

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



2.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.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. change_name 関数
変数 name への代入なので、オブジェクトは変化しませんでした。

2. change_person_name 関数
属性 person.name への代入なので、オブジェクトが変化しました。

束縛

identity を変数, 属性などの箱にいれることを、一般に「束縛」と言います。

名前束縛あるいは名前結合とは、値を識別子に対応付けることを意味する。値に束縛された識別子を、その値への参照と呼ぶ。
束縛 (情報工学) - Wikipedia


Python では代入するときに、束縛を行なっています。これまで見てきた通り、関数や for 文を呼び出したりしたときにも同じように、束縛が行われていました。

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

評価戦略

"引数をいつどういう順序で評価し、仮引数は実引数にどう置換されるのか" を定めたものを評価戦略と言います。

プログラミング言語では、その意味のうち、サブルーチン呼び出しや演算子式の評価において引数をいつどういう順序で評価し、仮引数は実引数にどう置換されるのか、サブルーチン呼び出しや演算子式の値への置換はどうなのかといったことが、言語仕様によって、あるいは実装によって定義される(あるいは未定義とされる)。
Wikipedia > 評価戦略 - Wikipedia


◯ 値渡し call by value

f:id:domodomodomo:20180412062710j:plain

def function(parameter):
    print(parameter)

argument = 'Hello, world!'
function(argument)


Python の関数は、値渡しです。 実引数 argument から仮引数 parameter という箱に identity という値を渡します。

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

仮引数 (parameter) は関数定義に表れる名前で定義されるのに対し、 実引数 (argument) は関数を呼び出すときに実際に渡す値のことです。仮引数は関数が受け取ることの出来る実引数の型を定義します。
実引数と仮引数の違いは何ですか?

値渡し(あたいわたし、call by value)は右辺値を渡す方法で、実引数として変数を渡したとしても、その値のみが渡される。もちろん即値や複雑な式を渡すこともでき、式の評価結果が渡される。その仕組みとしては、独立した新たな変数が関数内に用意され、元の値がコピーされる。そのため変数を渡したとしても、元の変数が変更されるという事はない。
Wikipedia > 引数 > 評価戦略


◯ 変数渡し call by variable

f:id:domodomodomo:20180412062833j:plain

variable = 0
del variable


Python の del 文 は、変数渡しです(変数渡しは、関数などに使われる用語なので、この言葉の使い方は正しくないかもしれません。しかし、理解しやすいので、取り上げました。)。del 文は、変数、属性を削除します。del 文は、変数、あるいは属性という箱そのものを渡します。del 文は、変数という箱から identity を取り除きます。

変数渡し(へんすうわたし、call by variable)は、変数そのもの(左辺値)を渡す方法で、この場合は仮引数に対する操作がそのまま実引数(渡された変数)に影響する。
Wikipedia > 引数 > 評価戦略


Python の関数が、identity という値に対して操作をしているのに対して、del 文は、変数という箱に対して操作をしています。
Python3 で、del 文が排除されなかったのはなぜですか?
PEP 3105 -- Make print a function


「参照渡し」という言葉があります。これは Wikipedia によると「変数渡し」の実装の仕方の1つらしいです。「参照渡し」とは「変数への参照を渡す」ことを指します。自分は、昔、Python は評価戦略は「参照渡し」なのかと誤解していまいした。なぜ誤解したかというと関数を呼び出したときに、「identity というオブジェクトへの参照を渡している」からです。でも、これは間違いでした。

参照渡し(さんしょうわたし、call by reference)はその実装手段の一つ(と見ることもできる[5])。変数に対する参照(アドレス情報)を渡す方法である(これは言語側が勝手に行う。C言語のように明示的にアドレス演算子を使うものは参照渡しとは呼ばない
Wikipedia > 評価戦略


ちなみに del 文は、オブジェクトを削除しません。オブジェクトの identity を束縛している変数、属性が 0 になった時(参照カウントが 0 になった時)、オブジェクトが削除されます。

注釈: del x は直接 x.__del__() を呼び出しません — 前者は x の参照カウントを 1 つ減らし、後者は x の参照カウントが 0 まで落ちたときのみ呼び出されます。
object.__del__(self)