8

故事:生成列表

目前,我有一個功能下的測試,預計以下規則整數列表的列表:

  1. 號子列表(讓我們稱之爲N)可以是從1至50
  2. 號內的子列表的值對於所有子列表是相同的(矩形形狀),並應是> = 0和< = 5
  3. 子列表中的值不能超過或等於子列表的總數。換句話說,子列表內的每個值是整數> = 0和< N

樣品有效輸入:

[[0]] 
[[2, 1], [2, 0], [3, 1], [1, 0]] 
[[1], [0]] 

樣品無效輸入:

[[2]] # 2 is more than N=1 (total number of sublists) 
[[0, 1], [2, 0]] # 2 is equal to N=2 (total number of sublists) 

我試圖與基於性質的測試接近,並與hypothesis library生成不同的有效輸入,並試圖將我的頭圍繞lists()integers(),但不能使其工作:

  • 條件#1很容易與lists()min_sizemax_size參數
  • 條件#2下Chaining strategies together
  • 覆蓋接近狀態#3我正在努力的 - 因爲,如果我們使用上面例子中的rectangle_lists,我們沒有提及integers()中的「父」列表的長度

問題:

如何限制內部子列表的整數值小於子列表的總數是多少?


我的一些嘗試:

from hypothesis import given 
from hypothesis.strategies import lists, integers 

@given(lists(lists(integers(min_value=0, max_value=5), min_size=1, max_size=5), min_size=1, max_size=50)) 
def test(l): 
    # ... 

這一個很遠不能滿足需求 - 列表不是嚴格意義上的矩形形狀和所產生的整數值可以去在名單的產生大小。

from hypothesis import given 
from hypothesis.strategies import lists, integers 

@given(integers(min_value=0, max_value=5).flatmap(lambda n: lists(lists(integers(min_value=1, max_value=5), min_size=n, max_size=n), min_size=1, max_size=50))) 
def test(l): 
    # ... 

這裏#1和#2滿足要求,但整數值可能大於列表的大小 - 不滿足要求#3。

回答

5

有一個很好的通用技術,當試圖解決這樣棘手的約束時經常有用:嘗試構建看起來有點像你想要的東西,但不滿足所有約束,然後用一個函數修改它(例如通過扔掉壞位或修補不太有效的位),使其滿足約束。

對於你的情況,你可以做類似如下:

from hypothesis.strategies import builds, lists, integers 

def prune_list(ls): 
    n = len(ls) 
    return [ 
     [i for i in sublist if i < n][:5] 
     for sublist in ls 
    ] 

limited_list_strategy = builds(
    prune_list, 
    lists(lists(integers(0, 49), average_size=5), max_size=50, min_size=1) 
) 

在此我們:

  1. 生成看起來大致正確的列表(它是整數,列表清單整數與可能的指數可能有效的範圍相同)。
  2. 從子列表修剪出任何無效的指數
  3. 截斷仍然在他們超過5元

結果應滿足你所需要的全部三個條件的任何子列表。

average_size參數不是嚴格必要的,但在實驗中,我發現它有點太容易產生空的子列表,否則。

ETA:道歉。我剛剛意識到我誤解了你的一個條件 - 這實際上並不是你想要的,因爲它不能確保每個列表的長度都是相同的。下面是修改此來修復的方式(它變得更爲複雜一點,所以我轉向使用替代的複合版本):

from hypothesis.strategies import composite, lists, integers, permutations 


@composite 
def limisted_lists(draw): 
    ls = draw(
     lists(lists(integers(0, 49), average_size=5), max_size=50, min_size=1) 
    ) 
    filler = draw(permutations(range(50))) 
    sublist_length = draw(integers(0, 5)) 

    n = len(ls) 
    pruned = [ 
     [i for i in sublist if i < n][:sublist_length] 
     for sublist in ls 
    ] 

    for sublist in pruned: 
     for i in filler: 
      if len(sublist) == sublist_length: 
       break 
      elif i < n: 
       sublist.append(i) 
    return pruned 

的想法是,我們生成一個「填充物」名單提供子列表看起來像什麼樣子的缺省值(所以他們會傾向於縮小與彼此更接近的方向),然後繪製子列表的長度以修剪以獲得一致性。

我承認這很複雜。您可能需要使用RecursivelyIronic的基於flatmap的版本。我更喜歡這個的主要原因是它會趨於更好的縮小,所以你會得到更好的例子。

+1

太好了,我明白了!很高興能有這樣一個真棒圖書館幫助SO的作者,非常感謝答案和假設本身 - 這是一件傑作! – alecxe

+1

非常歡迎。很高興你在享受圖書館。 :-) – DRMacIver

4

你也可以用flatmap做到這一點,儘管它有點扭曲。

from hypothesis import strategies as st 
from hypothesis import given, settings 

number_of_lists = st.integers(min_value=1, max_value=50) 
list_lengths = st.integers(min_value=0, max_value=5) 

def build_strategy(number_and_length): 
    number, length = number_and_length 
    list_elements = st.integers(min_value=0, max_value=number - 1) 
    return st.lists(
     st.lists(list_elements, min_size=length, max_size=length), 
     min_size=number, max_size=number) 

mystrategy = st.tuples(number_of_lists, list_lengths).flatmap(build_strategy) 

@settings(max_examples=5000) 
@given(mystrategy) 
def test_constraints(list_of_lists): 
    N = len(list_of_lists) 

    # condition 1 
    assert 1 <= N <= 50 

    # Condition 2 
    [length] = set(map(len, list_of_lists)) 
    assert 0 <= length <= 5 

    # Condition 3 
    assert all((0 <= element < N) for lst in list_of_lists for element in lst) 

正如David所說,這確實會產生很多空列表,所以需要一些平均大小的調整。

>>> mystrategy.example() 
[[24, 6, 4, 19], [26, 9, 15, 15], [1, 2, 25, 4], [12, 8, 18, 19], [12, 15, 2, 31], [3, 8, 17, 2], [5, 1, 1, 5], [7, 1, 16, 8], [9, 9, 6, 4], [22, 24, 28, 16], [18, 11, 20, 21], [16, 23, 30, 5], [13, 1, 16, 16], [24, 23, 16, 32], [13, 30, 10, 1], [7, 5, 14, 31], [31, 15, 23, 18], [3, 0, 13, 9], [32, 26, 22, 23], [4, 11, 20, 10], [6, 15, 32, 22], [32, 19, 1, 31], [20, 28, 4, 21], [18, 29, 0, 8], [6, 9, 24, 3], [20, 17, 31, 8], [6, 12, 8, 22], [32, 22, 9, 4], [16, 27, 29, 9], [21, 15, 30, 5], [19, 10, 20, 21], [31, 13, 0, 21], [16, 9, 8, 29]] 
>>> mystrategy.example() 
[[28, 18], [17, 25], [26, 27], [20, 6], [15, 10], [1, 21], [23, 15], [7, 5], [9, 3], [8, 3], [3, 4], [19, 29], [18, 11], [6, 6], [8, 19], [14, 7], [25, 3], [26, 11], [24, 20], [22, 2], [19, 12], [19, 27], [13, 20], [16, 5], [6, 2], [4, 18], [10, 2], [26, 16], [24, 24], [11, 26]] 
>>> mystrategy.example() 
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] 
>>> mystrategy.example() 
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []] 
>>> mystrategy.example() 
[[6, 8, 22, 21, 22], [3, 0, 24, 5, 18], [16, 17, 25, 16, 11], [2, 12, 0, 3, 15], [0, 12, 12, 12, 14], [11, 20, 6, 6, 23], [5, 19, 2, 0, 12], [16, 0, 1, 24, 10], [2, 13, 21, 19, 15], [2, 14, 27, 6, 7], [22, 25, 18, 24, 9], [26, 21, 15, 18, 17], [7, 11, 22, 17, 21], [3, 11, 3, 20, 16], [22, 13, 18, 21, 11], [4, 27, 21, 20, 25], [4, 1, 13, 5, 13], [16, 19, 6, 6, 25], [19, 10, 14, 12, 14], [18, 13, 13, 16, 3], [12, 7, 26, 26, 12], [25, 21, 12, 23, 22], [11, 4, 24, 5, 27], [25, 10, 10, 26, 27], [8, 25, 20, 6, 23], [8, 0, 12, 26, 14], [7, 11, 6, 27, 26], [6, 24, 22, 23, 19]] 
+0

單獨感謝您的額外明確的評論和斷言!完美的作品! – alecxe

1

姍姍來遲,但對於後人:最簡單的解決方案是挑選尺寸,然後從元素策略中建立起來。

from hypothesis.strategies import composite, integers, lists 

@composite 
def complicated_rectangles(draw, max_N): 
    list_len = draw(integers(1, max_N)) 
    sublist_len = draw(integers(0, 5)) 
    element_strat = integers(0, min(list_len, 5)) 
    sublist_strat = lists(
     element_strat, min_size=sublist_len, max_size=sublist_len) 
    return draw(lists(
     sublist_strat, min_size=list_len, max_size=list_len))