Python

エイリアシングによるオブジェクトの参照

Pythonプログラミングイントロダクション5章まで読みました。

5章をみていきます。この章ではPythonの構造型と呼ばれるtuple ,list, str, rangeについてみていきながら、可変性と不変性の違いが説明されています。 また、関数を引数として渡すような高階関数についても説明されています。

特にlist型のエイリアシングについて多くの説明がこの章では割かれています。確かにプログラミングでよくミスる部分ではありますね。

エイリアシング

下記のように変数aの要素を直接変更したり、変数bに対して副作用のあるappend関数を用いて値を変更すると、cの値も変わってしまいます。

1
2
3
4
5
6
7
8
a = [1]
b = [2]
c = [a , b]
print('c : ' , c)

a[0] = 0
b.append(3)
print('c : ' , c)
>> c :  [[1], [2]]
>> c :  [[0], [2, 3]]

上記の場合a,b が参照しているリストと、a,b を構成要素として生成されたcが参照しているリストが同一のものになっています。 a,bを通して参照したリストとcを通して参照しているリストは、別の経路から同じリストを参照している状態になっています。

c = [a[:] , b[:]] のようにスライスを使うと、リストは複製されるため変更してもcの値に影響しません。 a,bが参照しているリストと同じ値のリストを複製しますが、参照しているリストは同じものではなく、全く別のリストを参照している状態になります。

1
2
3
4
5
6
7
8
a = [1]
b = [2]
c = [a[:] , b[:]]
print('c : ' , c)

a[0] = 0
b.append(3)
print('c : ' , c)
>> c :  [[1], [2]]
>> c :  [[1], [2]]

Pythonではid() 関数により、オブジェクトが同じものを参照しているかを確認することができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
a = [1]
b = a

print(id(a) == id(b))

a.append(2)

print(id(a) == id(b))

b = a[:]

print(id(a) == id(b))
>> True
>> True
>> False

要素を直接変更した場合や、append()関数を使った場合はid()関数の戻り値はは変わらないですが、スライスを使うと複製されるためにid()関数の戻り値が変わります。

まとめ

5章からエイリアシングについて取り上げました。エイリアシングはわかりにくいバグにつながるケースもあります。なのでimmutableなオブジェクトが好まれることもあります。

静的スコープと動的スコープの違い

Pythonプログラミングイントロダクション4章まで読みました。

この章ではプログラムを書く上で避けては通れない関数、スコープ、抽象化についてです。

プログラミングにおいては処理をひたすら書いていくだけではなく、共通の処理は関数として切り出したり、変数のスコープを意識して変数が見える範囲を理解する必要があります。 スコープを理解していないと処理の対象がなんなのか、どこまで参照可能なのかを分からずにプログラミングすることになり、効率が悪くなります。

静的スコープ

Pythonは静的スコープ(レキシカルスコープ)の言語です。本書で出てくるスコープに関連する用語を整理しておきます。

名前空間違う名前空間では同じ名前の変数が異なるものとして扱われる。このように分割して衝突を回避する概念
スコープ変数や関数を参照できる範囲のこと
局所変数ローカル変数ともいう。関数内で利用できる変数のこと。
大域変数グローバル変数ともいう。全てのスコープから利用できる変数のこと

同じ名前の変数 x をプログラム内で使用した場合、それぞれの関数ではどの x が参照されているか見ていきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def hoge():
    def fuga():
        x = 'fuga'
        print('fuga print x: ' + x)

    def moga():
        print('moga print x: ' + x)


    x = 'hoge'
    print('hoge print x: ' + x)
    fuga()
    moga()
    foo()

def foo():
    print('foo print x: ' + x)


x = 'foo'
hoge()

出力は下記のようになります。

: hoge print x: hoge
: fuga print x: fuga
: moga print x: hoge
: foo print x: foo

下記は全て違うものになります。

  • globalで定義された x
  • hoge関数で定義された x
  • fuga関数で定義された x

下記は同じものになります。

  • hoge関数で定義された x とmoga関数で参照された x
  • globalで定義された x とfoo関数で参照された x

このように、関数がどこから呼び出されたかに関係なく、どこに定義されているかで変数のスコープが変わるのが静的スコープになります。 また、Pythonではlocal変数やグローバル変数をlocals()関数やglobals()関数で表示させることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def hoge():
    def fuga():
        x = 'fuga'
        print('fuga locals: ', locals())

    def moga():
        print('moga locals: ', locals())


    x = 'hoge'
    fuga()
    moga()
    foo()

    print('hoge locals: ', locals())

def foo():
    print('foo locals: ', locals())

x = 'foo'
hoge()
print('globals: ', globals())

結果は下記のようになります。同じ変数名 x でfuga, hoge, globalに定義がされていることがわかります。

: fuga locals:  {'x': 'fuga'}
: moga locals:  {}
: foo locals:  {}
: hoge locals:  {'fuga': <function hoge.<locals>.fuga at 0x1014db440>, 'moga': <function hoge.<locals>.moga at 0x1014db4d0>, 'x': 'hoge'}
: globals:  {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '<stdin>', '__cached__': None, 'hoge': <function hoge at 0x1014db320>, 'foo': <function foo at 0x1014db3b0>, 'x': 'foo'}

動的スコープ

静的があれば動的もあります。関数がどこから呼び出されたかで変数のスコープが変わるのが動的スコープになります。 bashで変数にlocalをつけると動的スコープになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
x="hoge"

hoge() {
    echo "in hoge x is $x"
}

foo() {
    local x="foo"
    hoge
}

foo
hoge
in hoge x is foo
in hoge x is hoge

foo関数からhoge関数が呼ばれていますが、hoge関数で参照される x はfoo関数が呼び出す直前に設定された x になっています。

まとめ

本書で説明されている静的スコープと対比して、動的スコープがどうなっているかを bashのlocal変数の例で整理してみました。 本書ではlocals()関数で表示されるシンボルテーブルの取り扱いをわかりやすい図で説明してくれています。是非こちらも参照してみてください。

ニュートンラフソン法ってなんだ?

Pythonプログラミングイントロダクション3章まで読みました。

この章は算術プログラムの章で、近似について学べます。 前回は浮動小数点について書きました。今回はニュートンラフソン法についてです。

ニュートンラフソン法とは

ニュートンと同じ時期にラフソンと言う人も同じ手法を発見したのでニュートンラフソン法と言うそうです。

ある関数f(x)があるとき、f(x) = 0となるxの近似値を求める場合を考えます。このとき推定値をguessとしたとき、 下記の値は元々の推定値よりさらに良い近似値になると言う定理がニュートンラフソン法です。

\[ guess - \frac{p(guess)}{p’(guess)} \]

下記は先日1章を読んだときに出てきた25の平方根の近似を求めたプログラムですが、何気にニュートンラフソン法を使っていました。

1
2
3
4
5
6
7
x = 25
g = 3

while abs(x - g * g) > 1:
    g = (g + x / g) / 2

return g

今回の場合だと下記のf(x)の解を求めれば良いことになります。

\[ f(x) = x^2 - 25 \]

つまりニュートンラフソン法により、より良い近似値を求める式は下記になります。この式は上記Pythonプログラムのwhile文中の記述と同じですね。

\[ guess - \frac{guess^2 - 25}{2 * guess} = \frac{guess + \frac{25}{guess}}{2} \]

ニュートンラフソン法の説明についてはこちらのページが非常にわかりやすかったです。 考え方としてはある関数f(x)の近似値を求める時、最初の推定値の時のf(x)の接線とx軸の交点が、最初の推定値よりもより良い推定値になるという考え方です。 式で考えると下記のようになります。まずは接線の傾きを考えます。接線の傾きは関数を微分したものになります。

\[ guessの時の接線の傾き = f’(guess) \]

この接線とy軸との交点をa、x軸との交点をbとしたとき、

\[ f(guess) = f’(guess)guess + a \]

\[ 0 = f’(guess)b + a \]

上記からbを求めると

\[ f(guess) = f’(guess)guess - f’(guess)b \]

\[ b = \frac{f(guess)guess - f(guess)}{f’(guess)} \]

\[ b = guess - \frac{f(guess)}{f’(guess)} \]

これでニュートンラフソン法の式が導かれました。

まとめ

3章の中からニュートンラフソン法について取り上げました。 式で考えるより図で考えた方が非常に直感的でわかりやすいので、是非参考ページも見てみてください。

浮動小数点の誤差はなぜ発生する?

Pythonプログラミングイントロダクション3章まで読みました。

3章は算術プログラムの章です。近似について学べます。 全順序について述べられていたりする点、さすがアカデミックだなぁと思いつつ読み進めました。

近似に関しては2文法とニュートンラフソン法が示されています。 2分法は名前から想像しやすいですが、2分探索のようにあり得る値の中間値の計算結果の大小から推定値を移動させていく手法です。 ニュートンラフソン法は初耳でした。

また浮動小数点の説明が非常にわかりやすかったです。実際の計算結果からずれることがある、くらいの理解だったのですが、2進数の都合上なぜ、どのようにずれるかが説明してあって納得でした。

\#+END\_HTML

浮動小数点とは

小数を浮動小数点を用いて表現するとは、仮数、基数、指数を用いて表現することになります。 名前の通り小数点の位置が固定ではないことから浮動小数点と言います。

例えば 5/8の場合は、下記のようになります。

\[\frac{5}{8} = 5 * 2^{-3}\]

この場合,仮数は5, 基数は2, 指数は3になります。この場合は誤差は出ないです。そしてコンピュータは2進数で値を表現するので、基数は2となります。

しかし1/10の場合は誤差がどうしても出てしまいます。 下記は1/10を指数部を5,6,7と増やしていった場合の実際の値です。

\[3 * 2^{-5} = 0.9375\]

\[6 * 2^{-6} = 0.9375\]

\[12 * 2^{-7} = 0.9375\]

\[25 * 2^{-8} = 0.9765625\]

このようにいくら指数部の値を増やして行っても0.1にはたどり着けません。 浮動小数点では値によってどうしても誤差が出てしまうわけです。

0.1が実際どのような値となっているかは下記のようにして見ることができます。

1
print( format(0.1, '.55f') )

値は0.1000000000000000055511151231257827021181583404541015625となります。

まとめ

3章の中から特に浮動小数点に関して取り上げました。 金額を計算するときとか、気を付けろぐらいにしか意識していなかった浮動小数点ですが、整理できてよかったです。

スカラ、リテラル

Pythonプログラミングイントロダクション2章まで読みました。 2章はPythonの概要を説明し、変数や分岐、繰り返しについて学べる章です。

この辺りは普段からプログラミングをしていると目新しいことはないですが、 曖昧に理解している用語の再確認には有用です。

オブジェクト・型。スカラ・リテラル

自分の場合はスカラとかリテラルがなんとなくな理解で進んでいました。

名称内容
オブジェクトPythonプログラムが操作する核。全てのオブジェクトは特定の型を持っている
プログラムがオブジェクトに対してどんな処理ができるかを定義する。型にはスカラと非スカラがある
スカラ 例えばint。スカラ・オブジェクトは分割不可能
非スカラ例えば文字列。 分割可能で内部構造を持つ
リテラル2 -数を表すリテラル ‘abc’ -文字列を表すリテラル

スカラと非スカラの違いは分割可能かどうかという点で、確かにStringは分割可能なので、非スカラになります。。 逆にint, float, booleanなどは分割不可能でスカラになります。

1
2
# Stringは分割可能
print('abcde'[2:])

なお、Pythonにはchar型はありません。

リテラルは、ソースコード内に値を直接表記したものをいいます。 例えば ‘2’ は数字、‘abc’は文字列を表すリテラルで、ソースコードに直接書かれた文字列とか数字とかのことです。

まとめ

なんとなく使っているリテラルとかスカラとかの用語を整理できた点で有用でした。