I remember that in the early 00's people called it "executable pseudocode". I bet nobody would use now that expression for modern Python with decorators, comprehensions, walruses and the like.
I remember that in the early 00's people called it "executable pseudocode". I bet nobody would use now that expression for modern Python with decorators, comprehensions, walruses and the like.
Code examples in docs and tutorials are still easy, libs api still clean, code base still readable.
Optional things stayed indeed, optional.
My grip is actually the contrary, many python code bases are stuck in the past. But I'm ok with it, people are not all pro devs, they need a productive language subset.
And the new syntax is providing some fantastic new tools, such as pydantic/fastapi/typer (if you haven't try it, it's really paradigm shift, and I say that as a django fan). f-string are awesome. dataclasses are nothing but convenient and readable. Advanced unpacking as well. I seldom use the walrus, almost nobody does, but when one does, it's useful.
> decorators, comprehensions,
Those existed in python 2, 10 years ago. It's hardly new python. And web frameworks would be terrible without those.
That was when I officially stopped recommending Python as a new language. It's just got too many things to explain now. And too many of those things to explain are like decorators, which requires two explanations, one to explain what it looks like they are doing when you use a decorator prepared by someone else, and a second explanation about what they are actually doing if someone ever wants to write one of their own.
It looks like "x.y" resolves the "y" value out of the instance, but actually, it's this complicated thing so that properties and other things work. It looks like the for loop simply ranges over the list, but it's actually this thing with iterators. It looks like calling this function "yields" a list but it's actually this magical thing where using a "yield" in the function body completely and non-locally changes the entire function. It looks like....
When Python was new and the list of this "it looks like..." was short, it was kinda need to be able to override __getitem__ for certain useful effects and such, but stacking on more and more of these with every release has created a real monster of comprehension.
Now it feels like Python has raised "it looks like... but actually..." to a design principle.
It looks like the match operator simply takes the argument you give it and sees if it is the same "shape" as the case statements, but it actually consults a new magic class variable and has this logic and that logic and....
This seems a little unfair. Testing frameworks are notorious for pushing the envelope of language constructs and weird introspection tricks. And if in this case you picked an exceptionally ripe example I'm not sure that should reflect on the entire language.
Back in the day people used to use the baroque nature of some frameworks such as Zope to bash Python but Zope was an outlier. There will always be some gorgonzola in any ecosystem.
I'm not sure i agree entirely: regardless of whether we're talking about decorators/annotations in Python/Java/.NET/... in the context of testing/web/... frameworks, any and every of those combinations have only caused me pain and headaches.
While i do agree in principle that it's nice to abstract away code with something that annotates a method and gives it additional meaning, in practice this sort of indirection usually has something to do with dynamic code calls, reflection or whatever the idiom is called in any particular language. All of those feel harder to debug and more brittle than just writing 3 lines of code instead of 1 annotation/decorator.
The problem with that, is that it's usually:
- hard to make your own decorators/annotations (especially if you want to extend their functionality)
- hard to introduce state, dependencies and just get them working nicely with the rest of your codebase and IoC/testing frameworks, or pass data between the logic behind them (from one decorator/annotation to another)
- hard to use them, as opposed to your "regular mode" of writing code, IDEs won't offer many refactoring options
- hard to debug them, especially in the case of wanting to examine their internals and use breakpoints
To me, it just generally seems like someone is trying to tack on functionality in ways that are hard to utilize, as opposed to just writing code in the first place, the very same thing that you see happen with generics, deeply nested inheritance and other "tricks" which are supposed to make one's life easier but do the opposite in practice in many cases.Personally, i enjoy libraries like Typer (https://typer.tiangolo.com/) for Python and think that it has plenty of use cases, but i've been hurt far too many times by similar functionality in non-trivial cases in every single language that i've used that supports something like that, so at best i'm guarded about utilizing them.
Java rant: But perhaps that's just because Java is a major source of pain for me in that regard. To give you a concrete example: i'm migrating Spring to Spring boot and someone used Jersey instead of RESTEasy as their JAX-RS implementation and now i need to transpose hundreds of API endpoint definitions (for example @GET to @GetMapping with bunches of parameters). If it were just Java code, i could probably use some clever refactoring in it with the help of my IDE, but now i'd have to figure out where the annotations come from, create a stub to replace them, call the proper stuff from within them and hope it works, unless the reflection that's used by them breaks. Not only that, but debugging is kind of hard when the source code and the actual logic behind said annotation is hidden below dozens of layers of Eldritch indirection and you have no chances of feasibly finding all of that stuff out.
From the outside looking in the Python community seems aggressively anti reworking their code to adapt to new language features. The 2 to 3 debacle comes to mind when compared to other languages, like JS, which made a much bigger change and everyone jumped in it and rewrote their code to adapt, or Ruby which went through some backwards incompatible changes at the same time as Python.
With Python I don't get the same feeling, they seem more resistant to change in general. I don't think it's just that Python has more so-called "non-programmers" (meaning, data science and math people). Most of the libraries for these things are made by full-time programmers anyway. Perhaps the idea that "there's only one way to do it" brought about a "the current way is good enough, why rewrite in a new one?" attitude.
But anyway, I'm not familiar enough, just an outside perspective.
I was once an avid Python user, but I switched to mostly Rust a few years ago and now write very little Python. A while ago I wrote some not quite trivial script-like code in Python, but ended up converting it to Rust before it finished running. The attraction of Python used to be that it was simple, but I now value robustness (through the type system in particular) much more than simplicity of the language.
Decorators though I totally agree; had some tooling at work that was gussied up to the point of impenetrability by gratuitous use of decorators. A colleague got so fed up with it he rewrote it in Perl, which was, shockingly, actually a maintainability improvement.
Same story for list comprehensions: it's almost beyond me how one can be against a simple single list comprehension. It's simple, readable, almost beautiful. Now if people start nesting 5 comprehensions it's not so nice anymore; but then is the language to blame (as in, would we really want to forbid nested comprehensions?), or the people?
More on topic, I do sort of feel like maybe the Python team should have made a Newthon (with a better name) or something, because Python is becoming a shambling beast. Starting as one of the world's most dynamic OO language and then shambling towards being a static language, maybe a bit of a functional language, it's crazy.
I often joke about "Katamari Dama-C++!", which has the irresistible phonetic allusion, but damn if Python isn't trying to keep up at this point.
If you don't like Python for some task... use something else suited for your task, then. It's not like using something else means you must put Python down and never use it again on pain of being shot or something. Stop waiting for Python to mutate into O'Caml or something and just use O'Caml or whatever.
Python doesn't need more features but better implementations, IMO.
Edit: Just remembered the Zen of Python, I don't think modern Python is following it.
...
Simple is better than complex.
...
Readability counts.
...
There should be one-- and preferably only one --obvious way to do it.
Having one way of doing something and sticking with it even when better ways emerge is nonsensical imo.
f-strings are more readable (for me) and concise than string formatting or .format()
I do agree on the implementation front though, many of the other implementations are still on 2.7 or 3.6 at most, and don't have mainstream appeal. Made more with interop in mind (like ironpython with .net and jython with the jvm)
Not for beginners though. One of Pythons main strengths was that it looked simple. Not anymore.
Point is Python used to be a complete yet simple language with few constructs. It more or less followed the path of least surprise and people revered it for that. For several years, it feels like PEPs just add more and more clutter to that foundation.
It sort of looks like an arms race and I just don't get why or what's going on. Certainly, Python doesn't feel the same anymore.
elif cmd.op == 'TUPLE':
arity, = cmd.args
tuple_args = []
for _ in range(arity):
tuple_args.append(self.data_stack.pop())
result = '{' + ', '.join(map(str, tuple_args)) + '}'
self.data_stack.append(result)
Is following use of comprehension any better? I don't think so: elif cmd.op == 'TUPLE':
arity, = cmd.args
tuple_args = [str(arg) for arg in reversed(self.data_stack[-arity:])]
self.data_stack = self.data_stack[:-arity]
result = '{' + ', '.join(map(str, tuple_args)) + '}'
self.data_stack.append(result)
Any other suggestions for improvement? elif cmd.op == 'TUPLE':
arity, = cmd.args
stack = self.data_stack
text = ', '.join(str(stack.pop())
for _ in range(arity))
stack.append(f'{{{text}}}')There's usually a threshold where I go "nope" and unroll it into a good old loop.
But when you're in their sweet spot, they definitely improve readability.
Buy I personally like it when I read other people's code and see something which I don't understand, which I've never seen, and then it turns out to be a feature which actually makes coding easier and more efficient. Then I think about all the places where I could have used that feature in my old code.
Comprehensions are a little less magical - if anything, they are more explicit. If I want to create a list where each element is generated by some function over another list, I just say it. Doing so with loops is obscuring what I wanted to say in the first place. The problem with comprehensions isn’t so much the comprehension, but the obtuse ways people can use them to eke out performance by avoiding explicit loops.
I’m all for things that are closer to what a programmer means, but less keen on features that entail obscuring details that may come back to haunt the programmer later (I see this most often with decorators).
"But someone will make a typo one day resulting in = instead of ==" argument is nonsense as that hasn't been an issue since forever in other languages. You can either require parentheses if you want to use the value (the way C compilers want it) or just make it := to begin with.