web-dev-qa-db-ja.com

Pythonの辞書キーとしての範囲

そのため、辞書の単一の値のキーとして、ある範囲の数値を使用できるという考えがありました。

以下のコードを書きましたが、動作させることができません。それも可能ですか?

    stealth_roll = randint(1, 20)
    # select from a dictionary of 4 responses using one of four ranges.
    ## not working.
    stealth_check = {
                    range(1, 6) : 'You are about as stealthy as thunderstorm.',
                    range(6, 11) : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                    range(11, 16) : 'You are quiet, and deliberate, but still you smell.',
                    range(16, 20) : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                    }

    print stealth_check[stealth_roll]
23
Cuylar Conly

Python 3 —およびxrangeの代わりにrangeを使用する場合はPython 2で可能です:

stealth_check = {
                xrange(1, 6) : 'You are about as stealthy as thunderstorm.', #...
                }

ただし、あなたがそれを使用しようとしている方法は機能しません。次のように、キーを反復処理できます。

for key in stealth_check:
    if stealth_roll in key:
        print stealth_check[key]
        break

これのパフォーマンスはニース(O(n))ではありませんが、あなたが示したような小さな辞書であれば大丈夫です。実際にそれをしたい場合は、dictをサブクラスにして、そのように自動的に動作させます。

class RangeDict(dict):
    def __getitem__(self, item):
        if type(item) != range: # or xrange in Python 2
            for key in self:
                if item in key:
                    return self[key]
        else:
            return super().__getitem__(item)

stealth_check = RangeDict({range(1,6): 'thunderstorm', range(6,11): 'tip-toe'})
stealth_roll = 8
print(stealth_check[stealth_roll]) # prints 'tip-toe'
15
L3viathan

範囲自体をキーにしない限り、範囲から直接辞書を作成することはできません。私はあなたがそれを望んでいるとは思わない。範囲内の可能性ごとに個別のエントリを取得するには:

stealth_check = dict(
                    [(n, 'You are about as stealthy as thunderstorm.')
                        for n in range(1, 6)] +
                    [(n, 'You tip-toe through the crowd of walkers, while loudly calling them names.')
                        for n in range(6, 11)] +
                    [(n, 'You are quiet, and deliberate, but still you smell.')
                        for n in range(11, 16)] +
                    [(n, 'You move like a ninja, but attracting a handful of walkers was inevitable.')
                        for n in range(16, 20)]
                    )

dictが小さな範囲の整数でインデックス付けされている場合、代わりにlistの使用を検討する必要があります。

stealth_check = [None]
stealth_check[1:6] = (6 - 1) * ['You are about as stealthy as thunderstorm.']
stealth_check[6:11] = (11 - 6) * ['You tip-toe through the crowd of walkers, while loudly calling them names.']
stealth_check[11:16] = (16 - 11) * ['You are quiet, and deliberate, but still you smell.']
stealth_check[16:20] = (20 - 16) * ['You move like a ninja, but attracting a handful of walkers was inevitable.']
7
Mark Ransom

はい、できます。rangeリストを不変のTupleとして変換する場合のみ、ハッシュ可能であり、辞書のキーとして受け入れられます。

_stealth_check = {
                Tuple(range(1, 6)) : 'You are about as stealthy as thunderstorm.',
_

編集:実際には、Python 3 rangeは不変のシーケンス型であり、L3viathanとしてTupleの代わりに不変のlistを生成します]述べました。

ただし、単一の整数をキーとしてそれらにアクセスすることはできません。最後の行は機能しません。

値に関係なく機能するソリューションを作成するのに少し時間をかけました(辞書内の1つのエントリを選択しても、行がより大きな範囲で「重み付け」されていない限り機能します)。

ソートされたキーでbisectを呼び出して挿入ポイントを見つけ、それを少しハッキングし、O(log(N))の複雑さで辞書で最適な値を見つけます。リスト(ここでは少し多すぎるかもしれません:)が、その場合は辞書も多すぎます)

_from random import randint
import bisect

stealth_roll = randint(1, 20)
# select from a dictionary of 4 responses using one of four thresholds.

stealth_check = {
                1 : 'You are about as stealthy as thunderstorm.',
                6 : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                11 : 'You are quiet, and deliberate, but still you smell.',
                16 : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                }

sorted_keys = sorted(stealth_check.keys())


insertion_point = bisect.bisect_left(sorted_keys,stealth_roll)

# adjust, as bisect returns not exactly what we want
if insertion_point==len(sorted_keys) or sorted_keys[insertion_point]!=stealth_roll:
    insertion_point-=1

print(insertion_point,stealth_roll,stealth_check[sorted_keys[insertion_point]])
_

このようなケースを処理するために、より一般的で使いやすいRangeKeyDictクラスを作成しました。使用方法については、__ main__のコードを確認してください

次を使用してインストールします。

pip install range-key-dict

使用法:

from range_key_dict import RangeKeyDict

if __name__ == '__main__':
    range_key_dict = RangeKeyDict({
        (0, 100): 'A',
        (100, 200): 'B',
        (200, 300): 'C',
    })

    # test normal case
    assert range_key_dict[70] == 'A'
    assert range_key_dict[170] == 'B'
    assert range_key_dict[270] == 'C'

    # test case when the number is float
    assert range_key_dict[70.5] == 'A'

    # test case not in the range, with default value
    assert range_key_dict.get(1000, 'D') == 'D'

https://github.com/albertmenglongli/range-key-dict

3
Menglong Li

dictは、このジョブの間違ったツールです。 dictは、特定のキーを特定の値にマッピングするためのものです。それはあなたがしていることではありません。範囲をマップしようとしています。より簡単なオプションを次に示します。

ifブロックを使用

値の小さなリストについては、明白でわかりやすいifブロックを使用します。

def get_stealthiness(roll):
    if 1 <= roll < 6:
        return 'You are about as stealthy as thunderstorm.'
    Elif 6 <= roll < 11:
        return 'You tip-toe through the crowd of walkers, while loudly calling them names.'
    Elif 11 <= roll < 16:
        return 'You are quiet, and deliberate, but still you smell.'
    Elif 16 <= roll <= 20:
        return 'You move like a ninja, but attracting a handful of walkers was inevitable.'
    else:
        raise ValueError('Unsupported roll: {}'.format(roll))

stealth_roll = randint(1, 20)
print(get_stealthiness(stealth_roll))

このアプローチには何の問題もありません。本当にこれ以上複雑である必要はありません。これはmuchここでdictを使用しようとするよりも直感的で、理解しやすく、はるかに効率的です。

この方法で行うと、境界処理がより見やすくなります。上記のコードでは、範囲で<または<=各場所。上記のコードは、1〜20以外の値に対して意味のあるエラーメッセージもスローします。また、整数以外の入力を無料でサポートしますが、気にする必要はありません。

すべての値を結果にマッピングする

キーの範囲を使用しようとする代わりに、特定のキーを特定の値にマッピングするdoesに問題を再定式化できます。そのためには、範囲をループして、可能なすべての値を含む完全なdictを生成します。

OUTCOMES = {}
for i in range(1, 6):
    OUTCOMES[i] = 'You are about as stealthy as thunderstorm.'
for i in range(6, 11):
    OUTCOMES[i] = 'You tip-toe through the crowd of walkers, while loudly calling them names.'
for i in range(11, 16):
    OUTCOMES[i] = 'You are quiet, and deliberate, but still you smell.'
for i in range(16, 21):
    OUTCOMES[i] = 'You move like a ninja, but attracting a handful of walkers was inevitable.'

def get_stealthiness(roll):
    if roll not in OUTCOMES.keys():
        raise ValueError('Unsupported roll: {}'.format(roll))
    return OUTCOMES[roll]

stealth_roll = randint(1, 20)
print(get_stealthiness(stealth_roll))

この場合、範囲を使用して、結果をルックアップできるdictを生成します。各ロールを結果にマッピングし、同じ結果を複数回再利用します。これはそれほど簡単ではありません。各結果の確率を識別することはそれほど簡単ではありません。ただし、少なくともdictを適切に使用します。キーを値にマップします。

確率に応じた計算

あなたはcould確率計算に基づいて結果を選択します。基本的な考え方は、「累積」確率(ロール値の上端で既に持っている)を計算し、累積確率がランダム値を超えるまでループスルーすることです。その方法についてのアイデアはたくさんあります こちら

いくつかの簡単なオプションは次のとおりです。

  • numpy.random.choice
  • ループ:

    # Must be in order of cummulative weight
    OUTCOME_WITH_CUM_WEIGHT = [
        ('You are about as stealthy as thunderstorm.', 5),
        ('You tip-toe through the crowd of walkers, while loudly calling them names.', 10),
        ('You are quiet, and deliberate, but still you smell.', 15),
        ('You move like a ninja, but attracting a handful of walkers was inevitable.', 20),
    ]
    
    def get_stealthiness(roll):
        if 1 > roll or 20 < roll:
            raise ValueError('Unsupported roll: {}'.format(roll))
        for stealthiness, cumweight in OUTCOME_WITH_CUM_WEIGHT:
            if roll <= cumweight:
                return stealthiness
        raise Exception('Reached end of get_stealthiness without returning. This is a bug. roll was ' + str(roll))
    
    stealth_roll = randint(1, 20)
    print(get_stealthiness(stealth_roll))
    
  • random.choices (Python 3.6以降)が必要です)

    OUTCOMES_SENTENCES = [
        'You are about as stealthy as thunderstorm.',
        'You tip-toe through the crowd of walkers, while loudly calling them names.',
        'You are quiet, and deliberate, but still you smell.',
        'You move like a ninja, but attracting a handful of walkers was inevitable.',
    ]
    OUTCOME_CUMULATIVE_WEIGHTS = [5, 10, 15, 20]
    
    def make_stealth_roll():
        return random.choices(
            population=OUTCOMES_SENTENCES,
            cum_weights=OUTCOME_CUMULATIVE_WEIGHTS,
        )
    
    print(make_stealth_roll())
    

実際の数値をロールアウトするというデメリットもありますが、実装と保守がはるかに簡単です。

パイソン

「Pythonic」とは、コードをわかりやすく親しみやすいものにすることを意味します。それは、設計された目的に構造を使用することを意味します。 dictはあなたがしていることのために設計されたものではありません。

速度

これらのオプションはすべて比較的高速です。 raratircomment によると、RangeDictがその時点で最速の回答でした。ただし、私の テストスクリプト は、numpy.random.choice、私が提案したすべてのオプションは、約40〜50%高速です。

get_stealthiness_rangedict(randint(1, 20)): 3.4458323369617574 µs per loop
get_stealthiness_ifs(randint(1, 20)): 1.8013543629786 µs per loop
get_stealthiness_dict(randint(1, 20)): 1.9512669100076891 µs per loop
get_stealthiness_cumweight(randint(1, 20)): 1.9908560069743544 µs per loop
make_stealth_roll_randomchoice(): 2.037966169009451 µs per loop
make_stealth_roll_numpychoice(): 38.046008297998924 µs per loop
numpy.choice all at once: 0.5016623589908704 µs per loop

numpyは、一度に1つの結果しか得られない場合、1桁遅くなります。ただし、結果を大量に生成する場合は、桁違いに速くなります。

3
jpmc26

このアプローチはあなたが望むものを達成し、最後の行が機能します(rangeprintのPy3の動作を想定しています):

def extend_dict(d, value, x):
    for a in x:
        d[a] = value

stealth_roll = randint(1, 20)
# select from a dictionary of 4 responses using one of four ranges.
## not working.
stealth_check = {}
extend_dict(stealth_check,'You are about as stealthy as thunderstorm.',range(1,6))
extend_dict(stealth_check,'You tip-toe through the crowd of walkers, while loudly calling them names.',range(6,11))
extend_dict(stealth_check,'You are quiet, and deliberate, but still you smell.',range(11,16))
extend_dict(stealth_check,'You move like a ninja, but attracting a handful of walkers was inevitable.',range(16,20))

print(stealth_check[stealth_roll])

ところで、20面のダイをシミュレートする場合、最終インデックスは20ではなく21にする必要があります(20はrange(1,20)にないため)。

2
Paul Cornelius
stealth_check = {
                    0 : 'You are about as stealthy as thunderstorm.',
                    1 : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                    2 : 'You are quiet, and deliberate, but still you smell.',
                    3 : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                    }
stealth_roll = randint(0, len(stealth_check))
return stealth_check[stealth_roll]
2
TheLazyScripter

以下は、確率を固定した固定カテゴリ文字列のセットの1つにrandintをマッピングする場合におそらく最大限効率的です。

from random import randint
stealth_map = (None, 0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3)
stealth_type = (
    'You are about as stealthy as thunderstorm.',
    'You tip-toe through the crowd of walkers, while loudly calling them names.',
    'You are quiet, and deliberate, but still you smell.',
    'You move like a ninja, but attracting a handful of walkers was inevitable.',
    )
for i in range(10):
    stealth_roll = randint(1, 20)
    print(stealth_type[stealth_map[stealth_roll]])
1
Terry Jan Reedy