Wow! Prior to reading this, I was not aware of "Zero Cost" exception handling. While I am only a Python developer, I always assumed that in any programming language, exception handling, regardless of whether an exception is raised or not, cost some CPU cycles. I work at an HFT firm and they test their changes in equations in Python programs on crypto rather than C++. So I resorted to using try-except blocks in Python to reduce "branching" i.e if-elif-else blocks. I would just add all the different conditional functions in a dictionary and manage calls based on keys and handle exceptions. I don't know if that's the best way to improve speed, but I would like to check if this has any impact on it.
For me it isn't about the cost. Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable. It's my only complain about Python, (outside of performance of course). In Python when you see a `try`, you don't know if it's because there's error handling going on, or if it's because that's the only way to achieve a certain goal due to Python being designed to mingle logic with error handling. After doing projects in Go and Rust, I can see the value in separating the two, and that makes me sad that Python is old now.
Maybe what they're planning to do with this is allow wrappers to hide the places where exception handling is gratuitous, and therefore try to bring Python forward into the world of more modern languages.
> Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable.
The (result, error) pattern in Go or Result<Ok(res), Err(error)> pattern in Rust usually mixes the two. Unless you're doing and_then in Rust, but Go doesn't have anything like that. If anything, I feel like it's exceptions that separate the error handling for the conventional logic. You have your normal code in try, and your error handling in except.
> Modern languages like Go and Rust separate the error handling from the conventional logic, and that makes the code more readable.
This is a very odd claim. Go and Rust are extreme examples of mixing up error handling with conventional logic. There are excellent reasons for doing it that way, but the fact remains that they do. Exceptions, on the other hand, definitely do separate error handling from conventional logic – that's the whole point of them.
I think you just happen to have seen Python code bases where exceptions are caught very close to where they are being thrown, but that's property of the code you read, not the language feature. And presumably you have also seen Rust/Go code bases where errors are often passed back up the stack, which is easy to do but still requires some code (even just a ? in Rust is still an explicit decision) in a way that allowing exceptions to propagate up does not.
You may enjoy programming in Elixir if you like that style. In Elixir, you only program the “happy path” and just let things fail. Then you rely on supervisor processes to handle the exceptions/errors. Well, at least that is the idea. I think people still do tests and function guards and things. but the “let it fail” idea is definitely part of the Erlang/Elixir world.
The sad thing is that there really isn’t any “learn elixir” book that teaches this idiomatic design. A student of Elixir should set up an umbrella application from the very first hello world, in my opinion.
Well, I don't enjoy the "happy path" programming. Admittedly, this implementation to improve speed feels a bit hacky. I only did it because it had a measurable impact on the computational performance of my program. In my other grunt worker scripts, I actually prefer if-elif-else statements because they make code readability better for other programmers who are not Python "natives", but use the scripts or modify them to suit their use cases.
There are some weird performance optimizations in Python, e.g.,
item = some_dict.get(key)
if item is None:
# key does not exist
Versus
try:
item = some_dict[key]
except KeyError:
# key does not exist
When I tested these (admittedly, a while ago), which one was faster depended on how often the key was missing. If “missing key” was an expected case, the first one was faster. If “missing key” was uncommon, the second was faster. It sounds like the fast path in the second case is getting faster, so this performance gap may be increasing.
Fun fact: all those approaches use multiple dict lookups, just of different dicts.
First approach is looking for `get` in `type(some_dict).__dict__` and then for `key` in `some_dict`.
Second approach is looking for `key` in `some_dict`, and then (only if missing) for `KeyError` in the module globals/builtins.
If the performance of hash lookups matters, Python is the wrong language for you.
> If the performance of hash lookups matters, Python is the wrong language for you.
Announcement to Python programmers: “Don’t bother trying to improve the performance of your Python code! If performance matters, just completely rewrite your code in a different language!”
I don’t know how to respond to that, except to disagree with the underlying assumptions that (1) there is a “right language”, (2) if performance matters, Python is not a suitable language, or (3) people are generally in a position to choose which language a project is written in.
Even if performance matters, it is not the only thing that matters. When you choose a language, there are necessarily tradeoffs... everything from the skillset of your team, to the ecosystem of libraries available affects that decision. Finally, there are projects already written in Python.
I am going to speculate here, so if I'm wrong please point it out.
Here, the number of steps directly affect the time.
In the first approach, the ".get()" method first analyses the type of "some_dict" and then uses an internal variable (the ones surrounded by double underscores) to try and fetch the value by using the provided key. If the key is present, then the value is returned, if not then a default value is returned. So if the key does not exist, the returning the default value saves 1 step (that of fetching the value from the map)
In the second approach, the exception raises the number of steps because the type of error has to be determined and the stack is traced every time an exception is raised. So the more exceptions are raised, the slower the code gets.
I tested this with 3.9.7 right now and in my testing, the runtime of first approach was virtually unchanged, while the second one was faster if exceptions were raised ~12% of the time or less. (I ran both 10 million times)