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

is not ...is (not ...)とは異なる

>>> 'something' is not None
True
>>> 'something' is (not None)
False

解説

  • is notで単一の二項演算子である。isnotを別々に使うのとは異なる動作をしていることに注意。
  • is notは両側の変数が同じオブジェクトを参照しているときにFalseを返し、そうでないときはTrueとなる。

ループ中で定義した関数が、同じ出力しかしない

funcs = []
results = []
for x in range(7):
    def some_func():
        return x
    funcs.append(some_func)
    results.append(some_func())

funcs_results = [func() for func in funcs]
>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

some_funcfuncsに追加する時点ではxの値がすべて異なるのだが、すべての関数は6を返す。

あるいは、

>>> powers_of_x = [lambda x: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

解説

  • ループ中で定義された関数は、その本体ではループ変数を用いる。ループ関数のクロージャは変数(x)に束縛され、値には束縛されない。よって、すべての関数は変数に割り当てられた最後の値(6)を利用する。
  • 最初のような望ましい挙動を得るためには、ループ変数を名前付き変数(オプション引数)として関数に渡してやるとよい。ここで考えて欲しい、なぜこれでうまくいくのか?
funcs = []
for x in range(7):
    def some_func(x=x):
        return x
    funcs.append(some_func)

出力

>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]

理由は、関数のスコープ内部で変数を定義し直しているからである。

ループ変数がローカルスコープ外に漏れる!

1.

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

出力

6 : for x inside loop
6 : x in global

xをforループの外では一度も定義していないのに…

2.

# This time let's initialize x first
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

出力

6 : for x inside loop
6 : x in global

3.

x = 1
print([x for x in range(5)])
print(x, ': x in global')

出力(Python 2.x)

[0, 1, 2, 3, 4]
4 : x in global

出力(Python 3.x)

[0, 1, 2, 3, 4]
1 : x in global

解説

  • Pythonのforループは新たにスコープを作らない。定義済みのループ変数はそのまま残る。2番目の例が示しているように、これは既存の変数をも再束縛する。
  • リスト内包はPython 2.xとPython 3.xとで扱いが異なる。What’s New In Python 3.0を引く。

リスト内包表記はもう [... for var in item1, item2, ...] という構文形をサポートしません。代わりに [... for var in (item1, item2, ...)] を使用してください。また、リスト内包表記は異なるセマンティクスを持つことに注意してください。リスト内包表記は list() コンストラクタ内のジェネレータ式の糖衣構文に近く、特にループの制御変数はスコープ外ではもう使用することができません。

○×ゲーム、初手でいきなり勝利

# Let's initialize a row
row = [""]*3 #row i['', '', '']
# Let's make a board
board = [row]*3

出力

>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

3つも'X'を代入してないんだけど、どういうことなのさ?

(これも有名だと思う)

解説

row["", "", ""]というデータを指し示している。

[row] * 3boardを初期化すると、board[0], board[1], board[2]は同じrowを参照する。

デフォルト可変引数にご用心

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

出力

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

解説

関数のデフォルト可変引数は関数呼び出しごとに初期化されず、最新の割り当てられた値をデフォルト引数として用いる。some_funcに明示的に[]を渡すことにより、default_argが使われることを避けることができる。

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

出力

>>> some_func.__defaults__ #This will show the default argument values for the function
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)

可変引数にまつわるバグを避ける一般的な方法として、まずNoneをデフォルト値として割り当て、あとでその引数に値が渡されているかチェックするというものがある。

def some_func(default_arg=None):
    if not default_arg:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

今日はここまで。