Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python question about constructor and match statement

Tags:

python

I'm learning Python using the tutorial on the official website and I came across an interesting example that I couldn't quite parse.

class Point:
    x: int
    y: int

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

In a previous example, it mentions that point is an (x, y) tuple but I don't know if it applies to this example. In any case, I don't quite understand how the function where_is is supposed to work. I've tried this example by putting in a tuple into the argument for where_is, but all it will return is the last case: Not a point. I've tried placing "Point()" into the argument, and that does return "Somewhere else", although that still doesn't explain how the first 3 cases are supposed to work. I've had success with it returning Origin, Y = whatever, or X = whatever2 if I change the class Point to read x = 0, y= 0, or x = 0, y = whatever, or x = whatever2, y = 0.


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

class Point:
    x=0
    y=0

>>> where_is(Point())
    Origin

class Point:
    x=1
    y=0

>>> where_is(Point())
    X=1

class Point:
    x=0
    y=1

>>> where_is(Point())
    Y=1

However, coming from a C++ background, I'm not sure if this is what the example is even trying to show, and if it is, why I can match x or y to random values in such a manner.

The tutorial then goes on to say: "You can use positional parameters with some builtin classes that provide an ordering for their attributes (e.g. dataclasses). You can also define a specific position for attributes in patterns by setting the match_args special attribute in your classes. If it’s set to (“x”, “y”), the following patterns are all equivalent (and all bind the y attribute to the var variable):"

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

And I have no idea what it's trying to say. Any help would be appreciated.

like image 248
Mezzoforte Avatar asked May 21 '26 22:05

Mezzoforte


2 Answers

Each case clause uses something that looks like an object construction, but it's just syntax used to match an object of type Point with various values for their x and y attributes.

First, start with a complete example of Point that will work.

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int

Now you'll call where_is with an appropriately constructed object:

>>> where_is(Point(0, 0))
Origin
>>> where_is(Point(9, 0))
X=9
>>> where_is((3, 5))
Not a point

Point is a tuple in the mathematical sense, namely an "ordered" (or at least discriminated) pair of values. An instance of Point is not an instance of the Python type tuple.

Point(x=0, y=0) as a class pattern only matches Point instances whose x attribute is 0 and y attribute is 0. Note it doesn't really matter how the object was created, and x and y refer directly to instance attributes, not necessarily keyword arguments used by __init__.

match_args basically provides a "mapping" of positions to attribute names. Consider this definition of Point:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Now, an attempt to match against Point(0, 0) rather than Point(x=0, y=0) would fail, because the match statement doesn't know which positional argument in the pattern corresponds to which argument. Defining match_args = ("x", "y") explicitly pairs the 0th positional argument to the attribute x and the 1th argument to y, which would let you write

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

Note that one of the things the @dataclass constructor does is set __match_args__ for you based on the defined fields.

>>> @dataclass
... class Point:
...   x: int
...   y: int
...
>>> Point.__match_args__
('x', 'y')
like image 74
chepner Avatar answered May 24 '26 11:05

chepner


The match statement you're asking about is a fairly new feature in Python. It was added in Python 3.10, and introduced in three PEPs, 634 (which gives a more formal specification of the syntax), 635 (which describes the motivation for the feature), and 636 (which is a tutorial, and likely the template for the documentation you were reading).

While a match looks a bit like a switch statement from C++, it has some important differences. Each case statement uses a special matching syntax that can describe class instances in a compact way. The value given in the match statement is checked to see if it matches each of the case clauses one by one, like a chain of if/elif conditions.

Your question looks to mostly be about the case syntax, which is a bit surprising if you don't know what it's doing. By "calling" a class in the case, you're asking to match only instances of that class. Thus case Point() matches any point object (that hasn't already been matched by a previous case clause).

The "arguments" to the Point in the case statement aren't real arguments being passed to a real call to construct an instance of the class. Rather, it's fancy syntax for matching attributes. A statement like case Point(x=0, y=0) is equivalent to if isinstance(obj, Point) and obj.x == 0 and obj.y == 0. If the argument has unbound names in it, like the y=y example, then the conditional part of the match is followed by an assignment, y = obj.y (the name on the left side of the assignment is weirdly the right side of the argument in the case statement).

The example is made a bit more confusing than necessary by the fact that the classes being defined don't have any constructors, the attributes are hard-coded class variables. Adding a @dataclass decorator would make them a lot more logical, that's the whole idea behind equating constructor arguments with attributes.

like image 35
Blckknght Avatar answered May 24 '26 12:05

Blckknght



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!