Pythonのどうしてこうなるの?トリッキーコード集 (part 2)

文字列

>>> print("\\ some string \\")
>>> print(r"\ some string")
>>> print(r"\ some string \")

出力(3行目)

    File "<stdin>", line 1
      print(r"\ some string \")
                             ^
SyntaxError: EOL while scanning string literal

解説

  • まずrを接頭辞とするraw文字列リテラルでは、バックスラッシュは特殊な意味を持たない。どうしてこうなるのか?
  • インタプリタが実際にやっていることは、単純にバックスラッシュの挙動を変更している。("so they pass themselves and the following character through.“)ここ何をいってるのかよくわからなかった。ここを参考にすると、raw文字列は有効な文字列リテラルである必要があるから、バックスラッシュで終われないということだと思う。

でっかい文字列を作ろう!

これはなんじゃこりゃ!という感じのコードではないのだが、気をつけておくといいことなので紹介するよ(o^^o)

def add_string_with_plus(iters):
    s = ""
    for i in range(iters):
        s += "xyz"
    assert len(s) == 3*iters

def add_string_with_format(iters):
    fs = "{}"*iters
    s = fs.format(*(["xyz"]*iters))
    assert len(s) == 3*iters

def add_string_with_join(iters):
    l = []
    for i in range(iters):
        l.append("xyz")
    s = "".join(l)
    assert len(s) == 3*iters

def convert_list_to_string(l, iters):
    s = "".join(l)
    assert len(s) == 3*iters

出力

>>> timeit(add_string_with_plus(10000))
100 loops, best of 3: 9.73 ms per loop
>>> timeit(add_string_with_format(10000))
100 loops, best of 3: 5.47 ms per loop
>>> timeit(add_string_with_join(10000))
100 loops, best of 3: 10.1 ms per loop
>>> l = ["xyz"]*10000
>>> timeit(convert_list_to_string(l, 10000))
10000 loops, best of 3: 75.3 µs per loop

解説

  • timeitについてはここを参照コードスニペットの実行時間を測るのに使われる。
  • Pythonでは+を長い文字列を生成するのに使ってはいけない。strは不変であり、+の右左の文字列は常にコピーされる。長さ10の文字列を結合する操作を繰り返すとき、(10+10) + ((10+10)+10) + (((10+10)+10) + (10+10)+10) = 90文字のコピーが生じる。これはパフォーマンス上、文字列長さnに対しO(n^2)の悪影響を与える。
  • よって、文字列の結合には.formatまたは%を使うべきである*1(ただし短い文字列に対しては少しだけ+より遅くなる)。
  • すでに反復可能な状態としてデータを持っている場合は、''.join(iterable_object)が非常に高速である。

文字列連結のインタプリタによる最適化

>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string") # Notice that both the ids are same.
140420665652016
# using "+", three strings:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# using "+=", three strings:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281

解説

  • +=+より高速である。これは最初の文字列を破棄しないため(例ではs1)。
  • CPythonはいくつかのケースに対し、現存する不変オブジェクトを再利用しようとする。これがa = "some_string""some" + "_" + "string"idが等しくなった理由である。詳しくはここを参照。

こんなところにelse節?

forループに対するelse節。

def does_exists_num(l, to_find):
    for num in l:
        if num == to_find:
            print("Exists!")
            break
    else:
        print("Does not exist")

出力

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist

例外ハンドリングに対するelse節。

try:
    pass
except:
    print("Exception occurred!!!")
else:
    print("Try block executed successfully...")

出力

Try block executed successfully...

解説

  • forループ中でbreakが起こらなかったときelse節が実行される。
  • tryブロックの後にくるelse節は完了節(completion clause)とも呼ばれる。その名の通り、tryブロックが成功裏に完了したとき実行される。

isの不思議な挙動

(インターネットでは) かなり有名な挙動である。

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

解説

is==の違い

  • isオペランドが同じオブジェクトを参照しているかを調べる。
  • ==は値が同じかどうか調べている。
  • 2つの違いが明確な例を挙げる。
>>> [] == []
True
>>> [] is [] # These are two empty lists at two different memory locations.
False

256はすでに存在するオブジェクトであり、257はそうではない

実は、-5から256までの数値は、Pythonを起動した時点で割り当てされている。これらの数は非常によく使われるためである。

https://docs.python.org/ja/3/c-api/long.html から引用する。

現在の実装では、-5 から 256 までの全ての整数に対する整数オブジェクトの配列を保持するようにしており、この範囲の数を生成すると、実際には既存のオブジェクトに対する参照が返るようになっています。従って、1 の値を変えることすら可能です。変えてしまった場合の Python の挙動は未定義です :-)

>>> id(256)
10922528
>>> a = 256
>>> b = 256
>>> id(a)
10922528
>>> id(b)
10922528
>>> id(257)
140084850247312
>>> x = 257
>>> y = 257
>>> id(x)
140084850247440
>>> id(y)
140084850247344

上のコードはインタプリタy = 257を割り当てる際、あまり賢くないことを示している(x = 257が存在するのに、再利用せず新しいオブジェクトを生成している)。

しかし、同じ値を同じ行で初期化するときはabは同じオブジェクトになる

>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296
>>> a = 257
>>> b = 257
>>> id(a)
140640774013392
>>> id(b)
140640774013488
  • 同じ行で同じ値(257)を割り当てると、Pythonはオブジェクトを再利用する。
  • 行を分離すると257がすでに存在しているか分からない(REPL限定の話で対話環境は対話行ごとにコンパイルを行うから)。
  • .pyファイル中で書いたプログラムはREPLと同じ挙動になるとは限らない。ファイルは一度にコンパイルされるため。

今日はここまで。

*1:f-stringも