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







各オブジェクトは identity と呼ばれる番号を1つずつ持っています。 Python は内部で処理を実行するときに identity を使ってオブジェクトを識別しています。 「変数」や「属性」とは、 identity を保存する箱です。 「代入」とは、 identity を箱にいれることです。




















f:id:domodomodomo:20171106122644j:plain










なんでこんなこと勉強しないといけないの?

こんなに長い文章を読んで「代入」について理解して、一体なんのためになるのでしょうか? Python の「代入」は、一番最初に習うのに、実は意外と混みいった概念です。

実は、これを知らないと、思わぬところででつまずいてしまいます。 これらの具体例については、あとで実際に触れていきますので、てきとーに読み流しておいてください。

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

どうやってお話を進めるの?

目次

1 章identity ってなに?
2 章代入コピーの違い
3 章変数への代入属性への代入の違い
4 章束縛ってなに?


1 章では、identity とはなにかについて触れていきます。ちょっと厄介ですが、「変数」と「代入」を理解するにはどうしても必要です。 identity について、もう少し詳しいスライドを作成しました。ざっくりイメージだけ掴んでもらえればと思います。

2 章では、代入してもオブジェクトは、コピーされないことを、実際にコードを触って理解していきます。 3 章では、変数に代入してもオブジェクトは、変化しませんが、属性に代入するとオブジェクトが変化することを、実際にコードを触って理解していきます。

4 章では、1 章, 2 章, 3 章を踏まえて、Python の「代入」の上位概念である「束縛」について説明していきます。

Python 属性」で検索して来られた方へ

この記事では、ここから先は identity を保存する箱としての「属性」について説明させていただきます。

属性には2種類あります。1つは「クラス変数」で、もう1つは「インスタンス変数」です。 属性なのに2つとも変数っていう名前がついていてややっこしいんですよね笑

この2種類の属性の違いについては、こちらの記事で紹介させていてだきました。
Python のクラス変数とインスタンス変数ってなに?


1. identity ってなに?

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


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


◯ 解答

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

>>> 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
>>> 
f:id:domodomodomo:20181211202312j:plain


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

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


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. 変数への代入とオブジェクトの属性への代入は違う。

基本的な考え方は次のような感じです。


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


① 変数への代入

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


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

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


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

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



3.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 = ''
変数への代入はオブジェクトを変化させません。

なぜなら、このように書いているときに...

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


ひたすら変数 element を書き換えてるだけだからです。 変数 element が格納している identity を変えています。

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



2. list3[index] = ''
シーケンスの要素への代入はオブジェクトを変化させます。

このように書いているときに...

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


リストオブジェクト list3 の index 番目に束縛されている identity を別の identity に書き変えています。

list3 = ['y', 'a', 'r', 'u', 'o']
list3[0] = 'y'
list3[1] = 'a'
list3[2] = 'r'
list3[3] = 'u'
list3[4] = 'o'



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

なぜならこのように書いているときに...

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


実際には変数 name に代入しているだけだから。 変数 name に代入しても変数 girl_friend に代入されたオブジェクトの属性 girl_friend.name は変化しません。

# def change_name(name):
name = '岩倉玲音'
print(name)
print(id(name))



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

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

4. 束縛ってなに?

式が評価されて identity を変数, 属性などの箱にいれることを、「束縛」と言います。

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


Python では代入するときに、束縛を行なっています。 束縛が行われるのは、代入の時だけではありません。 これまで見てきた通り、 関数を実行するときや for 文を呼び出したりしたとき(for ~ in ... の ... に記載された変数)にも同じように、 束縛が行われていました。 そうです、実は私たちは「束縛」について考えていたのです。

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

◯ 評価戦略

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

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


1. 値渡し 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. 関数を定義する - Python チュートリアル

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

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


"オブジェクトの値そのものではありません" とは、なんでしょうか?オブジェクトの値そのものについては、こちらで解説しました。
Python における値ってなに? - いっきに Python に詳しくなるサイト


2. 変数渡し call by variable

f:id:domodomodomo:20180412062833j:plain

variable = 0
del variable


Python の del 文 は、変数渡しです(変数渡しは、関数などの式に対する用語なので、文に対して使うのは、正しくないかもしれません。)。del 文は、オブジェクトではなく変数、属性という箱を削除します。del 文は、変数、あるいは属性という箱そのものを渡します。

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


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

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

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

3. よくある誤解: Python は「参照渡し」である。

「参照渡し」という言葉があります。これは Wikipedia によると「変数渡し」の実装の仕方の1つらしいです。「参照渡し」とは、「オブジェクトへの参照」ではなく「変数(箱)への参照」を渡すことを指します。

自分は、昔、Python は評価戦略は「参照渡し」なのかと誤解していまいした。なぜ誤解したかというと関数を呼び出したときに、「identity というオブジェクトへの参照を渡している」からです。でも、これは間違いでした。

繰り返しになりますが Python は「値渡し」であり、「参照渡し」ではありません。

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


プログラミング言語における「式」と「文」

束縛とは「式」が評価されて結果として返されたオブジェクトへの identity が変数や属性に対応づけられることを言います。 「式」の反対は「文」です。この2つを区別します。

1. 式

結果を、変数や属性に代入することができます。 プログラミング言語における式とは 1 + 1 のような算術式だけではなく、変数 v, 属性参照obj.attr, 関数 f(x) などが該当します。

1 + 1
v
obj.attr
f(x, y)
# 変数 = 式 と書ける。
a = 1 + 1
b = v
c = obj.attr
d = f(x, y)

6. 式 (expression) - Python 言語リファレンス
式 (プログラミング) - Wikipedia

2. 文

結果を、変数や属性に代入ができません。 プログラミング言語における文とは、del, return, for, if 文などが該当します。

del v
if True: print('Hello, World')
# 変数 = 文 とは書けない。 SyntaxError
a = del v
b = if True: print('Hello, World')


文は単純文と複合文に分けられます。1行でかける del, return は単純文、2行以上になる for, if 文は複合文に分類されます。
7. 単純文 (simple statement) - Python 言語リファレンス
8. 複合文 (compound statement) - Python 言語リファレンス
文 (プログラミング) - Wikipedia

3.「式」と「文」の使い分け

基本的に Python では関数では実現できないものだけを文にしています。

del 文

これまで見てきた通り del は変数や属性という名前そのものに対する操作です。 関数ではこれを実現できないため文として定義されています。

print 文

Python 2 の頃、print は値を返さない文でした。 しかし Python 3 では print は文である必要性はないので関数に戻されました。
PEP 3105 -- Make print a function

assert 文

del のほかにも文として定義されているものに assert があります。 なぜ、assert も文として定義されているかについて簡単に説明しています。
Python の assert 文でテストする。 - いっきに Python に詳しくなるサイト

まとめ

変数、属性とは...



identity を
保存する箱です(`・ω・´)キリッ










代入とは...



変数または属性に identity を
束縛することです(`・ω・´)キリッ