web-dev-qa-db-de.com

Bereich als Wörterbuchschlüssel in Python

Ich hatte also die Idee, einen Zahlenbereich als Schlüssel für einen einzelnen Wert in einem Wörterbuch verwenden zu können.

Ich habe den Code unten geschrieben, kann ihn aber nicht zum Laufen bringen. Ist es überhaupt möglich?

    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]
16
Cuylar Conly

Bei Python 3 und Python 2 ist es möglich, wenn Sie xrange anstelle von range verwenden:

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

Die Art und Weise, wie Sie es verwenden, funktioniert jedoch nicht. Sie könnten über die Tasten wie folgt iterieren:

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

Dies ist nicht schön (O (n)), aber wenn es ein kleines Wörterbuch ist, wie Sie es gezeigt haben, ist es in Ordnung. Wenn Sie das wirklich tun möchten, würde ich die Unterklasse dict so definieren, dass sie automatisch so funktioniert:

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'
11
L3viathan

Sie können ein Wörterbuch nicht direkt aus einem Bereich erstellen, es sei denn, der Bereich selbst soll der Schlüssel sein. Ich glaube nicht, dass du das willst. Um einzelne Einträge für jede Möglichkeit innerhalb des Bereichs zu erhalten:

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)]
                    )

Wenn Sie eine dict haben, die durch einen kleinen Bereich von Ganzzahlen indiziert ist, sollten Sie wirklich eine list verwenden:

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.']
6
Mark Ransom

Ja, das ist nur möglich, wenn Sie Ihre range-Listen als unveränderlich Tuple konvertieren, sodass diese als Hash-Schlüssel für Ihr Wörterbuch akzeptiert werden können:

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

BEARBEITEN: Eigentlich funktioniert es in Python 3, da range ein unveränderlicher Sequenztyp ist und eine unveränderliche Tuple anstelle einer list generiert, wie L3viathan angegeben hat.

sie können jedoch nicht mit einer einzelnen Ganzzahl als Schlüssel darauf zugreifen. Ihre letzte Zeile funktioniert nicht.

Ich habe einige Zeit gebraucht, um eine Lösung zu entwickeln, die für alle möglichen Werte geeignet ist. Die Auswahl eines Eintrags im Wörterbuch funktioniert, solange die Zeilen nicht durch größere Bereiche "gewichtet" werden.

Es ruft bisect für die sortierten Schlüssel auf, um den Einfügepunkt zu finden, hackt ihn ein wenig und findet den besten Wert im Wörterbuch mit O(log(N)) Komplexität, was bedeutet, dass er eine wirklich große Liste verarbeiten kann (hier vielleicht etwas zu viel :) das Wörterbuch ist in diesem Fall auch zu viel)

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]])

Ich habe eine RangeKeyDict-Klasse für die Behandlung von Fällen wie diesem geschrieben, die allgemeiner und einfacher zu bedienen ist. Überprüfen Sie zur Verwendung die Codes in __main__.

um es zu installieren mit: 

pip install range-key-dict

Verwendungszweck:

from range_key_dict import RangeKeyDict

if __== '__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 ist das falsche Werkzeug für diesen Job. dict dient zum Zuordnen bestimmter Schlüssel zu bestimmten Werten. Das ist nicht was du tust; Sie versuchen, Bereiche zuzuordnen. Hier sind einige einfachere Optionen.

Verwenden Sie if Blöcke

Verwenden Sie für eine kleine Liste von Werten die offensichtlichen und einfachen if -Blöcke:

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))

An diesem Ansatz ist absolut nichts auszusetzen. Komplexer muss es nicht sein. Dies ist viel intuitiver, viel einfacher herauszufinden und viel effizienter als der Versuch, hier ein dict zu verwenden.

Auf diese Weise wird auch das Randhandling sichtbarer. In dem Code, den ich oben präsentiere, können Sie schnell erkennen, ob der Bereich an jeder Stelle < Oder <= Verwendet. Der obige Code gibt auch eine aussagekräftige Fehlermeldung für Werte außerhalb von 1 bis 20 aus. Er unterstützt auch die kostenlose Eingabe von Nicht-Ganzzahlen, obwohl Sie sich möglicherweise nicht darum kümmern.

Ordnen Sie jeden Wert einem Ergebnis zu

Anstatt zu versuchen, die Bereiche für die Schlüssel zu verwenden, können Sie Ihr Problem so umformulieren, dass bestimmte Schlüssel bestimmten Werten zuordnet . Sie durchlaufen dazu die Bereiche und generieren ein vollständiges dict mit allen möglichen Werten:

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))

In diesem Fall verwenden wir die Bereiche, um ein dict zu generieren, in dem wir ein Ergebnis nachschlagen können. Wir ordnen jedem Wurf ein Ergebnis zu und verwenden dieselben Ergebnisse mehrmals. Dies ist weniger einfach; Es ist nicht ganz so einfach, die Wahrscheinlichkeit jedes Ergebnisses zu erkennen. Aber zumindest verwendet es dict richtig: Es ordnet einen Schlüssel einem Wert zu.

Berechnen Sie nach Wahrscheinlichkeiten

Sie könnten das Ergebnis basierend auf einer Wahrscheinlichkeitsberechnung auswählen. Die Grundidee besteht darin, eine "kumulative" Wahrscheinlichkeit zu berechnen (die Sie bereits mit dem oberen Ende der Rollwerte haben) und dann eine Schleife durchzuführen, bis die kumulative Wahrscheinlichkeit den Zufallswert überschreitet. Es gibt viele Ideen, wie man das macht hier .

Einige einfache Optionen sind:

  • numpy.random.choice
  • Eine Schleife:

    # 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 (erfordert Python 3.6 oder höher)

    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())
    

Einige haben den Nachteil, dass Sie den eigentlichen Zahlenwert nicht mehr benötigen, aber sie sind viel einfacher zu implementieren und zu warten.

Pythonic

"Pythonic" bedeutet, Ihren Code einfach und zugänglich zu halten. Es bedeutet, Strukturen für die Zwecke zu verwenden, für die sie entworfen wurden. dict wurde nicht für das entwickelt, was Sie tun.

Geschwindigkeit

All diese Optionen sind vergleichsweise schnell. Laut raratir 's comment war der RangeDict zu dieser Zeit die schnellste Antwort. Mein Testskript zeigt jedoch, dass mit Ausnahme von numpy.random.choice Alle von mir vorgeschlagenen Optionen etwa 40% bis 50% schneller sind:

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 ist eine Größenordnung langsamer, wenn Sie jeweils ein Ergebnis erhalten. Es ist jedoch eine Größenordnung schneller, wenn Sie Ihre Ergebnisse in großen Mengen generieren.

3
jpmc26

Dieser Ansatz wird das erreichen, was Sie wollen, und die letzte Zeile wird funktionieren (setzt Py3-Verhalten von range und print voraus):

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])

Übrigens, wenn Sie einen 20-seitigen Würfel simulieren, muss der Endindex 21 sein, nicht 20 (da 20 nicht im Bereich (1,20) liegt).

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

Das Folgende ist wahrscheinlich maximal effizient, wenn ein Randint mit fester Wahrscheinlichkeit auf einen Satz fester Kategoriestränge abgebildet wird.

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