News for hackers. Stuff that matters.
Re:Specific ways to write better Python (2017) by softwaredoug on Saturday June 20, 2020 @01:47PM (#23585304)
On “return generators instead of lists” I think it’s also important when you do this to return use a context manager to manage the generators lifetime. Otherwise you might be surprised when the underlying file or source is closed later in your code.

I blogged about this: https://opensourceconnections.com/blog/2020/06/10/python-gen...

Re:Specific ways to write better Python (2017) by xapata on Saturday June 20, 2020 @02:05PM (#23585468)
Maybe I misunderstood, but if you're suggesting the generator function open/close the resource, then I disagree. The caller has the responsibility of safely opening and closing. The generator function should not care what type of iterable it receives
Re:Specific ways to write better Python (2017) by softwaredoug on Saturday June 20, 2020 @05:06PM (#23587305)
Then the caller must know they are receiving a generator dependent on the files lifetime. Pretty brittle IMO. Receiving a list wouldn’t depend on the files lifetime. This causes client code to be pretty error prone as you can very easily think you’re saving off a list built from a file, but in reality you hold a generator. Client code closes the file, the generator is used much later, and blammo, crash! NOT doing a context manager forces clients to work harder at guessing lifetimes.

So you can’t just replace lists with generators.

This break the uniform access principal. Client code IMO shouldn’t need to think about whether they got a generator or an interable container.

Re:Specific ways to write better Python (2017) by xapata on Sunday June 21, 2020 @04:35AM (#23590705)
> Then the caller must know they are receiving a generator dependent on the files lifetime

How would they not? The caller provides the file as an argument to the generator function. Further, the caller decides when to close the file, presumably via leaving a context and after the generator is consumed.

Re:Specific ways to write better Python (2017) by Groxx on Monday June 22, 2020 @02:55PM (#23604263)
it's not too hard to write code like:

  with open(...) as f:
    lines = process(f)

  # do stuff with lines
^ if `process` returns a generator, the file will be closed without having done anything.
Re:Specific ways to write better Python (2017) by xapata on Monday June 22, 2020 @03:01PM (#23604409)
Put `do stuff with lines` inside the context. Where it belongs.
Re:Specific ways to write better Python (2017) by Groxx on Monday June 22, 2020 @11:38PM (#23609682)
and hold open a resource longer than necessary? or when looping over lines multiple times? it doesn't unambiguously belong in there. often, yes, but not always.
Re:Specific ways to write better Python (2017) by hansvm on Tuesday June 23, 2020 @01:27PM (#23617208)
As the caller you can always choose to write lines=list(process(f)) if your use case requires it. If process(f) returned a list though, you wouldn't be able to avoid the potentially large memory allocation if you only needed to iterate over the results.
Re:Specific ways to write better Python (2017) by Groxx on Wednesday June 24, 2020 @11:58PM (#23636768)
Sure. It's entirely possible.

Do you `list(...)` literally everywhere you want a list, whether or not it's currently a list? Extreme-defensive-programming like that tends to be extremely rare in my experience. Not nonexistent, but I'd be willing to bet that if you took a random stack overflow or github line of code, it wouldn't do this.

Re:Specific ways to write better Python (2017) by hansvm on Thursday June 25, 2020 @06:32PM (#23646525)
Defensive programming wasn't meant to be the takeaway.

Rather, if you need a list and the otherwise perfect library method `foo` returns an iterator then you as the caller have almost no downside in writing `list(foo())` instead.

On the flip-side, if you need an iterator (e.g. because of memory concerns) and the otherwise perfect library method `foo` returns a list then you as the caller have no options other than re-implementing `foo` or finding another tool to solve your problem.

If your project usually requires lists rather than iterators then the syntactic burden of wrapping everything in `list(...)` could be annoying, and I wouldn't be at all surprised to find that performance-critical code couldn't tolerate the iterator overhead (though I'd posit that most of the time just using a list instead probably wouldn't fix it), but using iterators rather than lists seems like a good default if you don't have a good reason not to.

Re:Specific ways to write better Python (2017) by Groxx on Friday June 26, 2020 @06:00PM (#23657555)
Isn't that just the opposite tradeoff for code you call though?

E.g. lists are strictly more flexible than iterators since they are multi-pass and can index. If you take a return from lib func A and pass it to lib funcs B and C (possibly in a different lib), if A returns an iterator then you need to know what B and C are going to do with it. If A returns a list, you do not - B and C cannot affect each other in this way unless they directly mutate the argument, which is relatively rare and usually has very strong documentation and/or naming patterns to make it not surprising. And B and C and all their calls can change without you needing to change your code between them and A.

You can of course `list(A())` before passing it to B and C, but that's arguably defensive programming unless you know A returns an iterator.

tbh I'd say I see multi-iteration and indexing several times more often than I see iterator-only use in most code, and almost never see memory issues except in large or embedded systems. Gigabytes of RAM are standard now, and dumping a few thousand files into memory before processing will quite often out-perform a more memory-efficient streaming approach. In the rare cases where you do exceed those memory bounds, you're likely in a niche area (gigabytes of files, tiny memory space, significant computation cost) and are likely using niche-oriented libraries that benefit more from coordinating tightly than from being perfectly general.

Re:Specific ways to write better Python (2017) by xapata on Friday June 26, 2020 @06:38PM (#23657847)
> never see memory issues

In contrast, I see them all the time (damn you, Pandas). Beyond out-of-memory errors, generators can be vastly more compute-efficient because of the CPU cache.

Re:Specific ways to write better Python (2017) by Groxx on Friday June 26, 2020 @10:00PM (#23658915)
Pandas I'd definitely categorize as being niche :) A moderately-sized one, but still a niche. And there you use whatever performs the way you need it, yeah - there are special constraints on the system, and pandas is built around dealing with them.
Re:Specific ways to write better Python (2017) by hansvm on Friday June 26, 2020 @10:19PM (#23658997)
Haha, that one hits close to home. Iirc Pandas was the author's first big project, and memory use is one of the key things he wants to improve if he gives it another go. It's still an impressive piece of software.
Re:Specific ways to write better Python (2017) by xapata on Thursday June 25, 2020 @06:06PM (#23646313)
No. I put that in places where I receive a generator and want a list.

But, there are plenty of instances of what you're talking about. Take a look at the standard library. Itertools often creates an iterator as the first operation, not knowing whether it received an iterator or some other kind of iterable.

https://github.com/python/cpython/blob/384621c42f9102e31ba2c...

If you search for `list` in the CPython code, you'll find many examples of what you're asking about.

Similarly, Pandas often makes a copy of a Series or DataFrame. It might be inefficient, but it keeps the API (more) simple.

Re:Specific ways to write better Python (2017) by cheez on Saturday June 20, 2020 @02:41PM (#23585796)
I agree, this unnecessarily complicates library-type code.
Re:Specific ways to write better Python (2017) by ehsankia on Saturday June 20, 2020 @08:25PM (#23588623)
Why not just have the with-statement/filehandler inside `judgments_from_file`? You're doing all sorts of messy stuff to make sure the file closes whenever `judgments_from_file` is done, wouldn't it make sense to just have it control the lifecycle of the file?

The only reason I can think of to do it the other way is if you want to make unit testing easier, allowing you to pass a fake iterable instead of a file pointer to `judgments_from_file` for testing.

Re:Specific ways to write better Python (2017) by softwaredoug on Saturday June 20, 2020 @09:07PM (#23588827)
I think that’s also valid...

What you’re describing is more or less what judgments_open does. I just did it with a context manager. But you’re right there are other ways to safely manage the files lifetime.

I also use this pattern for writing judgments, which has work to do when done judgments are all collected and the context exits.

Re:Specific ways to write better Python (2017) by carapace on Saturday June 20, 2020 @03:01PM (#23586010)
Long-time pythonista here. I read this the other day and, meaning no disrespect, I think you're off-base on this but I can't quite put my finger on it.
Re:Specific ways to write better Python (2017) by perrygeo on Saturday June 20, 2020 @05:40PM (#23587568)
Care to elaborate or provide an example? Also long-time pythonista here and I find the article's recommendations spot on. There is huge difference between returning values (a concrete list) versus returning a stateful object that yields values (a generator). Yet Python's syntax allows you to treat them as equivalent and it can lead to bugs exactly as described. A context manager is the Pythonic way to deal with stateful objects and it ensures that you manage the lifecycle of the object explicitly, thus avoiding a whole class of potential bugs in your code.

How would you handle the problem? Or do you not see it as a problem?

Re:Specific ways to write better Python (2017) by carapace on Saturday June 20, 2020 @07:56PM (#23588473)
Well for one thing, I think as written the solution code is buggy:

    with judgments_open('judgments.txt') as judgments
        gather_features(judgments)
        train_model(judgments)  # <-- bug.
The context manager is still returning a single generator, not a list, so when you call train_model(judgments) it will have been exhausted, no?

- - - -

Second, the bug is not really solved by the proposed solution.

This code is fine.

    with open('judgments.txt') as f:
        judgments = judgments_from_file(f)
        for j in judgments:
            process(j)
This code is buggy.

    with open('judgments.txt') as f:
        judgments = judgments_from_file(f)

    for j in judgments:  # <-- bug.
        process(j)
The generator must not be used after the file is closed at the end of the context manager's scope.

If you did this you would still have the bug:

    with judgments_open('judgments.txt') as judgments
        foo(judgments)  # okay

    for j in judgments:  # <-- same bug.
        process(j)

In any event, in this case, I would just do:

    with open('judgments.txt') as f:
        judgments = list(judgments_from_file(f))
        gather_features(judgments)
        train_model(judgments)
Re:Specific ways to write better Python (2017) by ehsankia on Saturday June 20, 2020 @08:23PM (#23588617)
I'm curious, personally I would just put the file handling inside the function handling the file. Is anything wrong with the following much simpler solution?

    def judgments_from_file(filename):
      with open(filename) as f:
        for line in f:
          yield parse_row(line)
Why wouldn't you let the method that deals with the file also own the file pointer, and then the context manager from the with-statement close it whenever the function is done? Instead of having to do some magical stuff to make sure it closes at the right time? Is it to make unittest/mocking easier?

    for judgements in judgments_from_file(filename):
      for j in judgements:
        process(j)
Re:Specific ways to write better Python (2017) by carapace on Saturday June 20, 2020 @10:38PM (#23589293)
Oh hey, that's a little nicer, isn't it? Cheers
Re:Specific ways to write better Python (2017) by softwaredoug on Saturday June 20, 2020 @08:14PM (#23588575)
First one, you’re right that’s a bug. I should fix that in the article (and it’s another way generators and lists aren’t interchangeable)

On saving off the judgements var, I think I disagree that’s as much an issue. “With” is the languages feature explicitly designated for safely managing resources/variable lifetimes. One could save a file off created using with in the same way. The language basically tells you doing that with a var created with “with” is a bad idea. The articles solution makes lifetime of the generator more explicit.

Re:Specific ways to write better Python (2017) by carapace on Sunday June 21, 2020 @12:34PM (#23593049)
> The articles solution makes lifetime of the generator more explicit.

Does it? What's the difference between these two snippets?

    with judgments_open('judgments.txt') as judgments
        gather_features(judgments)

    with open('judgments.txt') as f:
        gather_features(judgments_from_file(f))
You still have to complete all the work within the scope of the context manager either way, right?

The code ehsankia presented combines the generator with a closure that includes the context manager, and automatically carries the scope of the context manager along with it, so that when the generator is exhausted the context manager's scope also ends.

Re:Specific ways to write better Python (2017) by heavyset_go on Saturday June 20, 2020 @05:52PM (#23587652)
I have to agree with this sentiment.