1. ホーム
  2. python

[解決済み] リスト内包は、内包のスコープを越えても、名前を再束縛します。これは正しいのか?

2022-07-26 02:48:07

質問

Comprehensions は、スコープと予期しない相互作用をしています。これは期待された動作なのでしょうか?

メソッドを持っている

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

愚痴をこぼすかもしれませんが、これは残酷なエラーの元凶です。新しいコードを書いていると、たまに再バインドのために非常に奇妙なエラーを見つけることがあります -- それが問題だとわかっている今でも。リスト内包の temp vars の前には必ずアンダースコアを付ける、みたいなルールを作らないといけないのですが、それでも確実ではありません。

このランダムな時限爆弾が待っているという事実は、リスト内包の素晴らしい "使いやすさ"をすべて否定するようなものです。

どのように解決するのですか?

リスト内包は、Python 2ではループ制御変数をリークするが、Python 3ではリークしない。 Guido van Rossum (Pythonの作者)は次のように述べています。 説明 で説明しています。

私たちはまた、Python 3 では、リスト とジェネレータ 式の等価性を向上させるためです。Python 2 では、リスト を周囲のスコープにリークします。 変数を周囲のスコープにリークします。

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

これはオリジナルの リスト内包の実装の成果です。 これはPythonの汚い秘密の1つです。 汚い秘密" の一つでした。これは を高速にするための意図的な妥協として始まりました。 これはリスト内包を目も眩むほど速くするための意図的な妥協として始まりました。 初心者にありがちな落とし穴ではありませんでしたが 初心者にありがちな落とし穴ではありませんでしたが、間違いなく人を刺す をたまに見かけます。ジェネレータ 式では、このようなことはできません。 ジェネレータ式は、ジェネレータを使用して実装されます。 ジェネレータ式はジェネレータを使用して実装され、その実行には は別の実行フレームを必要とします。 そのため、ジェネレータ式は は(特に短いシーケンスを反復する場合 特に短いシーケンスを反復する場合は)リスト内包よりも効率が リスト内包よりも効率が悪かったのです。

しかし、Python 3で、私たちは リスト内包の汚い小さな秘密("dirty little secret")を修正することにしました。 と同じ実装戦略を用いて、リスト内包の と同じ実装戦略で ジェネレータ式と同じ実装戦略を使うことにしました。したがって、Python 3では、上の例(print(x) :-)は print(x)を使うように修正したもの) は は '前' と表示され、リスト内包の 'x' が一時的に '前' となったことが証明されます。 はリスト内包で一時的に は一時的に影を落としますが、周囲のスコープの 'x' を上書きするわけではありません。 を上書きしないことを証明しています。