I was looking into Python 3.10's Structural Pattern Matching syntax, and I want to refactor one of my code that uses if-else
, using structural pattern matching. My code works, but I'm trying to find out if there's a better way to deal with a particular section.
Let's consider this simple example problem, let's say I have the following list:
data = [ # index
[1, 2, 5, 3, 4], # 0
[7, 5, 8, 4, 9], # 1
[2, 3, 4, 4, 5], # 2
[1, 3, 1, 6, 7], # 3
[5, 6, 0, 7, 8], # 4
[4, 3, 0, 7, 5], # 5
[4, 4, 4, 5, 4], # 6
[5, 2, 9, 3, 5], # 7
]
What I want to do is:
IF: (there is a `4` *or* `5` at the *beginning*)
prepend an `'l'`.
ELIF: (there is a `4` *or* `5` at the *end*)
prepend a `'r'`.
ELIF: (there is a `4` *or* `5` at *both ends* of the list)
IF: (Both are equal)
prepend a `'b2'`,
ELSE:
prepend `'b1'`
ELSE:
IF : (There are at least **two** occurrences of `4` *and/or* `5`)
prepend `'t'`
ELSE:
prepend `'x'`
Each inner_list may contain arbitrary amount of elements.
Expected Result
index append_left
0 'r'
1 't'
2 'r'
3 'x'
4 'l'
5 'b1'
6 'b2'
7 'b2'
Now, I can do this using structural pattern matching, with the following code:
for i, inner_list in enumerate(data):
match inner_list:
case [(4 | 5) as left, *middle, (4 | 5) as right]:
data[i].insert(0, ('b1', 'b2')[left == right])
case [(4 | 5), *rest]:
data[i].insert(0, 'l')
case [*rest, (4 | 5)]:
data[i].insert(0, 'r')
case [_, *middle, _] if (middle.count(4) + middle.count(5)) >= 2: ## This part ##
data[i].insert(0, 't')
case _:
data[i].insert(0, 'x')
pprint(data)
Output
[['r', 1, 2, 5, 3, 4],
['t', 7, 5, 8, 4, 9],
['r', 2, 3, 4, 4, 5],
['x', 1, 3, 1, 6, 7],
['l', 5, 6, 0, 7, 8],
['b1', 4, 3, 0, 7, 5],
['b2', 4, 4, 4, 5, 4],
['b2', 5, 2, 9, 3, 5]]
The problem is the ##
marked block above. Of course I can move that part inside the block, and check there, but I was wondering whether the if
part can be avoided altogether, i.e. some pattern that would match at least two 4
and/or 5
s.
EDIT The [_, *middle, _]
part is actually me being suggestive that I am looking for a pattern to match the scenario, I'm aware, in actuality this could be done like: case _ if sum(i in (4, 5) for i in inner_list) >= 2
[(4 | 5), *rest]
is unfortunate is because it slices the list which can be an expensive operation. Now, for every case you are performing a seperate slice.[(4 | 5), *rest]
slices,[_, *middle, _]
slices etc. The syntax in itself is clear, but not efficient. \$\endgroup\$[_, *middle, _]
, which is unnecessary in every sense. At least the*rest
part would be useful if I needed to use therest
(which I don't at the moment). \$\endgroup\$[(4|5), *rest]
isn't necessarily inefficient. There can only be one starred field, so the implementation could try to match the other parts of the pattern first. If the other parts succeed, the*rest
matches whatever is left over--it always succeeds. So the actual slicing only has to occurs for a successful match. If you use*_
instead of*rest
the slicing isn't needed at all. Patterns like this, that match the beginning and/or end of a sequence, would be rather common and would be a good optimization target for the interpreter developers. \$\endgroup\$