2015-04-12 6 views
30

Ich arbeite in einer Domäne, in der Bereiche konventionell einschließlich beschrieben werden. Ich habe menschenlesbare Beschreibungen wie from A to B, die Bereiche darstellen, die beide Endpunkte enthalten - z. from 2 to 4 bedeutet 2, 3, 4.Wie sollte ich mit Include-Bereichen in Python umgehen?

Was ist der beste Weg, um mit diesen Bereichen in Python-Code zu arbeiten? Der folgende Code funktioniert inklusive Bereiche von ganzen Zahlen zu erzeugen, aber ich brauche auch inklusive Scheibe Operationen auszuführen:

def inclusive_range(start, stop, step): 
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step) 

Die einzige komplette Lösung, die ich sehe, ist ausdrücklich + 1 (oder - 1) ich jedes Mal zu verwenden, verwende range oder Scheibe Notation (zB range(A, B + 1), l[A:B+1], range(B, A - 1, -1)). Ist diese Wiederholung wirklich der beste Weg, um mit inklusiven Bereichen zu arbeiten?

Bearbeiten: Dank L3viathan für die Beantwortung. Das Schreiben eine inclusive_slice Funktion ergänzen inclusive_range ist sicherlich eine Option, obwohl ich wahrscheinlich würde es wie folgt schreiben:

def inclusive_slice(start, stop, step): 
    ... 
    return slice(start, (stop + 1) if step >= 0 else (stop - 1), step) 

... hier repräsentiert Code negative Indizes zu behandeln, die nicht einfach ist, wenn sie mit Scheiben verwendet - Note, zum Beispiel , dass die Funktion von L3viathan falsche Ergebnisse liefert, wenn slice_to == -1.

Allerdings scheint es, dass eine inclusive_slice Funktion umständlich zu bedienen wäre - ist l[inclusive_slice(A, B)] wirklich besser als l[A:B+1]?

Gibt es einen besseren Weg, um inklusive Bereiche zu behandeln?

Bearbeiten 2: Vielen Dank für die neuen Antworten. Ich stimme Francis und Corley zu, dass eine Änderung der Bedeutung von Slice-Operationen, entweder global oder für bestimmte Klassen, zu erheblicher Verwirrung führen würde. Ich lehne mich daher nun an, eine inclusive_slice Funktion zu schreiben.

Um meine eigene Frage aus dem vorherigen bearbeiten zu beantworten, ich bin zu dem Schluss gekommen, dass eine solche Funktion (zB l[inclusive_slice(A, B)]) wäre besser als das manuelle Hinzufügen/Subtrahieren von 1 (zB l[A:B+1]), da es Grenzfälle zulassen würde (wie B == -1 und B == None) an einem einzigen Ort behandelt werden. Können wir die Peinlichkeit bei der Verwendung der Funktion reduzieren?

Edit 3: Ich habe darüber nachgedacht, wie die Syntax der Verwendung verbessern, die derzeit wie l[inclusive_slice(1, 5, 2)] aussieht. Insbesondere wäre es gut, wenn die Erzeugung einer inklusiven Schicht der Standard-Slice-Syntax ähnelt. Um dies zu ermöglichen, könnte anstelle von inclusive_slice(start, stop, step) eine Funktion inclusive vorhanden sein, die eine Scheibe als Parameter verwendet. Die ideale Verwendungssyntax für inclusive wäre Linie 1:

l[inclusive(1:5:2)]   # 1 
l[inclusive(slice(1, 5, 2))] # 2 
l[inclusive(s_[1:5:2])]  # 3 
l[inclusive[1:5:2]]   # 4 
l[1:inclusive(5):2]   # 5 

Leider ist dies nicht von Python erlaubt, die nur die Verwendung von : Syntax innerhalb [] ermöglicht. inclusive müsste daher entweder mit der Syntax 2 oder 3 aufgerufen werden (wobei s_ wie the version provided by numpy wirkt).

Andere Möglichkeiten sind inclusive in ein Objekt machen mit __getitem__, Syntax ermöglicht 4 oder inclusive nur den stop Parameter der Scheibe anzulegen, wie in der Syntax 5. Leider glaube ich nicht, dass letzteres funktioniert werden kann, da inclusive die Kenntnis des Werts step erfordert.

Von dem bearbeitbar Syntaxen (das Original l[inclusive_slice(1, 5, 2)] plus 2, 3 und 4), die das Beste wäre, zu benutzen? Oder gibt es eine andere, bessere Option?

Final Edit: Vielen Dank für die Antworten und Kommentare, das war sehr interessant. Ich war schon immer ein Fan von Pythons "one way to do it" -Philosophie, aber dieses Problem wurde durch einen Konflikt zwischen Python's "one way" und dem "one way" der Problemdomäne verursacht. Ich habe definitiv eine Wertschätzung für TIMTOWTDI im Sprachdesign gewonnen.

Für die erste und am besten gewählte Antwort, ich die Belohnung an L3viathan.

+3

Beachten Sie, dass die von Ihnen geschriebene Funktion falsch ist. Das zweite Argument "stop + step" hat das Potenzial, den inklusiven Bereich über das hinaus, was es sein sollte, zu erhöhen. Es sollte "Stopp + 1" sein. z.B. 'range (0, 7, 3)' ist '[0, 3, 6]' aber Ihre Funktion würde '[0, 3, 6, 9]' ergeben. – Shashank

+0

Sind Elemente in Bereich immer ganze Zahlen in Ihrer Domäne? Ich meine z.B. 'von 2 bis 4' kann bedeuten '[2,3,4]' oder '2: 00,2: 01, ..., 4: 00'. –

+0

@qarma - Ja, genau wie die eingebaute range() -Funktion muss ich nur Bereiche von ganzen Zahlen behandeln. – user200783

Antwort

13

Schreiben Sie eine zusätzliche Funktion für inklusives Slice und verwenden Sie diese anstelle von Slicing. Während es möglich wäre, z.B. Unterklasse Liste und implementieren eine __getitem__ Reagieren auf ein Slice-Objekt, würde ich davon abraten, da Ihr Code wird wider Erwarten für jeden außer Ihnen - und wahrscheinlich auch für Sie, in einem Jahr.

inclusive_slice könnte wie folgt aussehen:

def inclusive_slice(myList, slice_from=None, slice_to=None, step=1): 
    if slice_to is not None: 
     slice_to += 1 if step > 0 else -1 
    if slice_to == 0: 
     slice_to = None 
    return myList[slice_from:slice_to:step] 

Was würde ich persönlich tun, wird benutzen Sie einfach die „vollständige“ Lösung, die Sie erwähnt (range(A, B + 1), l[A:B+1]) und auch kommentieren.

+1

Sie können hier einfach ein Standardschritt = 1, Argument hinzufügen. –

+1

slicing von a [n: -1] wird hier zu einem [n: 0] übersetzt, was etwas völlig anderes bedeutet – wim

+0

auch slice_to sollte in der Lage sein, 'None' ohne Absturz mit einem TypeError – wim

4

Ich glaube, dass die Standardantwort ist, einfach +1 oder -1 überall dort zu verwenden, wo es benötigt wird.

Sie möchten die Art, wie Slices verstanden werden, nicht global ändern (was viel Code kaputt macht), aber eine andere Möglichkeit wäre, eine Klassenhierarchie für die Objekte zu erstellen, für die die Slices inklusive sein sollen. Zum Beispiel für ein list:

class InclusiveList(list): 
    def __getitem__(self, index): 
     if isinstance(index, slice): 
      start, stop, step = index.start, index.stop, index.step 
      if index.stop is not None: 
       if index.step is None: 
        stop += 1 
       else: 
        if index.step >= 0: 
         stop += 1 
        else: 
         if stop == 0: 
          stop = None # going from [4:0:-1] to [4::-1] since [4:-1:-1] wouldn't work 
         else: 
          stop -= 1 
      return super().__getitem__(slice(start, stop, step)) 
     else: 
      return super().__getitem__(index) 

>>> a = InclusiveList([1, 2, 4, 8, 16, 32]) 
>>> a 
[1, 2, 4, 8, 16, 32] 
>>> a[4] 
16 
>>> a[2:4] 
[4, 8, 16] 
>>> a[3:0:-1] 
[8, 4, 2, 1] 
>>> a[3::-1] 
[8, 4, 2, 1] 
>>> a[5:1:-2] 
[32, 8, 2] 

Natürlich wollen Sie das gleiche mit __setitem__ und __delitem__ zu tun.

(verwendete ich ein list aber das funktioniert für jede Sequence oder MutableSequence.)

+2

Ich denke, mit einer solchen Klasse verwechselt Dinge mehr als es hilft. Zum Beispiel könnte ich 'InclusiveList (Bereich (11)) 'zu _include_' 11 erwarten. –

+1

Nun 'Bereich (11)' enthält nicht '11' und wird nur verwendet, um die Liste zu initialisieren. Ich verstehe, dass die Verwendung von 'range' hier verwirrend ist, ich habe das Beispiel geändert. –

4

Wenn Sie die Schrittgröße nicht angeben wollen, sondern die Anzahl der Schritte, besteht die Möglichkeit, numpy.linspace zu verwenden, die enthält die Start- und Punkt endet

import numpy as np 

np.linspace(0,5,4) 
# array([ 0.  , 1.66666667, 3.33333333, 5.  ]) 
+0

thx für die Erinnerung an 'linspace': aber gibt es eine Version, die * integers * im angegebenen Bereich um eins zurückgibt? Ansonsten sind so viele Konvertierungsarbeiten erforderlich wie einige andere Antworten. – javadba

8

Da in Python, ist die Endung Index immer exklusiv, es ist eine Überlegung wert, immer die „Python-Konvention“ intern Werte zu verwenden. Auf diese Weise ersparen Sie sich die Vermischung der beiden in Ihrem Code.

immer nur mit der „Außendarstellung“ durch spezielle Umwandlung Subroutinen befassen:

def text2range(text): 
    m = re.match(r"from (\d+) to (\d+)",text) 
    start,end = int(m.groups(1)),int(m.groups(2))+1 

def range2text(start,end): 
    print "from %d to %d"%(start,end-1) 

Alternativ können Sie auch die Variablen markieren können die „ungewöhnliche“ Darstellung mit der true Hungarian notation halten.

+1

Ich stimme nicht zu. Weil es heute so ist, heißt das nicht, dass es das in Zukunft nicht tun sollte. Viele Sprachen haben sowohl exklusive als auch exklusive Bereiche, weil es oft genau das ist, was Sie brauchen. (Ruby & Swift & Perl kommen mir sofort in den Sinn) – uchuugaka

+0

@uchuugaka Es ist praktisch unmöglich, dass solch eine Veränderung jemals in absehbarer Zeit passieren wird. 1) Python-Entwickler achten sehr auf Rückwärtskompatibilität. Sie werden dies definitiv nie anders als beim Übergang zu einer nächsten Hauptversion ändern. 2) Pythons Fokus ist Einfachheit und Wartbarkeit - dies wäre eine Büchse der Pandora. 3) grundlegende Computerkonzepte (von Neumann-Architektur, das binäre System), die vorschreiben, dass 0 ... N-1-Indexierung bequemer ist als 1..N gehen kaum irgendwo hin. Nach dem YAGNI-Prinzip sollten Sie sich in Ihren Python-Lösungen darum keine Gedanken machen. –

3

Ging zu kommentieren, aber es ist einfacher Code als Antwort zu schreiben, also ...

ich würde eine Klasse schreiben, die Slicing neu definiert, es sei denn, es sehr klar ist. Ich habe eine Klasse, die Ints mit Bit Slicing darstellt. In meinen Kontexten ist '4: 2' sehr klar inklusive, und Ints haben bereits keinen Nutzen für das Schneiden, also ist es (kaum) akzeptabel (imho, und einige würden nicht zustimmen).

Für Listen, haben Sie den Fall, dass Sie so etwas wie

list1 = [1,2,3,4,5] 
list2 = InclusiveList([1,2,3,4,5]) 

und später in Ihrem Code

if list1[4:2] == test_list or list2[4:2] == test_list: 

tun, und das ist ein sehr einfach, Fehler zu machen, da Liste hat bereits eine gut definierte Verwendung .. sie sehen identisch aus, aber handeln anders, und so wird dies sehr verwirrend sein zu debuggen, vor allem, wenn Sie es nicht geschrieben haben.

Das bedeutet nicht, dass Sie völlig verloren sind ... Schneiden ist praktisch, aber schließlich ist es nur eine Funktion. Und Sie können diese Funktion so etwas hinzuzufügen, so könnte dies ein einfacher Weg, um es zu bekommen zu sein:

class inc_list(list): 
    def islice(self, start, end=None, dir=None): 
     return self.__getitem__(slice(start, end+1, dir)) 

l2 = inc_list([1,2,3,4,5]) 
l2[1:3] 
[0x3, 
0x4] 
l2.islice(1,3) 
[0x3, 
0x4, 
0x5] 

Diese Lösung ist jedoch, wie viele andere, (abgesehen davon, unvollständig ... ich weiß) hat Die Achillesferse ist einfach nicht so einfach wie die einfache Slice-Notation ... es ist ein wenig einfacher als die Liste als Argument zu übergeben, aber immer noch härter als nur [4: 2]. Die einzige Möglichkeit, dies zu erreichen, besteht darin, etwas verschiedenes an die Scheibe zu übergeben, die anders dargestellt werden könnte, so dass der Benutzer beim Lesen wissen würde, was sie getan haben, und es könnte immer noch so einfach sein.

Eine Möglichkeit ... Gleitkommazahlen. Sie sind anders, so dass Sie sie sehen können, und sie sind nicht viel schwieriger als die "einfache" Syntax. Es ist nicht eingebaut, so gibt es immer noch einig ‚magische‘ beteiligt, aber so weit wie syntaktischen Zucker, es ist nicht schlecht ....

class inc_list(list): 
    def __getitem__(self, x): 
     if isinstance(x, slice): 
      start, end, step = x.start, x.stop, x.step 
      if step == None: 
       step = 1 
      if isinstance(end, float): 
       end = int(end) 
       end = end + step 
       x = slice(start, end, step) 
      return list.__getitem__(self, x) 

l2 = inc_list([1,2,3,4,5]) 
l2[1:3] 
[0x2, 
0x3] 
l2[1:3.0] 
[0x2, 
0x3, 
0x4] 

Die 3.0 genug sein sollte, jeden Python-Programmierer zu sagen: ‚Hey, was Ungewöhnlich ist da '... nicht unbedingt was vor sich geht, aber zumindest gibt es keine Überraschung, dass es' komisch 'wirkt.

Hinweis, dass es nichts Besonderes, darüber zu Listen ... Sie könnten leicht einen Dekorateur schreiben, dass dies für jede Klasse tun könnte:

def inc_getitem(self, x): 
    if isinstance(x, slice): 
     start, end, step = x.start, x.stop, x.step 
     if step == None: 
      step = 1 
     if isinstance(end, float): 
      end = int(end) 
      end = end + step 
      x = slice(start, end, step) 
    return list.__getitem__(self, x) 

def inclusiveclass(inclass): 
    class newclass(inclass): 
     __getitem__ = inc_getitem 
    return newclass 

ilist = inclusiveclass(list) 

oder

@inclusiveclass 
class inclusivelist(list): 
    pass 

Die erste Form ist wahrscheinlich nützlicher aber.

4

Ohne Ihre eigene Klasse zu schreiben, scheint die Funktion der richtige Weg zu sein. Was ich mir höchstens vorstellen kann, ist nicht die Speicherung der tatsächlichen Listen, sondern nur die Rückgabe der Generatoren für den Bereich, der Ihnen wichtig ist.Da wir nun über die Nutzung Syntax reden - hier ist das, was Sie

def closed_range(slices): 
    slice_parts = slices.split(':') 
    [start, stop, step] = map(int, slice_parts) 
    num = start 
    if start <= stop and step > 0: 
     while num <= stop: 
      yield num 
      num += step 
    # if negative step 
    elif step < 0: 
     while num >= stop: 
      yield num 
      num += step 

tun konnte, und verwenden Sie dann als:

list(closed_range('1:5:2')) 
[1,3,5] 

Natürlich werden Sie auch für andere Formen der schlechten Eingabe überprüfen müssen wenn jemand anderes diese Funktion benutzt.

2

Es ist schwierig und wahrscheinlich nicht klug, solche grundlegenden Konzepte zu überladen. mit einer neuen inklusiven Listenklasse, len (l [a: b]) in b-a + 1, die zu Verwirrungen führen kann.
den natürlichen Python Sinn zu bewahren, während der Lesbarkeit in einem BASIC-Stil geben, definiert nur:

STEP=FROM=lambda x:x 
TO=lambda x:x+1 if x!=-1 else None 
DOWNTO=lambda x:x-1 if x!=0 else None 

dann können Sie verwalten, wie Sie wollen, Logik, um die natürliche Python zu halten:

>>>>l=list(range(FROM(0),TO(9))) 
>>>>l 
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
>>>l[FROM(9):DOWNTO(3):STEP(-2)] == l[9:2:-2] 
True 
+0

behandelt negative Zahlen nicht richtig – wim

+0

@wim: danke, jetzt behoben. –

3

Fokussierung auf Ihre Anfrage für beste Syntax, was Targeting:

l[1:UpThrough(5):2] 

können Sie erreichen dies der __index__ traf mit Jetzt brauchen Sie nicht einmal eine spezielle Liste Klasse (und nicht globale Definition entweder ändern müssen)

class UpThrough(object): 
    def __init__(self, stop): 
     self.stop = stop 

    def __index__(self): 
     return self.stop + 1 

class DownThrough(object): 
    def __init__(self, stop): 
     self.stop = stop 

    def __index__(self): 
     return self.stop - 1 

:

>>> l = [1,2,3,4] 
>>> l[1:UpThrough(2)] 
[2,3] 

Wenn Sie eine Menge verwenden könnten Sie kürzere hod Namen upIncl, downIncl oder sogar In und InRev.

Sie können auch diese Klassen, so dass, anders als die Verwendung in Scheibe bauen aus, sie wirken wie der tatsächliche Index:

def __int__(self): 
    return self.stop 
+1

Gibt es einen Grund, warum Sie "UpThrough" und "DownThrough" als Klassen und nicht als einfache Funktionen definiert haben? – user200783

+0

Der Grund wäre, '__int__' und/oder' __float__' zu verwenden, so dass 'int (UpThrough (5)) == 5 ', aber im Segment den rechten Index erzeugt. Sie können auch arithmetische Operatoren hinzufügen. – shaunc

3

Statt API zu schaffen, die nicht üblich ist oder Datentypen wie Liste erstreckt, Es wäre ideal, um eine Slice Funktion einen Wrapper über die eingebaute slice zu erstellen, so dass Sie es überall dort weitergeben können, wo ein Slicing erforderlich ist. Python unterstützt diesen Ansatz für einige Ausnahmefälle, und der Fall, den Sie haben, könnte diesen Ausnahmefall rechtfertigen. Als Beispiel würde ein inklusives Scheibe aussehen

def islice(start, stop = None, step = None): 
    if stop is not None: stop += 1 
    if stop == 0: stop = None 
    return slice(start, stop, step) 

Und Sie es für jede sequence types

>>> range(1,10)[islice(1,5)] 
[2, 3, 4, 5, 6] 
>>> "Hello World"[islice(0,5,2)] 
'Hlo' 
>>> (3,1,4,1,5,9,2,6)[islice(1,-2)] 
(1, 4, 1, 5, 9, 2) 

können Schließlich können Sie auch einen Inklusivbereich irange die inklusive Scheibe ergänzen genannt erstellen (geschrieben in Linien von OP).

def irange(start, stop, step): 
    return range(start, (stop + 1) if step >= 0 else (stop - 1), step) 
Verwandte Themen