What’s New in Python 3.8
docs.python.org
docs.python.org
The "walrus operator" will occasionally be useful, but I doubt I will find many effective uses for it. Same with the forced positional/keyword arguments and the "self-documenting" f-string expressions. Even when they have a use, it's usually just to save one line of code or a few extra characters.
The labeled breaks and continues proposed in PEP-3136 [0] also wouldn't be used very frequently, but they would at least eliminate multiple lines of code and reduce complexity.
PEP-3136 was rejected because "code so complicated to require this feature is very rare". I can understand a stance like that. Over complicating a language with rarely-used features can definitely create problems. I just don't see why the three "headline" features I mentioned are any different.
Most devs don't understand that software can be done and still be useful.
Like how they removed the `print` keyword when going from Python 2 to Python 3.
But removing is harder than adding.
https://github.com/eveem-org/panoramix
(source code for Eveem.org, which is arguably the best decompiler for Ethereum smart contracts out there. you can see a lot of pattern matching in pano/simplify.py , and I found no way to do it without extending the language/walrus while maintaining the readability)
Feels like a case of preferring cleverness over readability/usability/maintainability.
Same can be said about e.g. Cobol.
Cobol also was a good idea back in the day. And some of the ideas in Cobol we know recognize as bad _had_ to be tried to gain that recognition.
There have been dozens of working C++ competitors over the years, each one technically as good as C++.
They all failed because their authors are good at writing languages, but very bad at knowing how languages are used in the real world.
The C++ committee really knows what it's doing, you're not going to do anything useful in this space without actually understanding the problem domain.
TL;DR - yes, C++ is complex, but that is because programming is complex. Your attitude of "programming is hard, let's go (language) shopping" will not displace C++.
I totally agree that C++ of 21st century is way better than C++ from 1998, and is becoming better. The problem is that maintaining backwards compatibility requires to keep a number of footguns in place, and some of them are cornerstones of the language :(
I have high hopes for Rust becoming the more sane competitor in the space traditionally occupied by C++. Its authors seem to understand the problem domain from a very practical standpoint, and are proficient in C++, to begin with. They also strive to apply reasonable design principles, and some actual math hopefully will keep the core of the language from logical pitfalls.
Another example of hitting a sweet spot is Go. I dislike many Go's language decisions. (Some are definitely great, though, e.g. the whole OOP approach lifted from Oberon.) But its authors definitely understand both the problem domain, and the target audience.
This is what I mean by talking about success despite the flaws. C++ has both flaws and merits, and merits outweighed the flaws. But the flaws are still painful.
To give you an example, let's look at this statement:
if exp ~ ('mask_shl', int:size, int:offset, -offset, :e): ...
It is the same as writing: if type(exp) == tuple and len(exp) == 5 and exp[0]=='mask_shl' and type(exp[1])==int aand type(exp[2])==int and exp[3] == -exp[1]: size, offset, e = exp[1], exp[2], exp[4] ...
If you can figure out syntax that makes this sort of matches more readable, I'll gladly use it :)
if exp ~ ('mask_shl', int:size, int:offset, -offset, :e): ...
vs if type(exp) == tuple and len(exp) == 5 and exp[0]=='mask_shl' and type(exp[1])==int aand type(exp[2])==int and exp[3] == -exp[1]:
size, offset, e = exp[1], exp[2], exp[4]
...Also, I wonder if there's an easy way to implement this in a jupyter notebook, without fiddling with the kernel...
(issues of whether it's a good idea aside, definitely seems useful to know about.. )
Also, in my case I had to use Codecs because a new operator causes AST-parser to not go through. But if you have some kind of syntax that will be compatible with Python AST, then you can implement it even easier.
You can google Python AST to get a lot of useful information :)
I went to your repo, and it wasn't clear to me what Panoramix actually is or does. So I checked out Eveem.org, and even on the about page, it wasn't clear to me what Eveem was.
But with that context, coming back to your GitHub page, I was able to get the gist of it.
It might be helpful to have a section above the installation section that gives a little context / tells about the project.
And in your example section -- again, I literally don't know anything about decompilation so maybe I'm not your target demographic... but if I understand the gist of it, kitties is an example function or binary. It might be better to use something more concrete (and provide the context).
https://github.com/eveem-org/panoramix/blob/bc55be84a9a6abb1...
It uses "~" custom operator, but implementing it would not be possible without ":=" operator (see /tilde directory).
class Any:
def __eq__(self, other):
self.value = other
return True
Then you can use an instance of it as the hole in a pattern. My use case was performing peephole optimizations in a simple 6502 assembler. For example, if the LDA operation (load value from memory location into accumulator) occurs twice in a row, the first load is redundant. So I did this test: x, y = Any(), Any()
if L[i:i+2] == [('LDA', x), ('LDA', y)]:
L[i:i+2] = [('LDA', y.value)]
(Note that the last line changes the length of the list, so it may be slow on large lists.)I was going to write a blog post about this back then, but I never did. If anyone is interested, I can write it now.
It is some spooky action at a distance.
$ perl
$a = "foo";
if ( my $a = "bar" ) { print "$a\n" }
print "$a\n"
bar
foo if (ptr = strchr(address, ':'))
port = atoi(ptr + 1);In particular, without the walrus operator you can't write an if-elif chain of regex comparisons. The best way to that that I've been able to find is with a bunch of nested if-else statements.
The primary one I want is
if m := re.match(...):
print(m.group(1))
and while s := network_service.read():
process(s)
both of which are both clearer and less error-prone than their non-walrus variants.The other one that I would have found useful an hour ago is in interactive exploration with comprehensions. I frequently take a look at some data with [i for i in data if i[something].something...], and being able to quickly give a name to something in my conditional, as in {i: x for i in data if x := i[something]}, helps maintain the focus on data and not syntax. Obviously it will get rewritten to have clearer variable names, at the least, when it gets committed as real code, and almost certainly rewritten to be a normal non-comprehension for loop.
Like comprehensions, I expect the walrus operator to be valuable when used occasionally, and annoying if overused. There's no real language-level solution to bad taste. In Python 3 you can now do [print(i) for i in...], and I occasionally do at the REPL, but you shouldn't do it in real code and that's not an argument against the language supporting comprehensions.
m = re.match(...)
if m is not None:
pass
Now, I wonder what you meant saying that single-line version is less error-prone, because I don't think so. I believe they're exactly the same in this regard, except for a bizarre case when someone would bastardize the code by putting some irrelevant lines between the assignment and the comparison, obfuscating the logic.Also, as I tend to use `if m is not None:` rather than just `if m:` (because I typically don't want to run all that subtle `__nonzero__` magic), it would - subjectively - look less pretty in the one-line version: `if (m := re.match(...)) is not None:`.
Use in the generator expressions to create aliases/shortcuts and avoid repetition is great, though. I love this use case.
while m := f(many, args):
# do stuff with m
Now, if you're writing this in Python 3.7, you often end up with some code duplication: m = f(many, args)
while m:
# do stuff with m
m = f(many, args) # duplicate
Or something like this: while True:
m = f(many, args)
if not m:
break
# do stuff with m
Personally, I consider the last version to be the most elegant, but still something of an anti-pattern. So I welcome this new operator.Edit: 1 line, not 2 (excluding the "do stuff")
def f_iter(many, args):
while True:
m = f(many, args)
if m:
yield m
else:
raise StopIteration
...
for m in f_iter(many, args):
# do stuff with m
This way you’re isolating all the initialization logic, error handling, etc. And you can focus on your domain logic in your client code. for m in filter([re.match(...)], bool):
...
;-) def f_iter(many, args):
while True:
m = f(many, args)
if not m:
return
yield m> [bpo-32670]: Enforce [PEP 479] for all code. This means that manually raising a StopIteration exception from a generator is prohibited for all code, regardless of whether ‘from __future__ import generator_stop’ was used or not.
[changelog]: https://docs.python.org/3.7/whatsnew/changelog.html#id115
[bpo-32670]: https://bugs.python.org/issue32670
[PEP 479]: https://www.python.org/dev/peps/pep-0479/
from functools import partial
with open('mydata.db', 'rb') as f:
for block in iter(partial(f.read, 64), b''):
process_block(block)Also if it comes to pure form I would have preferred `while (evaluate) as x:` out of establishing a parallel with existing syntax, but that's not very important.
In that sense, while loops over expressions always require knowledge of the expression to understand the semantic. On the other hand, something like:
while True:
with x as y:
print(y)
would not require understanding of x to understand the intent. Therefore it is not semantically equivalent to: while x as y:
print(y)
Note that in both cases, `:=` is strictly equivalent to `as` as it is defined for `with`. That is a matter of personal preference. for line in f:
if (m:= pat1.search(line)) is not None:
... do stuff ..
elif (m:= pat2.search(line)) is not None:
... do other stuff ..
elif (m:= pat3.search(line)) is not None:
... do something else ..
In older Python that's: for line in f:
m = pat1.search(line)
if m is not None:
... do stuff ..
else:
m = pat2.search(line)
if m is not None:
... do other stuff ..
else:
m = pat3.search(line)
if m is not None:
... do something else ..
I think the newer makes it clear that it's supposed to be a simple elif chain, where all branches following the same structure.There are other ways to structure it, but the alternatives I can think of also have their own cumbersome complexities.
LINE_ACTIONS = (
(re.compile("pattern 1"), do_stuff),
(re.compile("pattern 2"), do_other_stuff),
(re.compile("pattern 3"), do_something_else),
)
...
for pattern, action in LINE_ACTIONS:
m = pattern.search(line)
if m is not None:
action(m)
break
Or even use a method registry pattern that would auto-populate LINE_ACTIONS by just declaring the fuctions: @action("pattern 1")
def do_stuff(m):
...
Alternatively, I might just break the processing into a function: def _process(line):
m = pat1.search(line)
if m is not None:
... do stuff ...
return
m = pat2.search(line)
if m is not None:
... do stuff ...
return
m = pat3.search(line)
if m is not None:
... do stuff ...
return
_process(line)
Of course, this depends on the purpose. Could be completely inadequate in some situations.I've found that I don't like using it. As you write, it's "inadequate in some situations", which I consider as part of the cumbersome complexities I mentioned.
For examples, do_stuff() and do_other_stuff() may need to share variables, and do_something_else() might need the line number to report an error while the others don't.
This can be handled with shared state/nonlocal, and by passing in more parameters to the generic handler API, but these add complexity.
Or, different parts of the file may have different line dispatch processing (eg, a header block followed by a data block followed by a footer block) where one of the handlers must indicate the transition to a different processor.
Also, function dispatch in CPython is slow. While the regex tests are also slow, it can also be important to consider the (using a hypothetical number) 5% overhead for dispatching over inline code.
re.match() should always return a match object, but instead .group(1) will return None. Then we can write one-liners easier without the walrus operator.
The problem is not in re.match but in None being a relatively poor Maybe / Optional (still ways better than C-style `null` of many other languages).
re.match vs re.search
if re.match(...).matched:
But yeah, agree that Optional is what I want here. if let Some(m) := re.match(...)
m.group(1)...
m = re.match(...)
m.group(1) # TypeError because I haven't unwrapped the Optional
In current Python, that last line sometimes throws a runtime error. >>> import re
>>> pat = re.compile("(A+)|(B+)")
>>> m = pat.search("HUBBLE")
>>> m.group(1) is None
True
>>> m.group(2)
'BB'
>>> m.group(0)
'BB'
I don't see how using your proposed change would help.... I've got no idea how the syntax could look though.
if (value := func()) and another_condition:
...
Or: if another_condition or (value := func()):
...
Or: x=1
my_list = [x := x+1, x := x-1]
etc.The inside of the for-braces allow for keyword-based sub-clauses: There's
- ":when" which will suppress output for elements not matching a filter expression,
- ":while" which will stop when elements stop matching a filter, and
- ":let" which will let you bind some new values in mid-loop.
I rarely need these features, but when I do I find them really really helpful. Maybe someday someone will consider doing something similar in Python.
Watch out with this one though. In this case it's good, but if you receive numbers from such function, it will break loop not just on None but on 0 as well. Easy to forget.
In many cases you'll need to do:
while (s:= ....) is not None: for s in iter(network_service.read, ''):
process(s)That was the reasoning given for using ":=" instead of "as", to allow more complexity. I still think it was a mistake.
Python is a fairly old, mature language.
What features would you have been especially excited about?
I'm also looking forward to PEP-554 [0], which allows for "subinterpreters" for running concurrent code without removing the GIL or incurring the overhead of subprocesses.
If anything, user-input strings must be treated as tainted whatever the case.
1) existing uses of .format() would break:
aifc.py: raise Error('marker {0!r} does not exist'.format(id))
2) existing uses of "%" formatting would break: argparse.py: result = '{%s}' % ','.join(choice_strs)
3) many regular expressions would break: uuid.py: if re.fullmatch('(?:[0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
4) existing strings which contain uuids would break: uuid.py: >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')1) customizable templates (can't allow the user to access arbitrary variables)
2) i18n (can't allow the translator to access arbitrary variables)
3) complex values being passed to .format() instead of some variable
The current extension module API encourages global state, e.g. types (and objects too) are allocated statically in a global C variable. For example, there is a global C variable `_Py_NoneStruct` that is the Python `None` value, and extension modules are accessing this variable directly.
Every use of this object needs to adjust its reference count, and that reference count is directly stored within the C global variable.
`_Py_NoneStruct` is currently even exposed in the PEP 384 stable ABI. Existing extension module binaries are commonly directly touching `_Py_NoneStruct.ob_refcnt` without any synchronization. Breaking the PEP 384 compatibility promise is fundamentally unavoidable here.
One of two things must happen: Alternative one: All refcount operations must be made atomic for thread-safety. These are really really common in the Python interpreter, but atomic operations are expensive on modern CPUs (especially if there's contention). But multiple threads using the value `None` would be quite common in Python code, so I doubt you'd gain any speed even at today's core counts -- in fact I'd expect the constant inter-CPU-communication for the refcounts to make everything slower than just using a single core with today's Python!
So alternative two, ensure no Python objects are shared between the subinterpreters. That's the plan. But that also means it's a breaking change for extension modules. And it's not just an ABI change (which would be handled by merely recompiling against the new headers). Any extension modules that do not yet support PEP 489 are already incompatible with subinterpreters, so that will take quite a bit of work until the ecosystem is upgraded. But there will probably also be some other breaking API changes. I think type objects are currently shared across subinterpreters, and those are frequently defined as a `Py_TypeObject` global variable in extension module code. Also, if every subinterpreter has its own GIL, extension modules calling `PyGILState_Ensure()` will have to specify which subinterpreter they will be using, so that the appropriate lock can be acquired.
My prediction: 3.9 may have the basic functionality, but it still won't be able to run on multiple cores concurrently. That will take a bunch of more work and breaking changes, and will likely be released as Python 4.0.
There will be another slow upgrade process ("my dependencies must upgrade before I can") until the Python ecosystem is multi-subinterpreter-compatible. But at least this one only affects extension modules.
Python's heuristics for that are pretty annoying.
If it's any consolation, the scope of a variable isn't determined by heuristics, it's just that the rules are (IMO) kinda bad. Basically, if a variable is assigned (in addition to `=` and friends, `import`, `class` and `def` are also assignments in disguise) to in what Python calls a "block" (which is not like a C-style block; rather `class` body, `def` body, or top-level) it is considered to belong to that block (with the notable exception of the name for a caught exception in a `except Exception as e: ...`, see [1]). If you want it to refer to a variable outside the block, you need to use `global` or `nonlocal` as appropriate. The full gory details are in [2]
1: https://docs.python.org/3/reference/compound_stmts.html#the-...
2: https://docs.python.org/3/reference/executionmodel.html#nami...
Python didn't use to have lexical scoping, so the rules are retrofitting around that.
I've started using Docker for ensuring prod behaves the same as dev. Going from Mac dev to Linux prod would otherwise cause trouble.
Does your container expose ports or need volumes? Does it need gunicorn or uwsgi in front of it? What about system packages? None of those (except maybe the last one) are really in the scope of the package manager.
If you use setuptools and place all your configuration declaratively in setup.cfg it is not that bad.
Emacs Lisp, at least, can use question marks in names without batting an eyelash.
I see no reason that would not have been true for Lisps generally since their birth.
[1] http://www.lispworks.com/documentation/HyperSpec/Body/f_list...
I wonder if there is anything that can really be done, outside of something like Cython.
I compile my cli tools with nuitka [0], the resulting binaries take half the time to start. I find the difference quite notable.
But startup time increases an order of magnitude once you start importing 'requests' and other common packages.
Perhaps an option to turn imports lazy...
"end" statement which would enable automatic indentation.
"switch" statement instead of "if/elif/.../else" hell.
Correctly done multi-line lambdas/expressions could bring Python's expressivitiy to where ECMAScript is nowadays, where you can write both foo = function(...) or function foo().
Perl 6 has a great switch statement, called "given" https://docs.perl6.org/language/control#given
You can use mypy for that: https://github.com/python/mypy
I love the walrus operator with a love that is unholy.
(although I think "else" should have been a different keyword)
I did not follow all the conversation around the typing system but isn't the whole point of it to propose optimizations in the future? I find it exciting
I also thought, why bother at first until I learned this is already a feature in python, but only for c-functions. So of course it makes sense to level the playing field.
https://www.python.org/dev/peps/pep-0572/#differences-betwee...
https://www.python.org/dev/peps/pep-0572/#relative-precedenc...
Even examples of the spec shows how unintuitive and "unpythonic" this is. Explicit is better than implicit.
IMHO adding features to the language to save 1 line of code for 10% of cases when you need it (I agree that there's occasional case when walrus will save you more than 1 line) is just bloat.
I am not a big proponent of Go, because it has its own flaws, though language is indeed very simple and creators of the language try to leave it simple.
IMO Python was very readable, super simple, intuitive and should stay that way, though recent releases show that Python is giving in for the feature bloat.
EDIT:
> Try to limit use of the walrus operator to clean cases that reduce complexity and improve readability.
Facepalm.
But now if our big paint point is assigning a variable to len(), then in two lines evaluating that variable and printing it out, Python is just trying to find stuff to add.
I miss the size and simplicity of old Python. :/
- Complex is better than complicated.
Using the walrus operator makes the code more complex, but less complicated.
> Simple is better than complex.
I understand that nobody will force me to use this feature, but as I said before, even the spec and the write up tells how this feature is confusing and .. complex.
I know and I believe there is a purpose to be exactly after that line. Writing only simple code is not enough. If you write everything in a simple manner your code will most likely be complicated, that's exactly why you have next - complex is better than complicated.
I see this feature the same way as comprehensions, scary and complex at first, but once you learn it you would wish anyone would use it.
Reference: https://www.python.org/dev/peps/pep-0572/#scope-of-the-targe...
Agreed.
There's a number of (full) languages that compile to the Python AST [0] (I'm especially fond of http://hylang.org), but they're very different from Python. It would be interesting to see smaller variations of standard Python implemented in an interoperable way like these languages do.
[0] https://github.com/vindarel/languages-that-compile-to-python
def f(a, b, /, c, d, *, e, f):
Wow: IMHO that is a very ugly syntax to define a fn with 6 parameters, although it looks to be a path dependency on previous decisions (*, and CPython ,/) and clearly there was much discussion of the need for the functionality and the compromises: https://www.python.org/dev/peps/pep-0570/It amazes me to see how certain features make it into languages via community/committee (octal numbers in JavaScript - arrgh!).
One thing I find really difficult to deal with in languages is the overloading of different syntactic(edit:semantic) usages of different symbols, which itself is a result of limiting ourselves to the ASCII symbols that can be typed. I don't recall written mathematics having the issue badly (although I haven't had to write any for a long time!).
I think we're ready for programming languages using some visually good Unicode characters, instead of overloading `[]{}!@#$%^&*()-_/` for everything!
A next step could be for common dev environments to actually convert symbol/key sequences to operators (how do APL programmers do it?)
Avoiding ambiguity and semantic overloading of ASCII symbols would surely help beginners (if also given a UI that clearly exposes ways to enter the new symbols). I always find one letter operators extremely strange too like u"word" s/foo/bar/ etc.
It seems a shame we can type in hundreds of Unicode symbols on a mobile virtual keyboard, but not readily on a physical keyboard.
JavaScript has supported Unicode for a long time, but the core language doesn't use it at all.
option-= ≠
option-< ≤
option-> ≥
option-/ ÷
option-o ø
option-w ∑
I don't know why other OSes haven't adopted something similar. The first couple of these were even idiomatic in HyperCard's scripting language.How do you discover how to type ß, °, «, ‡ etc?
Android's gboard uses phonetics (long press [s] key for ß), symbolic similarity (long press [*] key for ‡) and visual similarity (long press [<] key for «) which is guessable for some symbols, but isn't discoverable for others (you can search for emoji by name, but not symbols).
(On MacOS, putting a .layout file in ~/Library/Keyboard\ Layouts is enough.)
That will replace the language flag icon in your top bar with an odd icon with the command key embedded.
The second option in that menu is the keyboard viewer. You can show that and drag on a corner to make it as large as you want. Dynamically changes the keyboard as you hold down modifier keys.
> option-w ∑
This would grate on me every single time I had to use it. It's an S. Put it on option-s.
Putting double S on option-s just makes option-w for Sigma more grating. If they were going by shape, it'd be on option-b, which is equally stupid. But since it's on S, we can conclude that... someone at Apple knows German, but nobody can even name a Greek letter? That Germans are right and Greeks are wrong? What?
It's a neat hack, but that's the opposite of what I want. It means I can't look at source code and know what characters make it up, which is the primary reason I'm still putting up with plain text files (and mostly ASCII) for source code at all.
Out of habit, I occasionally type some control-letter combination which is a valid command in Emacs, but in Xcode (and other native Mac apps) inserts an invisible character, which happens to be invalid in Swift. 10 minutes later, I get a compilation error pointing at a line of code that looks perfectly valid. Frustrating!
They borrow syntax from LaTeX math-mode to allow symbol entry, such that you can type "\ne" and as soon as you hit the space after the "e", you get ≠ instead.
The language plugin for Visual Studio Code does the same thing.
Lean Prover's not exactly a programming language, but :
def foo (a b : ℕ) : ℕ → ℕ → ℕ :=
λ a b, a + b
Still looks decent.I think it has to be all or nothing though, since if it's optional the odds of getting decent editor support for it is low.
https://www.hanselman.com/blog/MonospacedProgrammingFontsWit...
if a ≠ b {
is allowed along with if a != b {
vfmt (kinda like gofmt) will be able to convert != to ≠.So far the response has been mixed.
Not everyone is open to this change.
[0] https://vlang.io
Some already do: https://docs.julialang.org/en/v1/base/math/#Base.:!=
It will be a "good problem" to have the skillz to make this useful.
Their saving grace is that humans are more intelligent interpreters.
If you ever try to translate a human-readable proof, even a fairly formal and rigorous one, into a computer proof assistant like Agda or Coq, you can see all the little ways humans cut corners.
For https://www.oilshell.org/ , which has Python/JS-like functions, I chose to use Julia's function signature design, which is as expressive as Python's, but significantly simpler in both syntax and implementation:
Manual:
https://docs.julialang.org/en/v1/manual/functions/index.html...
Comparison:
https://medium.com/@Jernfrost/function-arguments-in-julia-an...
Basically they make positional vs. named and required vs. optional ORTHOGONAL dimensions. Python originally conflated the two things, and now they're teasing them apart with keyword-only and positional-only params.
A semicolon in the signature separates positional and named arguments. So you can have:
func f(p1, p2=0, ...args ; n1, n2=0, ...kwargs)
So p2 is an optional positional argument, while n1 is a required named argument.And then you don't need * and star star -- you can just use ... for both kinds of "splats". At the call site you only need ; if you're using a named arg / kwargs splat.
----
Aside from all the numeric use cases, which look great, Julia has a bunch of good ideas in dynamic language design.
The multiline strings in Julia appear to strip leading space in a way that's better than both Python multiline strings and here docs in shell.
Also Julia has had shell-like "f-strings" since day one (or at least version 1 which was pretty recent). Python has had at least 4 versions of string interpolation before they realized that shell got it right 40 years ago :)
Because of Julia's multiple dispatch paradigm, positional arguments are special compared to named arguments because they decide what method to dispatch to (in Julia f(x) is equivalent to x.f() in object oriented language, and it's extended to all positional arguments). That means that if you called f(a, b=nothing, c=nothing), f(1, c=1) it would dispatch to f(Int64, Nothing, Int64), while if you called f(a; b=nothing, c=nothing) with the same args it would dispatch to f(Int64). In Julia named arguments are effectively a way to pass more arguments without complicating the dispatch rules, and since there is only one way to call a function (outside of optional arguments, which appears to the end user as another implementation of a function) there is no ambiguity to where it dispatches to.
So basically, every language has it's own quirks, which the syntax decisions usually reflects, and Julia's scenario is fundamentally different from Python's.
There was a rule in Google's style guide that said to only pass optional arguments with names, and required args without names. It basically enforced this separation while being slightly less flexible, because you can't have optional positional args or required named args.
Tens of millions of lines of code was written by thousands of programmers in that style, and it caused no problems at all. On the contrary it made code more consistent and readable.
def foo(bar):
if isinstance(bar, Quux):
# Treat bar as a Quux
elif isinstance(bar, Xyzzy):
# Treat bar as an Xyzzy
# etc.
I understand runtime type checking like that is considered a bit of a Python antipattern. With `singledispatch`, you can do this instead: @singledispatch
def foo(bar:Quux):
# Quux implementation
@foo.register
def _(bar:Xyzzy):
# Xyzzy implementation
With `singledispatchmethod`, you can now also do this to class methods, where the type of the first non-self/class is used by the interpreter to check the type, based on its annotation (or using the argument to its `register` method). You could mimic this behaviour using `singledispatch` in your constructor, but this syntax is much nicer.[1] https://docs.python.org/3/library/functools.html#functools.s...
[2] https://docs.python.org/3/library/functools.html#functools.s...
My immediate thought is that it's going to be hard for PyCharm to reliably point me to a function definition.
I am certain PyCharm is going to special-case these decorators in their next release.
It can also do dispatch on multiple arguments and on equality and has dispatch cache implemented as C extension (from my rudimentary measurements it seems that dispatch with cache hit is actually slightly faster than normal CPython method call).
https://docs.python.org/3/whatsnew/3.8.html#f-strings-suppor...
Will it print what foo is if I type logger.info("{foo=}")?
The way I see it, it's a necessary price to pay, and I just continue to use c-style forming strings in logging.
{ k: eval(k) for k in ('theta', 'delta.days', 'cos(radians(theta))') }
This would trivially subsume the f'{user=}' syntax for the example given: just print out the dictionary. But it'd also be useful for: filling template dictionaries; printing out status pages for HTTP webservers; returning multiple variables from a function; flattening out complex data structures; creating dispatch tables out of local functions.You could even have a syntax like locals('theta', 'delta.days') and keep it familiar.
There's an Elixir library with macros to make a map (dictionary) using the variable names passed in [1]:
iex> import ShorterMaps
...> name = "Chris"
...> id = 6
...> ~M{name, id}
%{name: "Chris", id: 6}
Though wether its a good idea or not is another question. ;) If you want to do the type of programming you're talking about you should try Elixir/Julia/Clojure(/Rust?)... or any number of other languages with macros.As far I understand the following:
print(f"your name is {name}")
is roughly equivalent to: print(f"your name is " + format(name))
What is there to parse?From: ckpt-8-0.300 To: epoch=8, val_loss=0.300
They didn't select my preferred syntax, but I'm still looking forward to using assignment expressions for testing my re.match() objects.
I haven't used 3.8.0 yet but I hope its a good one because 2020 is the year that 2.7 dies and there will be a lot of people switching.
Of course I have to ask, what was your preferred syntax?
if re.match(pattern, string) as m:
#use m
Seems a bit more Pythonic, as "as" is already used like this with "with".Either one would be fine with me and useful.
https://www.python.org/dev/peps/pep-0572/#alternative-spelli...
Of course neither of those things have been seen in the wild or production code since. Some folks complain even the simple case above is less readable.
"as" was the Pythonic choice, rather than the C/Pascalic one.
It's insanely pedantic to try and point out the difference, given the ease with which one can find the generic definition.
I know longer work at the company, but i'd wager that the client wouldnt have been willing to pay $500/hr (the rate my company billed me out at for support, features, etc), 10 years ago, to have me port the app and the handful of 3rd party dependencies to Python 3.
Moving from 2 to 3 took a while. I moved once the libs I used moved. For an "app" developer, the migration was easy once my dependencies were ready. Most of the changes were straightforward. The print statement becoming a function was easy. str becoming Unicode and introducing bytes has been a headache. Still having issues time to time with text encoding, especially with encoded text dealing with SQL Server (looking at the default CP-1252 for US English). Another one that still trips me up is is needing a "newline=''" argument when opening as CSV file.
That said, I love Python 3. Ita mostly about forgetting/relearning old syntax, which is going to happen for anything youve been using for decades and undergoes a significant change. I started with Python over 15 years ago, at version 2.2, for reference.
"Getting shit done" doesn't happen if you can't move forward eventually. I'd have substantial concerns hiring someone who can't get over Python 2.7.
I would argue that people have been "getting shit done" with C for decades, despite newer shinier flavors of the month popping up, so your argument doesn't hold water to me.
Personally, I lost faith in the Python core team because of the Py3 migration. Yes, 3.x now has a bunch of nice features that 2.x did, but almost none of them actually depend on the 3.0's breakage (as proven by Tauthon).
If you want people to follow you through a break-the-world migration then you need to motivate why it is needed and why it couldn't be done incrementally, not try tempt them with a bunch of unrelated carrots that are bundled together with the breaking change.
We're well past your argument. It's been 11 years, it is no longer reasonable to hold your particular grudge. Get on board with the modern Python or get the hell out of the conversation.
They did. I remember looking through this, and I remain convinced that the core developers were correct and there was no way to fix Unicode handling incrementally.
Do the same for the type names.
I don't write much python so there's probably something obvious I'm missing, but I don't see why they didn't use "=". Is there some significant difference between assignment expressions and assignment statements that makes it worth having distinct syntax?
[1] https://effbot.org/pyfaq/why-can-t-i-use-an-assignment-in-an...
x = False
if x = True:
sadness() If x = 1
Has been the source of many errors in many languages. Forcing assignment to be more than a 1 character difference from the equality operator prevents this.So, wiki defines levenstein distance as including substitution, but I was taught or at least remember it only being additions and deletions?
Perhaps more importantly, “=” and “==” are more visually similar than “==” and “:=” and also easier to mistakenly type for each other.
Given that Python is statement-oriented, yes, having statements visually distinct from similar expressions is important.
It's also important to avoid making the equality operator and the assignment operator visually similar or easy to typo one for the other, which is arguably the bigger need for “:=” vs “=”, since “=” and “==” are quite similar and easy to accidentally mistype for each other.
The fact that this is the go-to example that everybody is using in justifying the introduction of the assignment expression convinces me that the real problem lies with the re module's API.
(To be clear: I will also be using assignment expressions for this case, but I don't think assignment expressions are really in line with the overall design of Python.)
if my_map.get(key) is not None:
// do something with my_map[key] match my_map.get(key):
case None:
// do thing (or nothing, i.e., pass)
case Some(val):
// do other thing
Scala seems to have figured out how to get pattern matching over arbitrary data (i.e., not statically-defined algebraic datatypes). I'd like to see this come to Python.(Normally I'd fight for pattern matching to always provide static type safety guarantees, but in Python it seems completely reasonable to omit such checks.)
EDIT: But you're absolutely right that this is a place where assignment expressions will be used regularly, so thank you for pointing that out!
> “Final” variables, functions, methods and classes. See PEP 591, typing.Final and typing.final(). The final qualifier instructs a static type checker to restrict subclassing, overriding, or reassignment:
> pi: Final[float] = 3.1415926536
As I understand it, this means Python now has a way of marking variables as constant (though it doesn't propagate into the underlying values as in the case of C++'s `const`).
The equivalent Java:
final float pi = 3.1415926536;For name mangling, I think you need to start your variable with underscores, and then it won't be accessible for reading outside the module either?
$ cat >foo.py
__FOO = 1
class Foo:
__FOO = 2
$ cat >bar.py
import foo
print(foo.__FOO)
print(foo.Foo()._Foo__FOO)
$ python3 bar.py
1
2 class Foo:
bar = 'public'
__bar = bar # stashedThey have a lot of code samples and examples about how and when to use the new features.Personally, I love the new debugging support in F strings.
> A dictionary type with per-key types.
Ah, I've been waiting for this. I've been able to use Python's optional types pretty much everywhere except for dictionaries that are used as pseudo-objects, which is a fairly common pattern in Python. This should patch that hole nicely.
It was a major missed opportunity to provide a properly duck-typed Dict type, which by definition would allow untyped key-value pairs to be added to a "partially-typed" dictionary.
Gradual typing in general is a massive win for many kinds of real-world problem solving, but when you make it as hard as Python has to introduce partial types to a plain data object, you're leaving a lot of developers out in the cold.
I love MyPy and the static type hints since 3.6, but structural subtyping is superior and so obviously more Pythonic than nominal, yet support for structural subtyping keeps lagging behind.
Quite happy with the new SyntaxWarning for identity comparison on literals and missing commas :)
Especially neat is also the `python -m asyncio` shell which allows you to run top-level awaits in your repl instead of needing to start a new event loop each time!
I understand that Python can't break all kinds of code (as the 2->3 conversion is still a pain), but still I imagine a Python-esque language without all the warts that Python have with it's 'organic' growth.
It is: "There should be one-- and preferably only one --obvious way to do it."
The core of that statement is "There should be one obvious way to do it" - there's no "only" there, it just says that when you need to do something, there should be an/some obvious way to do it. Then, preferably, that should be the only obvious way — though of course that doesn't preclude there being many other less-obvious ways.
With string interpolation we've certainly now got multiple ways to do it; that doesn't violate the principle. I agree that at least two of those are "obvious" (f-strings and .format()), and you could argue that %-interpolation is obvious too — but none of that is in violation of the principle that there should be an obvious way to do it, just of the preference that there's only one.
Finally it's worth remembering where this idea came from: it was in contrast to the perl mantra of "there's more than one way to do it", and a reaction to the resultant confusion frequently experienced when reading someone else's perl code — this was python saying "we're not perl, we value clarity and comprehension". I think that much of that problem was bound up in perl's syntax choices, and that in the cases in python where there's more than one way to do it (say, string formatting, dataclasses/attrs/namedtuples/etc.), it's usually pretty obvious what machinery is actually being used. When was the last time you looked at a line of python and said "I have no idea what the fuck is going on here?" That was a frequent experience with perl in the heady days of the late 1990s.
And you could 'import something from future' to opt-in to making those warnings errors right now.
I forget the exact details though.
Hard to understand why this is so far down. This is fantastic news!
I've been coding in algolesque languages for 20 years and hiding assignments inside of expressions instead of putting them on the left like a statement has always tripped me up.
I’ll just leave this here:
“There should be one—and preferably only one—obvious way to do it.”
Which is fine by me, it was a silly idea to begin with. What you really want is separated concerns that compose well, obvious here doesn't mean anything over there.
Wow. I believe it's a stated goal of CPython to prefer simple maintainable code over maximum performance. But relatively low-hanging fruit like this makes me wonder how much overall CPython perf could be improved by specialising for a few more of these.
I really like the walrus operator, but I didn't realize how many "you shouldn't do this" and "this is too hard already" cases exist where they are discouraging using the walrus operator.
v = obj?.prop1?.prop2 ?: "default"
instead of long if conditions: v = obj.prop1.prop2 if obj and obj.prop1 and obj.prop1.prop2 else "default"
A PEP for Python 3.8 existed but has been deferred: https://www.python.org/dev/peps/pep-0505/Presentation: https://docs.google.com/presentation/d/1a3Zoav7NmeN_gXjGcV_l...
Video from PyBay: https://www.youtube.com/watch?v=OtdQN24Z5MA
It covers assignment expressions in some depth, as well as highlights elsewhere.
if (n := len(a)) > 10:
print(f"List is too long ({n} elements, expected <= 10)")
Um, no it doesnt?: a1 = [10, 20, 30]
n1 = len(a1)
if n1 > 2:
print(f'{n1} is greater than two')Remember: every variable is an assignment into a dict, and every lookup a query into a dict. Reducing name lookups and assignments can yield good speedup in tight loops. For instance, caching os.path.join, os.path.split into local names can significantly speed up tight loops iterating over a filesystem. For example, os.path.split is potentially 5 dictionary lookups. Checking locals, nonlocals and globals for os. Then another to find path, and a final one for split. And this happens at runtime, for every invocation.
Edit: I realize that local name lookup doesn't need to follow the result of the locals() built-in function.
DUP_TOP:
PyObject *top = TOP();
Py_INCREF(top);
PUSH(top);
FAST_DISPATCH();
TOP: (stack_pointer[-1])
LOAD_FAST: PyObject *value = GETLOCAL(oparg);
if (value == NULL) { /* throw */ }
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
GETLOCAL: (fastlocals[i])
So looks like either way, you have an array access of something definitely in cache, a Py_INCREF, a PUSH, and a FAST_DISPATCH. The walrus operator saves you a null-check, but that check is probably skipped right over by the branch predictor, as it always throws. I'd bet the performance is indistinguishable, but I'd be interested to see for real.I don't really have a point, except that Python 3 feels like a moving target.
Considering this f-string example taken from another announcement:
f"Diameter {(diam := 2 * r)} gives circumference {math.pi * diam:.2f}"
This is valid Python 3.8, but it's not valid in Python 3.7 (no walrus operator). And removing the walrus operator still doesn't work in Python 3.5 (no f-strings). On top of that, other comments already mention how f-strings have lots of weird corner cases anyway.The entire point of Python in my circle of friends was that it made programming easy. Instead, I feel more and more in need of those "It works in my machine!" stickers. And good luck solving these issues if you are not a full-time programmer...
The minor releases are always backward-compatible, so just run the latest version and everything will work.
Asyncio was the worst culprit, as it essentially introduces an inner-platform with its own dataflow semantics, but at least there the upsides were large and tangible.
if m=whatever():
do_something
Why create a new assignment operator? For all the talk about making code not confusing, etc., some of these decisions sure seem nonsensical.Oh, OK, it confuses passing arguments into a function by name? Really? C'mon.
Also, I can't understand the nearly religious rejection of pre and post increment/decrement (++/--) and, for the love of Picard, the switch() statement.
I enjoy using Python but some of these things are just silly. Just my opinion, of course. What do I know anyhow? I've only been writing software for over thirty years while using over a dozen languages ranging from machine language (as in op codes) to APL and every new fad and modern language in between.
As I watch languages evolve what I see is various levels of ridiculous reinvention of the wheel for very little in the way of real gains in productivity, code quality, bug eradication, expressiveness, etc. Python, Objective-C, Javascript, PHP, C# and a bunch of other mutants are just C and C++ that behave differently. Sure, OK, not strictly true at a technical level, but I'll be damned if it all doesn't end-up with machine code that does pretty much the same darn thing.
The world did exist before all of these "advanced" languages were around and we wrote excellent software (and crappy software too, just like today).
What's worse is that some of these languages waste a tremendous amount of resources and clock cycles to do the same thing we used to do in "lower" languages without any issues whatsoever. Mission critical, failure tolerant, complex software existed way before someone decided that the switch() statement was an abomination and that pre and post increment/decrement are somehow confusing or unrefined. Kind of makes you wonder what mental image they have of a programmer, doesn't it? Really. In my 30+ years in the industry I have yet to meet someone who is laid to waste, curled-up into a fetal position confused about pre and post increment/decrement, switch statements and other things deemed too complex and inelegant in some of these languages and pedantic circles.
Geez!
</rant off>
There's something to be said for a more stable, if less elegant, language.
f"Diameter {(diam := 2 * r)} gives circumference {math.pi * diam:.2f}"
If this is not write-only code, I don't know what is.> This module provides support for maintaining a list in sorted order without having to sort the list after each insertion.
Edit: it was coined a few times on the python-ideas mailing list but it seems it just died a silent death there. https://mail.python.org/archives/list/python-ideas@python.or...
FWIW: https://github.com/calroc/xerblin/blob/master/xerblin/btree....
> The module [bisect] ... uses a basic bisection algorithm to do its work.
~ https://docs.python.org/3.0/library/bisect.html
> Binary search runs in logarithmic time in the worst case, making O(log n) comparisons...
~ https://en.wikipedia.org/wiki/Binary_search_algorithm
edit: Unless you mean moving the data?
I don't think I've ever cared to iterate over map values based on the alphanumeric ordering of their keys.
It's basically copy-on-write. The old and new map share all but O(log n) data.
You could probably do something like that with an unsorted map, but it's a good fit for a sorted one.
return (map[hash(key) % map_size])->value
or approximately that (I haven't written C in a while).Then iteration over objects in the array is consistent: you just iterate over the dense array. Removal from the dense array is done by tombstoning, and possibly eventual compaction. This is essentially how python's current hash table implementation works.
IIRC, if you're table can assume only insertions, this actually becomes really, really nice as a persistent data structure, since you can replace the backing vector with a backing linked list, and you only really lose out on iteration speed. Then you further extend that by swapping the linked list to a tree, and you share the entire backing structure.
[1]: And you can use an increment-only mutation counter instead of a timestamp to make this pure.
So how does the timestamp have any impact on persistence?
(We agree on basically everything you write.)
> [1]: And you can use an increment-only mutation counter instead of a timestamp to make this pure.
Agreed. That's what I'd call a logical timestamp or a logical clock.
> IIRC, if you're table can assume only insertions, this actually becomes really, really nice as a persistent data structure, since you can replace the backing vector with a backing linked list, and you only really lose out on iteration speed. Then you further extend that by swapping the linked list to a tree, and you share the entire backing structure.
That still doesn't tell you anything at all about how you make the hashtable itself persistent.
One simple way would be to just introduce an arbitrary order on your hashes, eg compare them as if they were ints, and then stick them in an ordered map container. Of course, that's just a round-about way to impose an arbitrary order on your keys. Works perfectly well, it's just not too interesting.
To come back to your original question:
> What's the value of a sorted map over an ordered map? > I don't think I've ever cared to iterate over map values based on the alphanumeric ordering of their keys.
In the cases we talked about making the keys comparable is only a means to an end, and any arbitrary order will do. That's the most common use case. Iterating over the keys in insertion order is also often useful.
In practice, I did come across some use cases that make genuine use of the order of keys. Eg when I was interested in (automatically) keeping track of the highest or lowest key or quantiles.
You can use a sorted map as a priority queue easily. A min heap would give you O(1) access to the minimum item, but if you actually want to pop it, it's O(log n) anyway. A sorted map as tree gives you O(log n) for basically all operations. For most uses that's either good enough, or doesn't even make a difference at all compared to a heap. Using your sorted map as a priority queue gives you arbitrary priority updates in O(log n) for no extra implementation complexity.
Sorted maps also came in handy when I was implementing geometric algorithms where I wanted to sweep a scanline over points. Or for divide and conquer algorithms over space.
A sorted map is also useful as a basic datastructure to build step functions on top of. (https://en.wikipedia.org/wiki/Step_function) A simple application is solving the infamous skyline problem.
When you want to store your data structure, having similar keys close together can help with compression. Google uses sorted string tables (sstable) show that principle quite well.
Something like:
r1 = foo(x:=bar)
r2 = baz(x)`f'{username= }'` 'username="tolgahanuzun"'
I like it.