web-dev-qa-db-ja.com

なぜ浮動小数点辞書キーが同じ値を持つ整数キーを上書きできるのですか?

私は http://www.mypythonquiz.com を使用していますが、 question#45 は次のコードの出力を要求しています:

confusion = {}
confusion[1] = 1
confusion['1'] = 2
confusion[1.0] = 4

sum = 0
for k in confusion:
    sum += confusion[k]

print sum

キー61.0を置き換えるため、出力は1です。これは私にとって少し危険だと感じていますが、これは便利な言語機能ですか?

52
sjdenny

dictは、データの表現方法ではなく、論理数値に応じてデータを保存することを目的としていることを考慮する必要があります。

intsとfloatsの違いは、実際には単なる実装の詳細であり、概念的なものではありません。理想的には、唯一の数値型は、サブユニティでさえ無限の精度を持つ任意の精度の数値でなければなりません...これはトラブルを起こすことなく実装するのが難しいです...しかし、それはPythonの唯一の将来の数値型になるかもしれません。

技術的な理由で異なるタイプを使用している間、Pythonはこれらの実装の詳細を隠そうとし、int-> float変換は自動的に行われます。

Pythonプログラムif x == 1: ...は、xが値1のfloatである場合には取得されませんでした。

また、Python 3の値1/20.5(2つの整数の除算)およびlong型と非Unicode文字列は、実装の詳細を隠そうと同じ試みで削除されました。

17
6502

まず第一に:動作は hash 関数のドキュメントに明示的に文書化されています:

hash(object)

オブジェクトのハッシュ値(ある場合)を返します。ハッシュ値は整数です。辞書検索時に辞書キーをすばやく比較するために使用されます。 等しいと比較される数値は、ハッシュ値が同じです(11.0の場合のように、異なるタイプであっても)。

第二に、ハッシュの制限が object.__hash__ のドキュメントで指摘されています

object.__hash__(self)

組み込み関数 hash() によって呼び出され、setfrozensetdict. __hash__()などのハッシュされたコレクションのメンバーに対する操作に対して整数を返します。 唯一の必須プロパティは、等しいと比較するオブジェクトが同じハッシュ値を持つことです。

これはpythonに固有のものではありません。 Javaには同じ警告があります:hashCodeを実装する場合、物事が正しく機能するためには、mustx.equals(y)x.hashCode() == y.hashCode()を意味します。

したがって、pythonは1.0 == 1が成立することを決定したため、hash(1.0) == hash(1)のようなhashの実装を提供することはforcedです。副作用は1.01は、dictキーとまったく同じように動作するため、動作します。

言い換えれば、動作自体を使用したり、何らかの方法で有用にする必要はありません。 必要です。その動作がなければ、誤って別のキーを上書きしてしまう可能性があります。

1.0 == 1があったがhash(1.0) != hash(1)があった場合でも、collisionが発生する可能性があります。また、1.01が衝突した場合、dictは同じキーであるかどうかを確認するために平等を使用し、kaboomを使用したとしても値が上書きされます異なる。

これを回避する唯一の方法は、1.0 != 1を使用することです。これにより、dictが衝突の場合でもそれらを区別できるようになります。ただし、実際にはfloatsとintsを辞書キーとして使用することは決してないため、1.0 == 1を使用することは、表示されている動作を回避することよりも重要であると見なされました。

pythonは、必要に応じて番号を自動的に変換することで数値の区別を隠そうとするため(例:1/2 -> 0.5))このような状況でもこの動作が反映されることは理にかなっています。


この動作は、anyの実装で見られます。この実装では、キーの一致が比較に基づいて少なくとも部分的に(ハッシュマップのように)行われます。

たとえば、dictが赤黒ツリーまたは他の種類のバランスの取れたBSTを使用して実装されている場合、キー1.0が検索されると、他のキーとの比較は1と同じ結果を返します。それでも同じように行動します。

ハッシュマップは、キーのエントリを見つけるために使用されるのはハッシュの値であり、比較はその後のみ行われるため、さらに注意が必要です。したがって、上記の規則に違反すると、dictが期待どおりに動作するように見える場合や、サイズが変更された場合に、見つけるのが非常に難しいバグを導入することになります間違った動作を開始します。


これを修正する方法はであることに注意してください。辞書に挿入されたタイプごとに個別のハッシュマップ/ BSTを用意してください。この方法では、異なるタイプのオブジェクト間で衝突が発生することはありません。また、引数が異なるタイプである場合、==の比較は重要ではありません。

ただし、これにより実装が複雑になりますが、アクセス時間をO(1))にするためにハッシュマップはかなりの数の空き場所を保持する必要があるため、おそらく非効率的です。複数のハッシュマップを作成することは、より多くのスペースを浪費することを意味し、キーの実際のルックアップを開始する前に、最初にどのハッシュマップを調べるかを選択する必要があります。

BSTを使用した場合、最初にタイプを検索し、2回目の検索を実行する必要があります。したがって、多くのタイプを使用する場合は、作業が2倍になります(そして、ルックアップはO(1)ではなくO(log n)を取ります)。

97
Bakuriu

Pythonの場合:

1==1.0
True

これは暗黙的なキャストによるものです

しかしながら:

1 is 1.0
False

floatint間の自動キャストが便利な理由がわかります。intfloatにキャストするのは比較的安全ですが、他の言語(例:暗黙のキャストを避けます。

それは実際には言語設計の決定であり、さまざまな機能よりも好みの問題です

7
Uri Goren

辞書はハッシュテーブルで実装されます。ハッシュテーブルで何かを検索するには、ハッシュ値が示す位置から開始し、等しいキー値または空のバケットが見つかるまで別の場所を検索します。

等しいがハッシュが異なる2つのキー値がある場合、他のキー値が検索された場所にあったかどうかによって、一貫性のない結果が得られる可能性があります。たとえば、テーブルがいっぱいになるとこれが発生する可能性が高くなります。これは避けたいものです。 Python開発者はこれを念頭に置いていたようです。組み込みのhash関数は、それらの値がintfloatかに関係なく、同等の数値に対して同じハッシュを返すためです。他の数値型では、False0に等しく、True1に等しく、fractions.Fractionおよびdecimal.Decimalでもこのプロパティを維持します。

a == bの場合hash(a) == hash(b)object.__hash__() の定義に文書化されているという要件

組み込み関数hash()によって呼び出され、setfrozensetdictなどのハッシュされたコレクションのメンバーに対する操作のために呼び出されます。 __hash__()は整数を返す必要があります。唯一必要なプロパティは、等しいと比較するオブジェクトが同じハッシュ値を持つことです。オブジェクトの比較においても役割を果たすオブジェクトのコンポーネントのハッシュ値を何らかの方法で混合することをお勧めします(たとえば、排他的ORを使用するなど)。

TL; DR:比較したキーが同じ値にマッピングされない場合、辞書は壊れます。

6
Mark Ransom

率直に言って、反対は危険です! 1 == 1.0。したがって、異なるキーをポイントさせ、評価された数値に基づいてそれらにアクセスしようとした場合、あいまいさを理解するのが難しいため、おそらくそれで問題が発生することを想像することはできません。

型は順応性があり(()非常に便利な機能)、したがってintsfloatsdistinctと同じ値の不必要なセマンティクスは、混乱を招くだけです。

3
SuperBiasedMan

私は、このコンテキストで11.0を同じものとして扱うのが理にかなっていることに他の人に同意します。 Pythonがそれらを異なる扱いをしたとしても、辞書の別個のキーとして11.0を使用しようとすることはおそらく悪い考えでしょう。 -キーのコンテキストで1.0のエイリアスとして1を使用するための自然なユースケースを考えるのに問題があります。問題は、キーがリテラルであるか、計算されることです。リテラルキーである場合、なぜ1ではなく1.0を使用しないのですか?計算されたキーである場合、丸め誤差により問題が発生する可能性があります。

>>> d = {}
>>> d[1] = 5
>>> d[1.0]
5
>>> x = sum(0.01 for i in range(100)) #conceptually this is 1.0
>>> d[x]
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d[x]
KeyError: 1.0000000000000007

だから、一般的に言えば、あなたの質問への答えは「これはこれまで便利な言語機能ですか?」 「いいえ、おそらくない」です。

3
John Coleman