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

連鎖演算には注意しよう

>>> True is False == False
False
>>> False is False is False
True
>>> 1 > 0 < 1
True
>>> (1 > 0) < 1
False
>>> 1 > (0 < 1)
False

解説

https://docs.python.org/2/reference/expressions.html#not-in より

形式的には、 a, b, c, …, y, z が式で op1, op2, …, opN が比較演算子である場合、 a op1 b op2 c ... y opN za op1 b and b op2 c and ... y opN z と等価になります。ただし、前者では各式は多くても一度しか評価されません。

連鎖演算 (chained operation) を初めて見た人には、上の例はバカバカしく見えるかもしれない。普通はa == b == c0 <= x <= 100のように条件文を連鎖するのに使う。

  • False is False is False(False is False) and (False is False)と同等である。
  • True is False == FalseTrue is False and False == Falseと同等。True is FalseFalse, よって式全体はFalse
  • 1 > 0 < 11 > 0 and 0 < 1よって全体はTrueと評価される。
  • (1 > 0) < 1True < 1であり、
>>> int(True)
1
>>> True + 1 #not relevant for this example, but just for fun
2

なので1 < 1と同等。これはFalseと評価される。

クラススコープを無視した名前解決

1

x = 5
class SomeClass:
    x = 17
    y = (x for i in range(10))

出力

>>> list(SomeClass.y)[0]
5

2

x = 5
class SomeClass:
    x = 17
    y = [x for i in range(10)]

出力(Python 2.x)

>>> SomeClass.y[0]
17

出力(Python 3.x)

>>> SomeClass.y[0]
5

解説

  • クラス定義の内側でネストしたスコープは、クラスレベルにおける名前束縛を無視する。
  • ジェネレータ式はそれ自身でスコープを持つ。
  • Python 3.xではリスト内包もスコープを持つ。

一度の操作でNoneもなくなる

some_list = [1, 2, 3]
some_dict = {
  "key_1": 1,
  "key_2": 2,
  "key_3": 3
}

some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})

出力

>>> print(some_list)
None
>>> print(some_dict)
None

解説

シーケンスの要素を変更するメソッド(list.append, dict.update, list.sortなど)はオブジェクトをインプレースに変更し、メソッド自体はNoneを返す。これは無駄なコピーを作らないため、つまりパフォーマンスのためである(参照)。

文字列の明示的な型キャスト

これもWTFではないが、原著者がPythonを始めてから気づくまでけっこう時間がかかったとのこと。

a = float('inf')
b = float('nan')
c = float('-iNf')  #These strings are case-insensitive
d = float('nan')

出力

>>> a
inf
>>> b
nan
>>> c
-inf
>>> float('some_other_string')
ValueError: could not convert string to float: some_other_string
>>> a == -c #inf==inf
True
>>> None == None # None==None
True
>>> b == d #but nan!=nan
False
>>> 50/a
0.0
>>> a/a
nan
>>> 23 + b
nan

解説

infnanは特別な文字列である(大文字小文字を区別しない)。float型に明示的型キャストを行うと、それぞれ'無限大'と'非数'を表現する値になる。

クラス属性とインスタンス属性

class A:
    x = 1

class B(A):
    pass

class C(A):
    pass

出力

>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)

2

class SomeClass:
    some_var = 15
    some_list = [5]
    another_list = [5]
    def __init__(self, x):
        self.some_var = x + 1
        self.some_list = self.some_list + [x]
        self.another_list += [x]

出力

>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True

解説

  • クラス変数とクラスインスタンス中の変数は、内部的にはクラスオブジェクトの辞書として扱われている。もし変数名がそのクラスで見つからなければ、親クラスの変数が探索される。
  • +=演算子は、可変オブジェクトを新しいオブジェクトを生成することなく、インプレースで変更する。ゆえに、あるインスタンスで変更された属性は、他のインスタンスやクラス属性に影響を及ぼす。

例外をキャッチせよ

some_list = [1, 2, 3]
try:
    # This should raise an ``IndexError``
    print(some_list[4])
except IndexError, ValueError:
    print("Caught!")

try:
    # This should raise a ``ValueError``
    some_list.remove(4)
except IndexError, ValueError:
    print("Caught again!")

出力 (Python 2.x)

Caught!

ValueError: list.remove(x): x not in list

出力 (Python 3.x)

  File "<input>", line 3
    except IndexError, ValueError:
                     ^
SyntaxError: invalid syntax

解説

  • except節に複数の例外を付するためには、最初の引数をカッコでかこむ必要がある。第2引数は任意の名前で、例外インスタンスがその名前に束縛される。例:
some_list = [1, 2, 3]
try:
   # This should raise a ``ValueError``
   some_list.remove(4)
except (IndexError, ValueError), e:
   print("Caught again!")
   print(e)

出力 (Python 2.x)

Caught again!
list.remove(x): x not in list

出力 (Python 3.x)

  File "<input>", line 4
    except (IndexError, ValueError), e:
                                     ^
IndentationError: unindent does not match any outer indentation level
  • 上のエラーから分かるように、カンマで例外とそれを束縛する変数を区切る記法はPython 3では廃止された。正しい書き方はasを使う方法である。
some_list = [1, 2, 3]
try:
    some_list.remove(4)

except (IndexError, ValueError) as e:
    print("Caught again!")
    print(e)

出力

Caught again!
list.remove(x): x not in list

今日はここまで。