Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a version of str.split which returns an iterator #61545

Closed
alex opened this issue Mar 4, 2013 · 20 comments
Closed

Add a version of str.split which returns an iterator #61545

alex opened this issue Mar 4, 2013 · 20 comments
Labels
3.7 (EOL) end of life type-feature A feature request or enhancement

Comments

@alex
Copy link
Member

alex commented Mar 4, 2013

BPO 17343
Nosy @birkenfeld, @rhettinger, @terryjreedy, @gpshead, @giampaolo, @alex, @serhiy-storchaka, @apalala, @uwinx
Files
  • main.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2017-03-07.18:43:11.881>
    created_at = <Date 2013-03-04.01:04:26.565>
    labels = ['type-feature', '3.7']
    title = 'Add a version of str.split which returns an iterator'
    updated_at = <Date 2021-02-26.15:28:56.200>
    user = 'https://github.com/alex'

    bugs.python.org fields:

    activity = <Date 2021-02-26.15:28:56.200>
    actor = 'apalala'
    assignee = 'none'
    closed = True
    closed_date = <Date 2017-03-07.18:43:11.881>
    closer = 'serhiy.storchaka'
    components = []
    creation = <Date 2013-03-04.01:04:26.565>
    creator = 'alex'
    dependencies = []
    files = ['49837']
    hgrepos = []
    issue_num = 17343
    keywords = []
    message_count = 20.0
    messages = ['183411', '183414', '183423', '183498', '183501', '183528', '183529', '183782', '183830', '186104', '186105', '186106', '186160', '186192', '186203', '186213', '281537', '384280', '387721', '387728']
    nosy_count = 12.0
    nosy_names = ['georg.brandl', 'rhettinger', 'terry.reedy', 'gregory.p.smith', 'giampaolo.rodola', 'alex', 'santoso.wijaya', 'tshepang', 'serhiy.storchaka', 'apalala', 'Pawe\xc5\x82 Miech', 'uwinx']
    pr_nums = []
    priority = 'normal'
    resolution = 'rejected'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue17343'
    versions = ['Python 3.7']

    @alex
    Copy link
    Member Author

    alex commented Mar 4, 2013

    str.split returns a list, which is inefficient when you just want to process items one be one. You could emulate this with str.find and tracking indexes manually, but this should really be a builtin behavior.

    @alex alex added the type-feature A feature request or enhancement label Mar 4, 2013
    @gpshead
    Copy link
    Member

    gpshead commented Mar 4, 2013

    The bytes (and bytearray?) version of this should generate memoryview's instead of new bytes objects to avoid a copy.

    While not required, It'd be useful if the implementation of this pre-scanned the data internally so that the length of the generated sequence was known up front. This could imply an internal bitset of vector of split indices is kept for the life of the generator (implementation detail left up to the implementor) if scanning over the input data more than once is undesirable.

    Start with a pure python proof of concept to work everywhere, then write a native version.

    @serhiy-storchaka
    Copy link
    Member

    While not required, It'd be useful if the implementation of this pre-scanned the data internally so that the length of the generated sequence was known up front. This could imply an internal bitset of vector of split indices is kept for the life of the generator (implementation detail left up to the implementor) if scanning over the input data more than once is undesirable.

    bytearray can be modified between iterations.

    @gpshead
    Copy link
    Member

    gpshead commented Mar 4, 2013

    Indeed, a bytearray version would require the talked about but not implemented due to complexity (in PEP-3118) support for locking a buffer from other mutations. best concentrate on bytes then.

    Do we have a memoryview equivalent for PyUnicode? If not, we should... (a separate enhancement request)

    @brettcannon
    Copy link
    Member

    There is no string view that I know of. Interesting idea, though, thanks to the immutability of strings. Would much have to be different other than boundary checking and __hash__ (and hoping extension authors are changing things in-place)?

    I say go ahead and open the issue for the idea.

    @serhiy-storchaka
    Copy link
    Member

    Indeed, a bytearray version would require the talked about but not implemented due to complexity (in PEP-3118) support for locking a buffer from other mutations.

    I rather think that a bytearray version can't pre-scan the data. Note that an array for pre-scanned result can be larger than input data (if we split into a large number of small items). Also note that iterative split useful when we do not want to process all input, but only several first items.

    Actually I think that in most common cases non-iterative split will be faster than iterative one.

    @serhiy-storchaka
    Copy link
    Member

    There is no string view that I know of. Interesting idea, though, thanks to the immutability of strings. Would much have to be different other than boundary checking and __hash__ (and hoping extension authors are changing things in-place)?

    Objects/stringlib/unicode_format.h contains internal structure SubString which can be taken as a basis. But it is unlikely that it will be useful. All API which accept strings will require converting substring views to regular strings. And substring object can consume more memory than full string. This looks like a step backwards from PEP-393.

    @terryjreedy
    Copy link
    Member

    I personally would have changed both str.split and os.walk to return iterators in 3.0, like many other builtins. The rationale for os.walk continuing to produce a list is that there would be little time saving as the list is not *that* long and most uses look at all the items anyway. (See tracker issue.) This argument might be even stronger for str.split.

    When I expressed my view about str.split (after 3.0, I think), Guido said that if people do not look at all the items, they typically need random access, and hence a list anyway, so why make them write list(xxx.split()) all the time?

    I will also note that Guido has opposed string views and partial object views in general on the basis that a tiny view can keep a mega object alive, and that it is too much to ask people to keep track of when they should copy the view to release the underlying object.

    Iterators also (should) keep the underlying object alive, but they are usually short-lived and not passed around hither and thither.

    I personally would prefer that the API for this proposal simply be a new parameter (iter(able)=False/True). But that runs into 'argument values should not change the return type' (but lists and iterators are *both iterables*!). Instead there supposedly should be a new, almost identical function with a new, almost identical name. But that runs into 'we should not add builtins with a really good reason' (which I agree with) and more directly 'we should not repeat the confusing range/xrange mess' (which I also agree with). So the status quo is a Catch 22 situation with conflicting principles that produce paralysis. As I said, I prefer redefining the return as an iterable.

    @gpshead
    Copy link
    Member

    gpshead commented Mar 9, 2013

    It'd perhaps have been better if things like memoryview were never exposed to the user at all as a distinct type and became an internal implementation detail behind PyBytes and PyUnicode objects (they could hold a reference to something else or collapse that down to their own copy on their own terms, up to the particulars of the Python VM).

    Anyways, the above is getting off topic for this issue. I retract my memoryview suggestion; that belongs in its own issue.

    An iterating version of str.split is indeed hard to add today IFF we are against a str.itersplit() method name or against an optional keyword only argument that'd cause split(iterator=True)'s return type to potentially be different.

    @rhettinger
    Copy link
    Contributor

    -1 on os.walk returning an iterator. The API is already a bit challenging for some and our experience with itertools.groupby() is that returning an inner iterator can be very confusing.

    @alex
    Copy link
    Member Author

    alex commented Apr 5, 2013

    Raymond: Is that for the wrong ticket, or was the message incorrect? :)

    @rhettinger
    Copy link
    Contributor

    Alex, it was response to Terry's message: http://bugs.python.org/issue17343#msg183782

    FWIW, I'm +1 on an iterator version of str.split().

    I'm not sure yet that it would be worthwhile to propagate the idea to other string-like objects though.

    @rhettinger
    Copy link
    Contributor

    If someone wants whip-up a patch for str.iter_index(), I would be happy to review it. Be sure to add a test case to make sure that the results are non-overlapping: list('aaaa'.iter_index('aa')) == [0, 2]

    @rhettinger rhettinger self-assigned this Apr 6, 2013
    @birkenfeld
    Copy link
    Member

    I'm guessing Terry wanted to say "os.listdir" instead of "os.walk".

    @serhiy-storchaka
    Copy link
    Member

    May be str.iter_indices() or even just str.indices()?

    @terryjreedy
    Copy link
    Member

    I'm guessing Terry wanted to say "os.listdir" instead of "os.walk".
    yes, sorry.

    @rhettinger
    Copy link
    Contributor

    No one has submitted a patch for this or has expressed an interest in a long time. Perhaps the use case is already served by re.finditer()

    Unassigning. Feel free to push this forward or to close due to lack on interest.

    @rhettinger rhettinger added the 3.7 (EOL) end of life label Nov 23, 2016
    @rhettinger rhettinger removed their assignment Nov 23, 2016
    @uwinx
    Copy link
    Mannequin

    uwinx mannequin commented Jan 3, 2021

    Perhaps the use case is already served by re.finditer()

    def split_whitespace_ascii(s: str):
        return (pt.group(0) for pt in re.finditer(r"[A-Za-z']+", s))

    solution above does not cover all possible data and is incorrect for bytes-like objects.

    writing regular expressions for different separators/data would be a quite overheadish, so the idea of one-case solution doesn't seem to go very far and requires a bigger change in code for different separators.

    let's try to revive this one :)

    @PaweMiech
    Copy link
    Mannequin

    PaweMiech mannequin commented Feb 26, 2021

    Making string.split iterator sounds like an interesting task. I found this issue because recently we talked in project that string.split returns a list and it can cause increased memory usage footprint for some tasks when there is large response to parse.

    Here is small script, created by my friend Juancarlo Anez, with iterator version of string.split. Compared with default string split it uses much less memory. When running with memory-profiler tool: https://pypi.org/project/memory-profiler/

    It creates this output
    3299999
    Filename: main.py

    Line # Mem usage Increment Occurences Line Contents
    ============================================================
    24 39.020 MiB 39.020 MiB 1 @Profile
    25 def generate_string():
    26 39.020 MiB 0.000 MiB 1 n = 100000
    27 49.648 MiB 4.281 MiB 100003 long_string = " ".join([uuid.uuid4().hex.upper() for _ in range(n)])
    28 43.301 MiB -6.348 MiB 1 print(len(long_string))
    29
    30 43.301 MiB 0.000 MiB 1 z = isplit(long_string)
    31 43.301 MiB 0.000 MiB 100001 for line in z:
    32 43.301 MiB 0.000 MiB 100000 continue
    33
    34 52.281 MiB 0.297 MiB 100001 for line in long_string.split():
    35 52.281 MiB 0.000 MiB 100000 continue

    You can see that default string.split uses much more memory.

    @apalala
    Copy link
    Mannequin

    apalala mannequin commented Feb 26, 2021

    def isplit(text, sep=None, maxsplit=-1):
        """
        A lowmemory-footprint version of:
    
            iter(text.split(sep, maxsplit))
    Adapted from https://stackoverflow.com/a/9770397
    """
    
        if maxsplit == 0:
            yield text
        else:
            rsep = re.escape(sep) if sep else r'\s+'
            regex = fr'(?:^|{rsep})((?:(?!{rsep}).)*)'
    
            for n, p in enumerate(re.finditer(regex, text)):
                if 0 <= maxsplit <= n:
                    yield p.string[p.start(1):]
                    return
                yield p.group(1)

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.7 (EOL) end of life type-feature A feature request or enhancement
    Projects
    None yet
    Development

    No branches or pull requests

    7 participants