6 ms·
I don't find that so bad. They look complicated but behave as expected, and would only be a problem if you inherit a code base from a lone wolf without common s
by BoppreH 3y ago
I don't find that so bad. They look complicated but behave as expected, and would only be a problem if you inherit a code base from a lone wolf without common sense.
Python has plenty of warts[1], but I don't think the syntax is one of them (yet). Some of the more concerning warts:
# Mutable defaults in function parameters.
def fn(arg=[]):
arg.append(1)
print(arg)
fn() # [1]
fn() # [1, 1]
fn() # [1, 1, 1]
# Loop variables are shared across executions.
lambda_list = [lambda: print(i) for i in range(5)]
for fn in lambda_list: fn()
# 4
# 4
# 4
# 4
# 4
# Iterating over an exhausted generator yields an empty list.
numbers = (i for i in range(5))
assert 1 in numbers
print(list(numbers))
# []
# I hate this one because it could have easily been an error, it's
# hard to track down, and lots of stdlib functions return generators.
# Chained operators can sometimes be very unintuitive.
should_be_ascending = True
assert 1 < 2 == should_be_ascending
# AssertionError because it was interpreted as
# `(1 < 2) and (2 == should_be_ascending)`
[1]: https://github.com/satwikkansal/wtfpython https://github.com/satwikkansal/wtfpython
- travisjungroth 3y agoOn the generator example, it doesn’t yield an empty list. It continues to return an empty iterator. This couldn’t have easily been an error because one aspect of generators is they can be dynamic. There could be an API call in there, where it polls for new results. Raising a “NoMoreForever” error or something would break the interface.
- BoppreH 3y agoYou're right, "empty list" is an imprecise description. But couldn't they have made it behave like Go channels, for example? Read it until exhaustion, then close it, so that subsequent reads (or calls to 'next()') fail with something other than StopIterator. I know it's not backwards compatible and hence a no-go now, but the original decision puzzles me.
- formerly_proven 3y ago1 and 2 are the scoping rules (since Python has no variable declarations [1], making all blocks scopes would be pretty awkward and require tons of "xy=None # reassigned later" and nonlocals), 3 should result in [2, 3, 4], not []. [1] It actually does nowadays, ever since non-assigning annotations came around.
- colejohnson66 3y ago> ... making all blocks scopes would be pretty awkward and require tons of "xy=None # reassigned later" and nonlocals) ... C, C++, Java, C#, JS/TS (through let), Rust, and more all require variable declaration before use. Python is the odd one out here. JavaScript had something similar (variable hoisting), but it's so problematic that we ditched it for let/const.
- babel_ 3y agoI agree that functions sharing the references to their arguments is awkward (though useful on occassion), however the alternative is to re-instansiate the arguments by a shallow copy, a deep copy, or full-on re-evaluation, all of which have their problems and are significantly slower (calls are already slow in Python as is!) so it makes sense on balance for many use cases, especially as most defaults in practice are immutables like numbers and strings (so it's really just the usual warts for references to mutables). The others are a little less concerning to me. Sure, the lack of fine-grained scoping is annoying, but that's just how Python operates; the example confuses the point via the lack of variable capture in lambdas, when fn is called it does a normal locals/globals lookup for i, and the lack of scope means the last value of i spills outside of lambda_list, meaning all evaluate to 4, but this is primarily a problem with how lambda works without capture (again, scoping in Python is what it is). I've even begrudgingly (ab)used Python's lack of scoping on occassion, albeit mindfully aware of when I can and when it is actually defined, because it is very easy to cause problems with and generally best avoided. Empty generators to empty lists isn't a huge concern in my experience, as it lines up with empty lists, sets, and dicts as the behaviour for comprehensions and how to handle them when there's nothing. Also, your example for it, uh, doesn't work as you say (it prints [2,3,4]). Operator precedence is always awkward in every langauge that has it, and operators are still awkward in those that don't, and Python's is no exception, though it's largely better than C's -- in Python, comparisons share precedence and are lower than most other operators. I feel there isn't really a right answer to the problem as is, though ==/!=/in/is maybe shouldn't be part of chaining (but chaining is defined over all comparison operators [1] so that's probably not going to change). As always, when in doubt, use parenthesis. [1]: https://docs.python.org/3/reference/expressions.html#comparisons https://docs.python.org/3/reference/expressions.html#compari...
- gcbirzan 3y agoFirst one is annoying, but it makes sense, to some degree. You say the default is this particular instance that gets created when you define the function. Second is just how things work, the function is a closure that evaluates the non local variable when you run it. Third one... Iterating over the generator does NOT yield an empty list, it raises StopIterator. As does trying to iterate after a generator is done, so there's no way for list to know what's happening. You can argue that's a bad design, but I'm not so sure, an iterator is something you need to make sure you don't try to iterate over twice... I don't get the 4th. Comparison operators have the same precedence, so yeah.
- gpm 3y ago> Second is just how things work, the function is a closure that evaluates the non local variable when you run it. In python, yes. In other languages, not necessarily. There are two ways that this could reasonably not be the result: 1. For i in iterator could create a new variable i (shadowing the old i) on each iteration of the loop. This isn't how python works (which also means you can do things like access i after a for loop terminates) but there are languages that work that way. 2. Capturing an integer could copy that integer, instead of copying a pointer to a value that can change in the future. Again, not how python works, but how some languages work. The first in particular I think is more intuitive in other languages, and leads to other footguns in python as well, e.g.: for i in range(5): for i in range(10): pass print("Finished loop iteration", i) # I meant 0, 1, 2, 3, 4. Instead I got 9 9 9 9 9
- BoppreH 3y ago> First one is annoying, but it makes sense, to some degree. You say the default is this particular instance that gets created when you define the function. Could just as easily be an expression that's reevaluated on each call. In fact it's what most other languages do, making it twice as bad a decision. Even Javascript gets it right: function fn(arg=[]) { return arg; } fn() === fn() // false > Second is just how things work, the function is a closure that evaluates the non local variable when you run it. That's again a decision made by Python. Some other languages behave as if each iteration had declared a different variable. Here's Javascript again, you can even use `const` for the iterating variable: const fns = []; for (const i of [1, 2, 3]) { fns.push(() => console.log(i)); }; fns.forEach(fn => fn()); // 1 // 2 // 3 > Third one... Iterating over the generator does NOT yield an empty list, it raises StopIterator. Sorry, my phrasing was imprecise. It's not doing 'yield []', but an exhausted generator does behave like an empty generator. Maybe they could have made it so the first exhaustion raises StopIterator, and subsequent ones a different exception? Like reading from a closed channel. > I don't get the 4th. Comparison operators have the same precedence, so yeah. It's not precedence, it's Comparison Chaining, the same feature that enables '1 < a <= 10'. If the operators had simple precedence, it would be equivalent to '(1 < a) <= 10' or '1 < (a <= 10)', but Comparison Chaining evaluates it as '(1 < a) and (a <= 10)'. Useful for specifying ranges, but a foot gun in other scenarios, like in my example.
- deleted 3y ago[deleted]
- crabbone 3y agoSyntax is pretty much one of the downsides of Python. Especially the new additions, including f-strings, match-case, type annotations, async-await, data-classes etc. The largest downside is that they hugely blew up the language complexity for no tangible gains. It's like you had a crappy bicycle before, but after an "upgrade", you have a crappy bicycle that doubles as a crappy ice-cream maker. This means that anyone who wants to write a parser for Python from scratch is screwed. The language was garbage before, and writing a parser for Python was already very difficult, but after more junk was added to it, it just became unnecessarily more difficult. So, if you might have had a glimmer of hope to have tools that analyze the source code and do something with it, even as trivial as highlighting... well, there's less and less hope of that happening. So, OPs complaint about f-string is sort of valid. OP didn't really look for a very bad example though, but bad examples are hard to refine into more concise form. Fundamentally, what sucks about f-strings beyond them being completely unnecessary is the situations where one has to deal with multiple incoherent escaping rules, s.a. when one has to interface with, eg. logging module or string.Template or str.format() or % in general. Another vomit-inducing combo is raw strings combined with f-strings (i.e. building regular expressions through interpolation). Yet another downside of f-strings is the departure from object-oriented approach. The benefit of str.format() was it being object-oriented, which allowed for extensions in the same way objects can be extended. f-strings are an extension of the language syntax, but Python doesn't have an easy general way to extend its syntax (barring the fact that you can abuse the import system to import altered Python sources s.t. they have desired syntax). But, again, these disadvantages are hard to present as a one-liner, and for many simple uses of the language, which is the majority of its uses they are inconsequential. Unfortunately, that majority of uses is also transient and contributes nothing back to the language... so, it could be confusing to rely on the majority's opinion when it comes to judging the quality of various language features.
- mixmastamyk 3y agoYou don’t write a parser from scratch, you use the ast module. The rest of this is excessive negativity trying to sound superior while ignoring history. (I even agree about the match statement and walrus, but fstring is a very useful compromise.)
- deleted 3y ago[deleted]
- BoppreH 3y agoCorrection, the empty iterator example should have been `assert 5 not in numbers`, or another expression that exhausts the generator. And to everyone saying that the behaviors are correct because of scoping rules, or iteration protocol: I know. Those are not compiler bugs, but bad design decisions. Every example here has a more intuitive behavior in another language.
- remram 3y agoAnother weird syntax quirk is this: e = 4 try: b = 4/0 except Exception as e: print("exception raised") print(e) # NameError: name 'e' is not defined The captured exception only exists in the 'except:' block, but instead of creating a new scope (shadowing the outer 'e'), it deletes it afterwards.