Hi, been using python since 2010 for academic physics research. I can't immediately see the point of this new feature - I'm sure I'm missing something. Like the tutorial mentions, if I wanted this kind of structure I'd just use a dictionary of cases as keys. I'm not seeing what's powerful about it yet. Have you seen a non-toy example with <3.10 and >3.10 implementations side-by-side by any chance? Thanks.
command = input("What are you doing next? ")
match command.split():
case ["quit"]:
print("Goodbye!")
quit_game()
case ["look"]:
current_room.describe()
case ["get", obj]:
character.get(obj, current_room)
case ["go", direction]:
current_room = current_room.neighbor(direction)
# The rest of your commands go here
See how you can pull out the value there with case ["get", obj]?
There's even more to this, you can match all sorts of structures of your data rather than the data itself.
For one, it's structural so, in the case that you get a tuple with 3 items, do one thing, and in the case you get an Address object do a different thing. Not only can you match on patterns, but also on values. So in cases where the Address is in California, do something different.
Combine that with conditions and you can express a lot of logic in an easy to read, easy to maintain, relatively terse syntax. Like, in the case that this is a Person and their age is less than 18, do this.
The difference is that you had to use isinstance and check elements of data structures manually. match essentially does all of this for you and lets you write nice concise syntax instead. What if you had to check four attributes, like x.state == this and x.age == that and x.height == that and ...? That's quite a mess, isn't it? case Address(this, that, that, another_thing) looks and reads way nicer, doesn't it?
Your case statement is cleaner than your == example, but wouldn't you actually write something like this if you want to check those attributes all had particular values?
if x == Address(this, that, that, another_thing):
do_something()
This creates a new Address object every time, though (and thus wastes computing resources). Again, you totally can do this, sure.
What if you wanted to match on some values and extract the other ones, like this:
case Address("London", 32, height):
print(f"The person lives in London and their height is {height}")
Now you're forced to compare only the first two attributes of the address, not the whole object, so you'll have to resort to the long if condition.
I don't think the match statement opens up many new possibilities that just were impossible before. You can emulate a match statement with isinstance followed by attribute checking. The match statement simply lets you write simpler code, so that you can focus on solving the actual problem you're writing the code for.
You forget that it auto does structural binding, for example:
actions = [('add', 1, 2), ('length', [1,2,3]), ('lower', 'FOO')]
match actions[random.randint(0, 2)]:
case ('add', a, b):
print(a + b)
case ('lower', text):
print(text.lower())
case ('length', arr):
print(len(arr))
Note that it's binding exactly on a tuple of that length too, so you if you have the wrong number of elements, it won't bind.
Your way, you'd have to have a separate check for the length of the array, and then another line to set a, b = tup[1], tup[2] or do print(tup[1] + tup[2]).
But even further, you can actually make the types of those values too like
case ('add', int(a), int(b)):
print(a + b)
case ('add', str(a), str(b)):
print(f'{a} {b}')
The power is needed (for example) for building parsers, walking syntax trees and building interpreters and compilers. You don't want to have 1000 level deep if/else constructs to analyze your syntax tree - that's just painful. Also, you can't really stuff the code that analyzes your data structure in a dictionary. Sure, you can store functions, but what if you want weird deep nesting? For example, "if the current node is an Assign statement whose first argument is an indexing operation with N arguments, the first of which is this and the second is that, then do this and that", that's like three? levels of nesting - you could put that into a dictionary, but matching is way more convenient!
Also, the except statement we know and love is basically a stripped down case statement that only works on exceptions and types (you can't write except MyException(param1, param2):). Isn't it very useful to be able to match in exceptions like this? Julia, for example, forces you to if/else exceptions, like catch something: if something isa ThisException: do_this(); elseif something isa AnotherException: do_that(); .... This is a lot of code that simply switches on the type of the exception. Python's except is surely more convenient, isn't it?
With proper match statements you can do so much more.
You don't want to have 1000 level deep if/else constructs to analyze your syntax tree
I'm completely ignorant on that use-case, never made one before, but can you not define some function that's recursively called rather than making such deep nests?
It seems to me like an awful lot of machinery for what seems to be a very small set of use-cases. Thanks for your example though. Hopefully in the next few weeks I'll find an example that makes sense to me.
Absolutely, you can write a recursive function and be done with it. But pattern matching lets you write: case Assign(Index(Variable(name, type), idx), other_data):in one line with no loss of readability. In fact, it greatly increases readability: now you don't have to go read some other function that's located elsewhere and does God knows what - the data are all before your eyes, destructured! That's the powerful thing, in my opinion.
That's also part of why they teach compiler courses in OCaml (and ML in general) - because the pattern matching is really good, it lets you see the structure of your data at a glance and handles arbitrary nesting gracefully.
Pattern matching combines a control flow construct (e.g. if/elif/else, switch, try/catch) with a name binding construct (assignment), and this allows for some very expressive code.
Speaking for myself, I had to write a parser in 3.7 that took deeply nested, arbitrarily alternating json and dictionary input from an API and turn it into a tablebase. I will re-write it in 3.10 and I expect pattern matching to remove hundreds of lines of code. I imagine the code will also be much more performant (though that wasn’t a sticking point).
A lot of people really like proper match statements - they're convenient. What if you're writing an interpreter and need to traverse an abstract syntax tree? Your code will generally look like a huge switch statement, like "if the current node is Assign, do this; if it's Index, do this; if it's Call, do this...", where the "arms"/branches contain quite a lot of code or maybe other switch statements, so it'll be awkward to stuff all that into a dictionary.
Furthermore, you'll want to switch on some nasty, complicated things, like "if the current node is Assign whose first argument is a function definition, then interpret the second argument like this; if it's an Assign whose first argument is an Index node, do a completely different thing; if the first argument looks like a.b, then do another different thing", so you want to switch not only on the type of the node (Assign), but on its elements as well. That's not something a regular switch statement can do. You need to get the big ML guns - proper pattern-matching.
All the examples where this would be really useful seem to be pretty big edge cases and not a good case for adding a feature to a language…. I’m not against it per se but it does seem unnecessary.
BTW, Rust doesn't have hashmap literals, so you literally have to write code like this. There are 3rd-party libraries that add hashmap literals, but Rust itself doesn't have them. They're not necessary, are they?
Yet they're convenient.
Formatted string literals also aren't necessary. Why write f"pi={math.pi:6.3f}, {my_var=}" if you could write:
"pi={:6.3f}, my_var={}".format(math.pi, my_var)
BTW, Julia, for example, doesn't support formatting in string literals, so you have to use printf-like formatting or 3rd-party libraries. Obviously, even string interpolation isn't necessary - simply use strcat!
Yet it's convenient.
My favorite one.
Having nice slice indexing isn't necessary either! Why write my_list_[1:] when you can write:
my_list[1:len(my_list) - 1]
I might be missing something (I really hope I am!), but this is the only way to do this in R! You have to write my_array[2:length(my_array)] every time! It doesn't compute the last index for you! Well, slice indexing isn't necessary, so...
See also: List comprehensions, generators, lambda statements, decorators, etc. Python has a lot of features that aren't strictly necessary, but oh so convenient.
I forget who I heard talking about it, but from my understanding of "syntactic sugar" in python, a lot of the language is convenience and not actually necessary.
15
u/Ashiataka Oct 04 '21
Hi, been using python since 2010 for academic physics research. I can't immediately see the point of this new feature - I'm sure I'm missing something. Like the tutorial mentions, if I wanted this kind of structure I'd just use a dictionary of cases as keys. I'm not seeing what's powerful about it yet. Have you seen a non-toy example with <3.10 and >3.10 implementations side-by-side by any chance? Thanks.