Python match/case using global variables in the cases (solvable by use of classes)

Viewed 2087

I wanted to implement match/case by matching inputs stored in variables. The intended logic should be like:

match x:
    case y:
        print(...)    
    case z:
        print(...) 

Turns out this approach doesn't work.
It causes this error: "Irrefutable pattern is allowed only for the last case statement",
which I believe occurs because somehow the variable next to the first case is assigned to the value of the variable next to match: if I go and debug, by the "case y" line, y has its value changed to whatever is stored in x.

That, however, does not happen if everything belongs to a class, as in:

class Vars:
    x = int(input())
    y = int(input())
    z = int(input())
match Vars.x:
    case Vars.y:
        print("something")
    case Vars.z:
        print("anything")

This approach causes no errors whatsoever.

Why is that? I mean, what makes a class variable a refutable pattern?

2 Answers

I find the new match command (officially: structural pattern matching) quite a complex thing. It took 3 PEPs to document and explain this new Python feature (PEP-634 Specification, 635 Rationale and 636 Tutorial).

I consider myself a novice in this topic but would like to answer this question. So, why does case y: not work and case Vars.y: works just fine?

case y: does not mean "compare with y" as one might have expected. It is a "capture pattern", meaning it always matches anything and stores it into the variable y.

Python calls a pattern that always matches "irrefutable" and has some special rules for it. In short: any irrefutable pattern must be the last one (which is the source of the error mentioned in the question.) But as I explained, it wouldn't work even with this error corrected.

On the other hand, case Vars.y is a "value pattern"; the dot makes the difference. As the names suggests, it means "compare with this value".

There are many other pattern types. To summarize, it is hardly possible to understand the matching rules by mere intuition. And I don't mean some obscure details. A good tutorial (e.g. PEP-636) is recommended.

Structural pattern matching introduces a match statement and two new soft keywords: match and case. As the name suggests, it can be used to match a given value against a list of specified "cases" and act accordingly to the match.

for i in range(100):
    match (i % 3, i % 5):
        case (0, 0): print("3 and 5")
        case (0, _): print("3")
        case (_, 0): print("5")
        case _: print(i)

• The (0, 0) pattern: This will match a two-element sequence if both elements are equal to 0.

• The (0, _) pattern: This will match a two-element sequence if the first element is equal to 0. The other element can be of any value and type.

• The (_, 0) pattern: This will match a two-element sequence if the second element is equal to 0. The other element can be of any value and type.

• The _ pattern: This is a wildcard pattern that will match all values.

class Point:
    x: int
    y: int
    def __init__(self, x, y):
        self.x = x
        self.y = y
def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

Point(x=0, y=0): This matches if point is an instance of the Point class and its x and y attributes are equal to 0.

Point(x=0, y=y): This matches if point is an instance of the Point class and its x attribute is equal to 0. The y attribute is captured to the y variable, which can be used within the case block.

Point(x=x, y=0): This matches if point is an instance of the Point class and its y attribute is equal to 0. The x attribute is captured to the x variable, which can be used within the case block.

Point(): This matches if point is an instance of the Point class.

_: This always matches.

Pattern matching can look deep into object attributes. Despite the Point(x=0, y=0) pattern looking like a constructor call, Python does not call an object constructor when evaluating patterns. It also doesn't inspect arguments and keyword arguments of __init__() methods, so you can access any attribute value in your match pattern.

Match patterns can also use "positional attribute" syntax, but that requires a bit more work. (PEPs: 634, 635, and 636)

Related