変数に代入するってなに?











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

はじめに

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

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





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

(´・ω・`)
変数に代入してもオブジェクトをコピーできない

(´;ω;`)ブワッ
リストを別の変数に代入して pop したら
代入元のリストも pop された

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

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

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

目次

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

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

2 章では、代入してもオブジェクトは、コピーされないことを、 実際にコードを触って習熟していきます。

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

4 章では、1 章, 2 章, 3 章を踏まえて、  Python の「代入」の上位概念である「束縛」へと理解を昇華させていきます 

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

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

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

この2種類の属性の違いについては、こちらの記事で紹介させていてだきました。

1. identity ってなに?

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

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

# 都度 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 には、それぞれ別々のリストオブジェクトを代入しました。

3. 変数と属性

変数への代入とオブジェクトの属性への代入は違います。

3.1. ポイント

以下のような感じです。

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

カッコつけてシーケンス、マッピングと書いてしまいました。 シーケンスとはリスト list のこと、マッピングとは辞書 dict のことです。 シーケンスとマッピングについては、 このだいぶ先の記事である イテラブルってなに? の中で、 ご説明させていただきます。

3.1.1. 変数への代入

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

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

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

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

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

3.1.4. マッピングの要素への代入

# マッピングの要素 = オブジェクト
# マッピング[ハッシャブル] = オブジェクト
# mapping[ n ] = 1
mapping[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 ... の ... に記載された変数)にも同じように、 束縛が行われていました。

そうです、実は私たちは「束縛」について考えていたのです。 束縛は具体的には以下のような場所で起こっています。

  1. 代入文
  2. import 文
  3. クラス定義文の名前
  4. 関数定義文の名前
  5. 関数定義文の引数
  6. for 文の in
  7. with 文の as
  8. try 文の except

上記のうち 1. 代入文, 5. 関数定義文の引数, 6. for 文の in は、見てきました。 2. import 文は触れていませんが、そこまで難しそうでもありません。

import math
math  # <- 変数 math にモジュールオブジェクトが束縛される。

3. クラス定義文の名前、4. 関数定義文の名前というのは、以下のような具合です。

class C:
    pass

def f():
    pass

C  # <- 変数 C にクラスオブジェクトが束縛される。
f  # <- 変数 f に関数オブジェクトが束縛される。

>>> C  # <- 変数 C にクラスオブジェクトが束縛される。
<class '__main__.C'>
>>> f  # <- 変数 f にクラスオブジェクトが束縛される。
<function f at 0x1099d5598>
>>> 

7, 8 は、今回は割愛させてください。

◯ 公式マニュアル

以下、公式マニュアルを引用します。 適当に流してください。

4.2.1. 名前の束縛 - Python 言語リファレンス

名前 (name) は、オブジェクトを参照します。 名前を導入するには、 名前への束縛 (name binding) 操作を行います。
Names refer to objects. Names are introduced by name binding operations.

以下の構造は名前を束縛します:
The following constructs bind names:

関数への 仮引数,
formal parameters to functions,

あるいは import 文,
import statements,

あるいはクラス定義文と関数定義文 (これらは定義しているブロックの中でクラス名または関数名を束縛します)、
class and function definitions (these bind the class or function name in the defining block),

あるいは次の場所で記述される 識別子 である ターゲット
and targets that are identifiers if occurring

代入文か、
in an assignment,

for ループの最初の部分か,
for loop header,

もしくは with 文の as ないし except 節の後ろで。
or after as in a with statement or except clause.

変数と定数を合わせて 名前 と呼びます。 定数とは変更できない変数のことです。 None, True, False などが該当します。 名前については、以下の記事で、書かせていただきました。

仮引数 というのは関数定義文で書かれた引数。 対して実際に呼び出されるときに書かれる引数を 実引数 と呼びます。

def f(x):  # <- x は仮引数
    return 2 * x

f(3)  # <- 3 は実引数

上の文章で、 識別子, ターゲット という言葉が難しいのですが...

あるいは以下の場所で記述される 識別子 である ターゲット。 and targets that are identifiers if occurring

この辺りの文章とすこし、睨めっこが必要になります。 target ターゲットと identifier 識別子という 文字が見えますでしょうか?

target          ::=  identifier
                    | "(" [target_list] ")"
                    | "[" [target_list] "]"
                    | attributeref
                    | subscription
                    | slicing
                    | "*" target<Paste>

これは EBNF と呼ばれる記法です。 Python の文法は、この EBNF というもので定義されています。 EBNF については、 いつか解説したいなと思っています。 いまはどうかご容赦ください。

◯ 評価戦略

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

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

1. 値渡し call by value

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

"オブジェクトの値そのものではありません" とは、 なんでしょうか?オブジェクトの値そのものについては、 この先の以下の記事でご説明させていただきたいと考えております。

2. 変数渡し call by variable

variable = 0
del variable

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

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

Python の関数が、identity という値に対して操作をしているのに対して、 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)

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 文は複合文に分類されます。

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

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

del 文

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

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

assert 文

del のほかにも文として定義されているものに assert があります。 なぜ、assert も文として定義されているかについて簡単に説明させていただきました。

5. まとめ

変数、属性とは...



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










代入とは...



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










ここまで以下のように見てきました。




ありがとうございました。 identity とは何かを理解する4ステップのうちの1つ「代入」が終わりました。

次は immutable と mutable についてご紹介させていただきます。 mutable とは属性に束縛された identity を変更できる オブジェクトのことを指しています。例えば list が mutable です。

immutable とは属性に束縛された identity を変更できない オブジェクトのことを指しています。例えば tuple が immutable です。

imutable属性に直接束縛されている
identity を変更できない。
mutable属性に直接束縛されている
identity を変更できる。

identity を変更できるか、できないかという、ただそれだけの話なのですが、 地味に重要だったりします。

自分が作業を立ち会っている隣で、これに関連した作業ミスによる大規模障害が起こりました。 次の次の記事でご紹介させていただきます。







Last Updated: 8/4/2019, 2:03:18 PM