8 ms·
I'm annoyed at the reason that any/all have to be on this list. If they (and map, filter, …) were methods, you could just write `foo.` and your IDE could show y
by skitter 3y ago
I'm annoyed at the reason that any/all have to be on this list. If they (and map, filter, …) were methods, you could just write `foo.` and your IDE could show you what methods are available. Postfix would make things easier to read too:
bar.baz()\
.filter(some_filter)\
.map(some_op)\
.min()\
.foo()
Data/control flows from top to bottom. One operation per line. But with freestanding functions:
min(map(some_op, filter(some_filter, bar.baz()))).foo()
To follow the flow of data/control, you start in the middle, go right, then skip left to filter, read rightwards to see which filter, skip left to map, read rightwards to see what map, go left to min, then skip all the way to the right. Just splitting it into multiple lines doesn't help, you need to introduce intermediate variables (and make sure they don't clobber any existing ones) and repeat yourself whether they clarify things or not. The same issue exists for list/dict/set comprehensions.
- KMnO4 3y agoWould that really work? You can chain those functions because they return the same type. For example, filtering a list returns a subset of the list. Any/all return a Boolean, so the chain would stop there. I also personally think any(x % 5 in range(y)) Is more clear than range(y).any(lambda x: x % 5)
- zmmmmm 3y agoit's interesting I completely agree with you and it's a big reason I find Python irritating to write (compared to Groovy, Kotlin, Ruby, etc). However there do seem to be a lot of people that dislike this method chaining style and will assert that functional style is better in every way. But I just can't fundamentally agree that writing these as functions is as readable. Even if you go far out of your way to format it similarly, it still forces you to do a lot of mental work to see the inner most starting point and then deduce what the sequence of operations that happens is backwards, eg: foo( min( map(lambda x: ..., filter(lambda: y: ...., baz(bar) ) ) ) (and of course, the python linters are typically configured to hate this so you can't realistically write it this way even if you want to)
- llwu 3y agoThere is a niche use-case for the reverse order `(foo min map filter baz bar)`, which is, solving typed holes (you could refine the hole as like `_.foo()` although that wouldn't be interoperable with things like next token prediction). But that's more of a math thing than an everyday coding thing, where dot chaining usually reads nicer. Mixed is definitely the worst, like you said.
- kragen 3y agoany, all, map, filter, min, max, for loops, zip, list, tuple, reduce, list comprehensions, cycle, repeat, islice, and so on in python work on iterables, and iterable is a protocol, not a class. it would certainly be interesting to program in a language where conforming to a protocol (perhaps one that nobody had thought up yet when you wrote your class) would give your class new methods, or where all iterables had to derive from a common base class, but it would be a very different language from python incidentally in your example, though data does flow from top to bottom, control does not, assuming the filter and map methods are lazy as they are in python; it ping-pongs back and forth up and down the sequence in a somewhat irregular manner, sometimes reaching as far as .min() before going back up, and other times turning around at .filter(...) i wonder if you could implement the ide functionality you want with a 'wrap' menu of popular functions that are applicable to the thing to the left of your cursor, so when you had filter(some_filter, bar.baz())| (with | representing your cursor) you could select `map` or `min` or whatever from the wrap dropdown and get min(filter(some_filter, bar.baz()))| for any given cursor position in python there are potentially multiple expressions ending there, in cases like "y: %s" % y| but maybe that's not such a hard problem to solve
- hwayne 3y ago> it would certainly be interesting to program in a language ... where all iterables had to derive from a common base class, but it would be a very different language from python You mean Ruby? :P (All Ruby iteratables mixin Enumerable, which is baaaaaaaasically inheritance.)
- josephg 3y agoOr Rust! Everything that implements the Iterator trait gets access to all of Iterator’s goodies, like map, filter, reduce, etc. Implementing iterator just requires adding a single next(&mut self) -> Option<Item> method on your type. Lifetimes and async are a massive pain in rust. But the trait system is a work of art.
- kragen 3y ago
- di456 3y agoWith python I'd decompose that one-liner into several variables for readability. That probably ends up using more memory than it would otherwise but I generally don't work on systems where that matters much. Scala was really nice for this syntax when I used it for Spark.
- Jtsummers 3y agoMap and filter don't actually consume anything until they're used later, they produce iterables. So if you pulled them into their own lines they wouldn't consume (much) extra memory. Taking the original: min(map(some_op, filter(some_filter, bar.baz()))).foo() An alternative is also to use a generator comprehension that's identical to the inner part (in effect): min(some_op(item) for item in bar.baz() if some_filter(item)).foo() Which could still be pulled out to a pair of lines for clarity: items = (some_op(item) for item in bar.baz() if some_filter(item)) # or some better name given a context min(items).foo()
- gorgoiler 3y agoYour point about ordering and readability really rang true for me. My way around this in Python is to separate the map and the reduce: do the map in one part with a list comprehension and the reduce in a second part on a new line. I’ll wrap the whole thing in a named function as a way of describing what I’m doing and make it a closure if it’s used only once: def f(bar): def smallest_baz(): bazs = ( some_op(b) for b in bar.baz() if some_filter(b) ) return min(bazs) return smallest_baz().foo()
- ajb117 3y agoMakes me wish python had a pipe operator like Julia's |> and R's %>%
- lordgrenville 3y agoPandas added one for this purpose https://pandas.pydata.org/pandas-docs/dev/reference/api/pandas.Series.pipe.html#pandas.Series.pipe https://pandas.pydata.org/pandas-docs/dev/reference/api/pand...
- JoshuaDavid 3y agoHere you go: class WrappedList: _fns = [map, filter, min, max, all, any, len, list] def __init__(self, it): self.it = it def __getattr__(self, name): for fn in self._fns: if name == fn.__name__: def m(*args, **kwargs): result = fn(*args, self.it, **kwargs) if hasattr(result, '__iter__'): return self.__class__(result) else: return result return m def unwrap(self): return self.it This allows you to do stuff like WrappedList([1, 2, 3, 4]).filter(lambda x: x % 2 == 0).map(lambda x: x * 3).list().unwrap() # [6, 12] WrappedList([1, 2, 3, 4]).map(lambda x: x >= 5).any() # False Deciding whether or not this is something you should do, rather than just something you can do, is left as an exercise for the reader.
- selfmodruntime 3y agoThis reads so much better
- JoshuaDavid 3y agoIf you actually think this code is better there's a real library that does this: https://github.com/EntilZha/PyFunctional https://github.com/EntilZha/PyFunctional.
- kzrdude 3y agoOne puzzling thing is that it uses backslash continuation in its examples. The most favoured style, IMO is to use ()'s for line continuation, maybe the author just doesn't know about those.
- SpikyBlackHard 3y agoPython debugging is most sane when the code just tries to keep it simple. After thousands of pdb sessions I can say most people should not be allowed to do this kind of thing in real code!
- 3y ago
- _ZeD_ 3y agoOr, you know, you could write good'ol for loop and use multiple statements, instead of having a gigantic expression
- saulpw 3y agothose \ are super ugly though
- za3faran 3y agoThis is the same mistake that golang did.
- movpasd 3y agoIn my mind this is a holdover from when Python was much more procedural/C-like and as a Python developer it's one of my pet peeves. (I can't count how many times I've started writing the name of a list, had to backtrack to stick a `len` in front, and then tap tap tap arrow keys to get back to the front.) I suppose we really ought to blame Euler for introducing the f(x) notation 300 years ago... Very practical when the function is the entity you want to focus on, often less useful in (procedural) programming, where we typically start with the data and think in terms of a series of steps. Some languages like D and Nim have "UFCS", uniform function call syntax, where all functions can be called as methods on any variable. Basically, it decouples the implicit association between method dispatch and namespacing/scoping semantics. Rust also has something they call UFCS, but it only goes one way (you can desugar methods as normal functions, but you can't ... resugar? arbitrary functions as methods). Python couldn't implement this without breaking a lot of stuff due to its semantics, but it is definitely a feature I'd like to see more of.
- crabbone 3y ago> and then tap tap tap arrow keys to get back to the front.) Learn a better editor, and this will stop being a problem.
- 0x008 3y ago> If your pavement has potholes, just learn to jump over them.
- crabbone 3y agoNo. That's a wrong analogy. There's no way around having to navigate the text of the program back and forth, by character, by word, by statement, by definition and so on. This is bread and butter of people who write code. If you complain about doing this, this is because you don't know how to perform the basic functions necessary to write code. Heuristically, this is because you are either using a bad editor or didn't learn how to use a decent one. I.e. your complaint is more comparable to Amazon reviews coming from people who don't know how to use the product and then write something asinine, like that one about a loo brush that feels too rough when used in the capacity of toilet paper (though I believe that one was actually a joke inspired by similarly stupid but less funny reviews).
- JimDabell 3y agoYou wouldn’t really write it as you have in the second example though. The Pythonic way of writing something like this is to use list comprehensions or generator expressions, for example: min(some_op(item) for item in bar.baz() if some_filter(item)).foo() Or decomposed a little for clarity: processed_items = (some_op(item) for item in bar.baz() if some_filter(item)) min(processed_items).foo() This is pretty readable – a natural language description of the first line is “do some_op for each item in bar.baz that matches some_filter”, which corresponds 1:1 with the code.
- 0x008 3y agoAnd if you have nested list comprehensions this totally stops making sense somehow.
- thiht 3y agoThis feels somehow even worse. I don’t think it’s more readable than Op’s example, but now it’s also weird to write. The chaining example in OP’s first example is way better
- manvillej 3y agoboth examples seem pretty contrived and I think this comes down just what language you are used to. Their other code seems very JS-y. I work in both python & js. The python reads like natural language: Processed items is a set of some transformation of each item in bar.baz() where something is true for that item. then Foo the smallest in that list. It reads like english. JS-y stuff doesn't read like natural language, but I do think its more concise and fits the IDE function discovery workflow better. Both models can be made into horrid messes or elegant solutions. Both are highly readable. Now I like the python one because I find it natural to attach contextual "whys" or "because" comments to them. Processed items is a set of some transformation of each item in bar.baz() where something is true for that item. then Foo the smallest item. # because foo is a slow function and we don't want to foo every bar and baz
- crabbone 3y agoBoth your suggestions are awful. The reason they are bad is that intermediate results are never named (and thus are never explained). In simple situations, it's possible to infer from the context what the author's intention was, but in more complicated cases, if you want to understand someone's code, especially if it's written in the way you did, you'd have to "disassemble" it into simpler operations, name the variables (after investigating or guessing the purpose of each operation) and then try to come up with the full picture of what's going on. Also, as a style suggestion: avoid using backslashes. In your situation, you could just put dots at the end of the line and it will be enough to not need the backslashes. It adds noise to your code, i.e. characters that add no meaning, just sort of a "scaffolding" to hold your code together.
- gpderetta 3y agoIn my own python toolbox (specifically for the list-of-dictionaries use-case) I inject .log(). calls into the pipeline as need to show what the actual intermediate values would be. Naming intermediates is fine (and encouraged) if there are actually meaningful names to be given. But sometimes the expression itself is the shortest meaningful name for the expression. Re backslashes, you can also just wrap the expression in parentheses.
- zelphirkalt 3y agoA perhaps more appropriate name for your 'log' would be 'peek'. Log reminds of logging, which usually does not return a value, but writes to stdout or a file or similar.
- gpderetta 3y agobut that's exactly what log does, it is a pipeline filter that logs the content to standard output but otherwise it is passthrough.
- nextlevelwizard 3y agofor my smooth brain `map(this, to_that)` makes better sense than `to_this.map(that)` same with give me `min(of_this)` instead of `of_this_want.min()`
- kzrdude 3y agoAgree to a big extent. Rust has lots of methods, because their traits work best or most habitually with methods. So I see a comparison of Rust x.min(y) vs Python min(x, y). The Rust x.min(y) to me is so asymmetric. min(x, y) conveys the symmetry of the operation much better, x and y are both just elements. (And the latter is how it can be used in Python. In Rust, you can call Ord::min(x, y) to get the symmetry back, but it is less favoured right now for some reason.)
- izoow 3y agoInspired by others here, I tried hacking something together myself from functools import partial class Pipeable: def __init__(self, fn): self.fn = fn def __ror__(self, lhs): return self.fn(lhs) def pipeable(fn): return lambda *args: Pipeable(partial(fn, *args)) filter = pipeable(filter) map = pipeable(map) list = pipeable(list) sum = pipeable(sum) min = pipeable(min) max = pipeable(max) any = pipeable(any) # Usage: range(1, 100) | filter(lambda x: x < 50) | max() # 49 [1, 2, 3, 4] | filter(lambda x: x % 2 == 0) | map(lambda x: x * 3) | list() # [6, 12] [1, 2, 3, 4] | map(lambda x: x >= 5) | any() # False
- xcombelle 3y agoHow I eventually resolve this kind of problems. minimum = +Inf for b in bar.naz(): if not some_filter(b): continue b = some_op(b) minimum = min(minimum, b) foo(minimum) Yes, plain old procedural python. data flow from top to bottom. it allows `print` debugging, very usefull to debug some_filter and some_op are broken.