2015-04-12 75 views
30

我工作在一個範圍通常被包含性描述的域中。我有人類可讀的描述,例如from A to B,它們表示包括兩個端點的範圍 - 例如, from 2 to 4表示2, 3, 4我應該如何處理Python中的包含範圍?

在Python代碼中使用這些範圍的最佳方式是什麼?下面的代碼工作,以生成一個整數包括的範圍,但我也需要執行包片操作:

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

唯一的完整的解決方案,我看到的是明確使用+ 1(或- 1)我每次使用range或分時符號(例如range(A, B + 1),l[A:B+1]range(B, A - 1, -1))。這種重複真的是使用包容性範圍的最佳方式嗎?

編輯:感謝L3viathan回答。寫一個inclusive_slice功能,以補充inclusive_range當然是一個選擇,雖然我可能會寫如下:

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

...這裏代表代碼來處理負指數,用切片使用時並不簡單 - 注意,例如,如果slice_to == -1,L3viathan的功能給出不正確的結果。

但是,看起來inclusive_slice函數會使用起來很尷尬 - l[inclusive_slice(A, B)]真的比l[A:B+1]更好嗎?

有沒有更好的方法來處理包含範圍?

編輯2:謝謝你的新答案。我同意弗朗西斯和科利的觀點,即改變全球或某些類別的切片操作的含義會導致重大混亂。因此,我現在傾向於編寫一個inclusive_slice函數。爲了回答我之前編輯的問題,我得出結論:使用這樣的函數(例如l[inclusive_slice(A, B)])會比手動添加/減少1(例如l[A:B+1])更好,因爲它會允許邊界情況(如B == -1B == None)將在一個地方處理。我們可以減少使用該功能的尷尬嗎?

編輯3:我一直在想如何改進使用語法,目前看起來像l[inclusive_slice(1, 5, 2)]。特別是,如果包含切片的創建類似於標準切片語法,那將是一件好事。爲了實現這個功能,可以使用inclusive而不是inclusive_slice(start, stop, step),該功能將切片作爲參數。對於inclusive理想的使用語法是行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 

不幸的是,這是不Python,只允許內[]使用:語法允許的。因此必須使用語法23(其中s_的行爲如the version provided by numpy)來調用inclusive

其他可能性是使inclusive成一個對象與__getitem__,允許語法4,或者應用inclusive僅向片的stop參數,如在語法5。不幸的是,我不相信後者可以工作,因爲inclusive需要知道step的價值。

在可行的語法(原始l[inclusive_slice(1, 5, 2)],加上2,34),這將是最好的使用?還是有另一種更好的選擇?

最後修改:謝謝大家的回覆和評論,這已經非常有趣了。我一直都是Python「做它的一種方式」哲學的忠實粉絲,但是這個問題是由Python的「單向」和問題域所禁止的「單向」之間的衝突造成的。在語言設計中,我確實獲得了TIMTOWTDI的一些讚賞。

給了第一和投票最高的答案,我要獎勵的賞金L3viathan。

+3

請注意,你寫的功能是不正確的。第二個參數「stop + step」有可能使包含範圍超出應有的範圍。它應該是'stop + 1'。例如'range(0,7,3)'是'[0,3,6]',但是你的函數會給出'[0,3,6,9]'。 – Shashank

+0

域中的元素是否始終是整數?我的意思是'從2到4'可以表示爲'[2,3,4]'或'2:00,2:01,...,4:00'。 –

+0

@qarma - 是的,就像內置的range()函數一樣,我只需要處理整數範圍。 – user200783

回答

13

寫出包容片的附加功能,並使用它切片。雖然有可能例如子類列表和實施__getitem__反應切片對象,我建議不要它,因爲你的代碼的行爲與預期相反的人,但你 - 也許你也一樣,在一年的時間。

inclusive_slice看起來是這樣的:

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] 

我會親自做的,僅僅是用你所提到的「完整」的解決方案(range(A, B + 1)l[A:B+1])和評論很好。

+1

您可以在這裏簡單地添加一個默認步驟= 1,參數。從 –

+1

切片一[:-1]將轉化爲一個[N:0]在這裏,這意味着完全不同的東西 – wim

+0

也slice_to應該能夠採取'None'沒有與類型錯誤 – wim

4

我認爲,標準答案是隻使用+1或-1到處它是必要的。

你不想全局更改切片被理解的方式(即會破壞大量的代碼),但另一種解決方案是建立要爲其切片具有包容性的對象類層次結構。例如,對於一個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] 

當然,你想要做同樣的__setitem____delitem__

(我用了list但適用於任何SequenceMutableSequence。)

+2

我認爲使用這樣的類會讓事情變得更加困難。例如,我可能期望'InclusiveList(範圍(11))'到_include_'11'。 –

+1

'範圍(11)'不包含'11',只是用來初始化列表。我明白在這裏使用'range'會讓人困惑,我已經改變了這個例子。 –

4

如果你不想指定步長,而是步數,存在使用numpy.linspace其中包括選項起點和終點

import numpy as np 

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

thx提醒'linspace':但是有一個版本返回*整數*在給定的範圍內遞增1?否則,這個轉換工作需要儘可能多的一些其他答案。 – javadba

8

因爲在Python中,結束索引始終是排他的,這是值得考慮內部始終用「巨蟒公約」的價值觀。這樣,你就可以避免在代碼中混淆兩者。

永遠只能對付「對外表示」通過專用轉換子程序:

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) 

或者,你可以標記保持與true Hungarian notation的「不尋常」表現的變量。

+1

我不同意。因爲它今天這樣做並不意味着它在將來永遠不應該這樣做。許多語言都具有包容性範圍以及獨佔性,因爲它通常正是您所需要的。 (紅寶石和斯威夫特和Perl的浮現在腦海中立即) – uchuugaka

+0

@uchuugaka這實際上是不可能,這樣的改變將不會發生在可預見的未來。 1)Python開發者對於向後兼容性非常小心。當轉換到下一個主要版本時,它們絕對不會改變這種情況。 2)Python的重點是簡單性和可維護性 - 這是一個潘多拉盒子。 3)基本計算概念(馮諾依曼架構,二進制系統),它規定0 ... N-1索引比1..N更方便。所以,根據YAGNI的原則,你不用擔心你的Python解決方案。 –

3

本來要發表評論,但它更容易編寫代碼作爲一個答案,所以......

我不會寫,重新定義切片,除非它是很清楚的一類。我有一個代表整個位分片的類。在我的上下文中,'4:2'非常清楚包容,整數並沒有用於切片,所以它(幾乎)是不可接受的(恕我直言,有人會不同意)。

對於列表,你有,你會在你的代碼

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

這樣做

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

,後來的情況下,這是一個很容易犯的錯誤,因爲列表已經有一個明確的用法..他們看起來完全相同,但行爲不同,所以這將非常容易混淆調試,特別是如果你沒有寫它。

這並不意味着你完全失去了......切片很方便,但畢竟它只是一個功能。你可以說功能添加到這樣的事情,所以這可能是一個更簡單的方式來獲得它:

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] 

然而,這種解決方案,許多人一樣,(除了是不完整的......我知道)有它跟隨着簡單的切片符號一樣簡單......它比通過列表作爲參數要簡單一些,但仍然比[4:2]更難。實現這一點的唯一方法是將不同的傳遞給切片,這可以用不同的方式進行區分,以便用戶在閱讀它時可以知道它做了什麼,而且它可能仍然很簡單。

一種可能性......浮點數。它們是不同的,所以你可以看到它們,它們並不比'簡單'語法困難得多。這不是內置的,所以還是有一些「神奇」參與,但就語法糖,它不壞....

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] 

3.0應該足以告訴任何Python程序員「哎,事不尋常的是在那裏'...不一定什麼正在進行,但至少有沒有意外,它行爲'怪異'。

注意,也沒什麼可說是獨一無二的名單...你可以很容易寫一個裝飾,可以爲任何類做到這一點:

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) 

@inclusiveclass 
class inclusivelist(list): 
    pass 

第一種形式是可能雖然更有用。

4

沒有寫自己的班級,這個功能似乎是要走的路。我能想到的最多不是存儲實際列表,只是返回您關心的範圍內的發電機。由於我們現在談論的使用語法 - 這裏是你可以做

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 

然後用什麼爲:

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

當然你還需要檢查是否有其他形式的不良輸入如果其他人將要使用這個功能。

2

重載這些基本概念很困難,也可能不明智。 與b-a + 1中的新包含列表類len(l [a:b])可能導致混淆。
保存天然蟒蛇感,同時在基本風格給人可讀性,只是定義:

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 

,那麼你可以管理,只要你想,保持自然的蟒蛇邏輯:

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

沒有正確處理負數 – wim

+0

@wim:謝謝,現在修復。 –

3

關注您的最佳的語法要求,有關定位的是什麼:

l[1:UpThrough(5):2] 

您可以在此使用__index__滿足實現HOD:

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] 

如果你用了很多,你可以使用更短的名稱upIncldownIncl或甚至 InInRev

你也可以打造出這些類,這樣,比切片使用其他的,他們喜歡實際的指數 行爲:

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

是否有你將'UpThrough'和'DownThrough'定義爲類而不是簡單函數的原因? – user200783

+0

原因是使用'__int__'和/或'__float__',這樣'int(UpThrough(5))== 5',但在切片中會生成正確的索引。您也可以添加算術運算符。 – shaunc

3

而不是創建API不是傳統的或擴展數據類型,例如列表,最好創建一個Slice函數作爲內置的slice的包裝器,以便您可以將其傳遞到切片所需的任何位置。 對於一些特殊情況,Python已經支持這種方法,並且您可以爲該例外情況保證。舉個例子,一個包容性的片會是什麼樣子

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

而且你可以將其用於任何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) 

最後,你還可以創建一個名爲irange所包括的範圍,以補充包片(寫在OPs)。

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