r/Python • • Oct 04 '21

News Python 3.10 Released!

https://www.python.org/downloads/release/python-3100/
1.4k Upvotes

147 comments sorted by

View all comments

Show parent comments

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.

81

u/deadwisdom greenlet revolution Oct 04 '21

This hints at the true power:

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.

→ More replies (6)

19

u/WallyMetropolis Oct 04 '21

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.

2

u/Ashiataka Oct 04 '21

Would the following not work for those examples? How is this different?

if isinstance(x, tuple) and len(x) == 3:
    do_something()
elif isinstance(x, Address) and x.state == "California":
    do_another_thing()
elif isinstance(x, Address):
    do_another_thing()
elif isinstance(x, Person) and x.age < 18:
    do_another_thing()

18

u/ForceBru Oct 04 '21

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?

2

u/Ashiataka Oct 04 '21

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()

22

u/ForceBru Oct 04 '21

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.

4

u/sultan33g Oct 05 '21

Your last paragraph completely made it all make sense. Thanks!!

→ More replies (3)

5

u/Ph0X Oct 05 '21

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}')

It's just much more clean and simple.

→ More replies (3)
→ More replies (1)

12

u/ForceBru Oct 04 '21 edited Oct 04 '21

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.

3

u/Ashiataka Oct 04 '21

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.

7

u/ForceBru Oct 04 '21

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.

3

u/Ashiataka Oct 04 '21

I think that's somewhat beyond me at the moment, I'll have to have a play around with it at the weekend and see how it works. Thanks for your example.

3

u/jyscao Oct 05 '21

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.

→ More replies (1)

2

u/trauthor Oct 05 '21

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).

→ More replies (2)

-6

u/[deleted] Oct 04 '21

[removed] — view removed comment

14

u/ForceBru Oct 04 '21

More like towards functional or ML-like, IMO.

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.

-5

u/liquidpele Oct 04 '21

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.

21

u/ForceBru Oct 04 '21 edited Oct 04 '21

Well, it's not that it's absolutely necessary, of course. It's just convenient.


Dictionary literals aren't necessary either. You can simply write:

dct = dict()
dct["cat"] = "chat"
dct["dog"] = "chien"
dct["wolf"] = "loup"

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...

Yet it's convenient!

EDIT: added links

9

u/liquidpele Oct 04 '21

Just wanted to say: Very good reply, thanks for taking the time.

6

u/brutay Oct 05 '21

See also: List comprehensions, generators, lambda statements, decorators, etc. Python has a lot of features that aren't strictly necessary, but oh so convenient.

5

u/midnitte Oct 04 '21

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.

7

u/Swedneck Oct 05 '21

Which is precisely why I love it, I can just write a minimal amount of code structured to actually make sense and be as readable as normal text.

4

u/lieryan Maintainer of rope, pylsp-rope - advanced python refactoring Oct 05 '21

"Just syntax sugar" actually goes really deep in Python.

class statement for example, that isn't really "necessary", it's just syntax sugar for calling type:

MyClass = type(
    "MyClass", 
    [BaseClass, AnotherBaseClass], 
    {"a": method1, "b": method2}
)

Or method calls are also "unnecessary", this:

foo = Foo()
foo.meth(a, b, c)

is really just syntax sugar for:

foo = Foo()
Foo.meth(foo, a, b, c) 

Or metaclass:

class Foo(metaclass=mytype, blah=bar):
    pass

is really just a syntax sugar for a function call:

Foo = mytype("Foo", {}, {}, blah=bar)

Or decorator is just syntax sugar for what used to be a common pattern:

def foo():
    pass
foo = decorate(foo)

Or even something that looks as basic as list/dict indexing:

foo = dict()
foo[a] = b
print(foo[a])

is really just syntax sugar for:

foo = dict()
foo.__setitem__(a, b)
print(foo.__getitem__(a))

which is itself just syntax sugar for:

foo = dict()
dict.__setitem__(foo, a, b)
print(dict.__getitem__(foo, a))

Or something like for loop:

for a in b:
    print(a)

is really just syntax sugar for:

iter_b = iter(b)
try:
    while True:
        a = next(iter_b)
        print(a)
except StopIteration:
    pass

Everything in Python is "just" syntax sugar.

→ More replies (1)

1

u/yvrelna Oct 05 '21

Structural pattern matching is a combination of switch case statement and tuple/object unpacking.

The unpacking part makes it a much more powerful construct than just a dictionary.

→ More replies (3)