簡體   English   中英

在不列出每個塊的情況下解析可迭代對象

[英]Parsing an iterable without listifying each chunk

假設我想實現 Python 可迭代對象的拆分,而不列出每個塊,類似於itertools.groupby ,其塊是惰性的。 但我想在比鍵相等更復雜的條件下進行。 所以更像是一個解析器。

例如,假設我想在一個可迭代的整數中使用奇數作為分隔符。 比如more_itertools.split_at(lambda x: x % 2 == 1, xs) (但是more_itertools.split_at列出了每個塊。)

在解析器組合器語言中,這可能稱為sepBy1(odd, many(even)) 在 Haskell 中,有解決此類問題的Parsecpipes-parsepipes-group庫。 例如,從 Pipes.Group 編寫itertools.groupby版本的groupsBy'就足夠了,也很有趣(請參閱此處)。

可能有一些聰明的柔術與itertools.groupby ,也許應用itertools.pairwise ,然后itertools.groupby ,然后回到單個元素。

我想我可以自己將它寫成一個生成器,但是在 Python(下面)中編寫itertools.groupby已經相當復雜了。 也不容易推廣。

似乎應該有更普遍的東西,比如為任何類型的流編寫解析器和組合器的相對輕松的方式。

# From https://docs.python.org/3/library/itertools.html#itertools.groupby
# groupby() is roughly equivalent to:
class groupby:
    # [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B
    # [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D
    def __init__(self, iterable, key=None):
        if key is None:
            key = lambda x: x
        self.keyfunc = key
        self.it = iter(iterable)
        self.tgtkey = self.currkey = self.currvalue = object()
    def __iter__(self):
        return self
    def __next__(self):
        self.id = object()
        while self.currkey == self.tgtkey:
            self.currvalue = next(self.it)    # Exit on StopIteration
            self.currkey = self.keyfunc(self.currvalue)
        self.tgtkey = self.currkey
        return (self.currkey, self._grouper(self.tgtkey, self.id))
    def _grouper(self, tgtkey, id):
        while self.id is id and self.currkey == tgtkey:
            yield self.currvalue
            try:
                self.currvalue = next(self.it)
            except StopIteration:
                return
            self.currkey = self.keyfunc(self.currvalue)

這里有幾個簡單的迭代器拆分器,是我在無聊中寫的。 我不認為它們特別深刻,但也許它們會以某種方式提供幫助。

我沒有花很多時間考慮有用的界面、優化或實現多個交互子功能。 如果需要,可以添加所有這些東西。

這些基本上是模仿itertools.groupby的,其界面可能被認為有點奇怪。 這是 Python 實際上不是函數式編程語言的結果。 Python 的生成器(以及實現迭代器協議的其他對象)是有狀態的,並且沒有用於保存和恢復生成器 state 的工具。因此函數確實返回一個迭代器,該迭代器連續生成迭代器,迭代器從原始迭代器產生值。 但是返回的迭代器共享底層的迭代器,它是傳遞給原始調用的迭代器,這意味着當你推進外部迭代器時,當前內部迭代器中任何未使用的值都會被丟棄,恕不另行通知。

有(相當昂貴的)方法可以避免丟棄這些值,但由於最明顯的方法 --listifying-- 從一開始就被排除在外,盡管准確記錄行為很尷尬,但我還是使用了groupby界面。 可以使用itertools.tee包裝內部迭代器,以使原始迭代器獨立,但代價類似於(或可能略高於)列表化。 它仍然要求每個子迭代器在下一個子迭代器啟動之前完全生成,但它不需要在開始使用值之前完全生成子迭代器。

為簡單起見(根據我的說法:-)),我將這些函數實現為生成器而不是對象,就像itertoolsmore_itertools 外部生成器生成每個連續的子迭代器,然后在生成下一個子迭代器之前收集並丟棄其中的所有剩余值 [注 1]。 我想大多數時候子迭代器會在外循環嘗試刷新它之前完全耗盡,所以額外的調用會有點浪費,但它比你為itertools.groupby引用的代碼簡單。

仍然有必要從子迭代器傳回原始迭代器已耗盡的事實,因為這不是您可以詢問迭代器的事情。 我使用nonlocal聲明在外部和內部生成器之間共享 state。 在某些方面,將 state 保留在nonlocal中,如itertools.groupby所做的那樣,可能更靈活,甚至可能被認為更符合 Pythonic,但 nonlocal 對我有用。

我實現more_itertools.split_at (沒有maxsplitskeep_separator選項),我認為等同於Pipes.Groups.groupBy' ,重命名為split_between以指示它在滿足某些條件的兩個連續元素之間拆分。

請注意, split_between總是在運行第一個子迭代器請求它之前從提供的迭代器強制第一個值。 值的 rest 是延遲生成的。 我嘗試了幾種方法來推遲第一個 object,但最終我還是采用了這種設計,因為它要簡單得多。 結果是split_at不執行初始力,它總是至少返回一個子迭代器,即使提供的參數為空,而split_between則不然。 我必須嘗試這兩種方法來解決一些實際問題,才能決定我更喜歡哪個界面; 如果您有偏好,一定要表達出來(但不保證會改變)。

from collections import deque

def split_at(iterable, pred=lambda x:x is None):
    '''Produces an iterator which returns successive sub-iterations of 
       `iterable`, delimited by values for which `pred` returns
       truthiness. The default predicate returns True only for the
       value None.

       The sub-iterations share the underlying iterable, so they are not 
       independent of each other. Advancing the outer iterator will discard
       the rest of the current sub-iteration.

       The delimiting values are discarded.
    '''

    done = False
    iterable = iter(iterable)

    def subiter():
        nonlocal done
        for value in iterable:
            if pred(value): return
            yield value
        done = True

    while not done:
        yield (g := subiter())
        deque(g, maxlen=0)

def split_between(iterable, pred=lambda before,after:before + 1 != after):
    '''Produces an iterator which returns successive sub-iterations of 
       `iterable`, delimited at points where calling `pred` on two
       consecutive values produces truthiness. The default predicate
       returns True when the two values are not consecutive, making it
       possible to split a sequence of integers into contiguous ranges.

       The sub-iterations share the underlying iterable, so they are not 
       independent of each other. Advancing the outer iterator will discard
       the rest of the current sub-iteration.
    '''
    iterable = iter(iterable)

    try:
        before = next(iterable)
    except StopIteration:
        return

    done = False

    def subiter():
        nonlocal done, before
        for after in iterable:
            yield before
            prev, before = before, after
            if pred(prev, before):
                return

        yield before
        done = True

    while not done:
        yield (g := subiter())
        deque(g, maxlen=0)

筆記

  1. 我認為collections.deque(g, maxlen=0)目前是丟棄迭代器剩余值的最有效方法,盡管它看起來有點神秘。 感謝more_itertools向我指出了該解決方案,以及用於計算生成器生成的對象數量的相關表達式:
     cache[0][0] if (cache:= deque(enumerate(it, 1), maxlen=1)) else 0
    盡管我並不是要將上述怪異行為歸咎於more_itertools (他們用if語句來做,而不是海象。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM