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.
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')
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With