La coincidencia de patrones finalmente se ha llevado al aniversario menor de la tercera pitón. El concepto en sí difícilmente se puede llamar nuevo, ya se ha implementado en muchos lenguajes, tanto de nueva generación (Rust, Golang) como los que ya tienen más de 0x18 (Java).
La coincidencia de patrones fue anunciada por Guido van Rossum , el autor del lenguaje de programación Python y un "dictador generoso de toda la vida"
Mi nombre es Denis Kaishev, y soy un revisor de código para el curso para desarrolladores de Middle Python . En esta publicación, quiero contarte por qué Python tiene coincidencia de patrones y cómo trabajar con él.
Sintácticamente, la coincidencia de patrones es esencialmente la misma que en otros idiomas:
match_expr:
| star_named_expression ',' star_named_expressions?
| named_expression
match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
patterns: value_pattern ',' [values_pattern] | pattern
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: '|'.closed_pattern+
closed_pattern:
| capture_pattern
| literal_pattern
| constant_pattern
| group_pattern
| sequence_pattern
| mapping_pattern
| class_pattern
capture_pattern: NAME !('.' | '(' | '=')
literal_pattern:
| signed_number !('+' | '-')
| signed_number '+' NUMBER
| signed_number '-' NUMBER
| strings
| 'None'
| 'True'
| 'False'
constant_pattern: attr !('.' | '(' | '=')
group_pattern: '(' patterns ')'
sequence_pattern: '[' [values_pattern] ']' | '(' ')'
mapping_pattern: '{' items_pattern? '}'
class_pattern:
| name_or_attr '(' ')'
| name_or_attr '(' ','.pattern+ ','? ')'
| name_or_attr '(' ','.keyword_pattern+ ','? ')'
| name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
signed_number: NUMBER | '-' NUMBER
attr: name_or_attr '.' NAME
name_or_attr: attr | NAME
values_pattern: ','.value_pattern+ ','?
items_pattern: ','.key_value_pattern+ ','?
keyword_pattern: NAME '=' or_pattern
value_pattern: '*' capture_pattern | pattern
key_value_pattern:
| (literal_pattern | constant_pattern) ':' or_pattern
| '**' capture_pattern
Puede parecer complicado y confuso, pero en realidad todo se reduce a algo como esto:
match some_expression: case pattern_1: ... case pattern_2: ...
Parece mucho más claro y agradable a la vista.
Las propias plantillas se dividen en varios grupos:
- Patrones literales;
- Capturar patrones;
- Patrón comodín;
- Patrones de valor constante;
- Patrones de secuencia;
- Patrones de mapeo;
- Patrones de clase.
Te contaré un poco de cada uno de ellos.
Patrones literales
El patrón literal, como su nombre indica, implica hacer coincidir una serie de valores, a saber, cadenas, números, booleanos y
Parece que
string == 'string'
se está utilizando el método
__eq__
.
match number:
case 42:
print('answer')
case 43:
print('not answer')
Capturar patrones
Una plantilla de captura le permite vincular una variable con un nombre dado en la plantilla y usar ese nombre dentro del ámbito local.
match greeting:
case "":
print('Hello my friend')
case name:
print(f'Hello {name}')
Patrón comodín
Si hay demasiadas opciones de coincidencia, puede usar
_
, que es un cierto valor predeterminado y coincidirá con todos los elementos de la estructura.
match
match number:
case 42:
print("Its’s forty two")
case _:
print("I don’t know, what it is")
Patrones de valor constante
Cuando use constantes, debe usar nombres con puntos, por ejemplo enumeraciones; de lo contrario, el patrón de captura funcionará.
OK = 200
CONFLICT = 409
response = {'status': 409, 'msg': 'database error'}
match response['status'], response['msg']:
case OK, ok_msg:
print('handler 200')
case CONFLICT, err_msg:
print('handler 409')
case _:
print('idk this status')
Y el resultado esperado no será el más obvio.
Patrones de secuencia
Se le permite comparar las listas, tuplas, y cualquier otro objeto de
collections.abc.Sequence
, excepto
str
,
bytes
,
bytearray
.
answer = [42]
match answer:
case []:
print('i do not find answer')
case [x]:
print('asnwer is 42')
case [x, *_]:
print('i find more than one answers')
Ahora no es necesario llamar cada vez
len()
para comprobar el número de elementos de la lista, ya que se llamará al método
__len__
.
Patrones de mapeo
Este grupo es un poco como el anterior, solo que aquí estamos haciendo coincidir diccionarios o, para ser precisos, objetos de tipo
collections.abc.Mapping
. Se pueden combinar bastante bien entre sí.
args = (1, 2)
kwargs = {'kwarg': 'kwarg', 'one_more_kwarg': 'one_more_kwarg'}
def match_something(*args, **kwargs):
match (args, kwargs):
case (arg1, arg2), {'kwarg': kwarg}:
print('i find positional args and one keyword args')
case (arg1, arg2), {'kwarg': kwarg, 'one_more_kwarg': one_more_kwarg}:
print('i find a few keyword args')
case _:
print('i cannot match anything')
match_something(*args, **kwargs)
Y todo estaría bien, pero hay una característica. Este patrón garantiza la entrada de esta (s) clave (s) en el diccionario, pero la longitud del diccionario no importa. Así que encuentro argumentos posicionales y aparecerán argumentos de una palabra clave en la pantalla .
Patrones de clase
Para los tipos de datos definidos por el usuario, la sintaxis es similar a la inicialización del objeto.
Así es como se verá con el ejemplo de clases de datos:
from dataclasses import dataclass
@dataclass
class Coordinate:
x: int
y: int
z: int
coordinate = Coordinate(1, 2, 3)
match coordinate:
case Coordinate(0, 0, 0):
print('Zero point')
case _:
print('Another point')
También puede usar
if
, o así llamado
guard
. Si la condición es falsa, la coincidencia de patrones continúa. Vale la pena señalar que primero se hace coincidir el patrón, y solo después de que se verifica la condición:
case Coordinate(x, y, z) if z == 0:
print('Point in the plane XY')
Si usa clases directamente, entonces necesita un atributo
__match_args__
en el que se necesiten argumentos posicionales (para namedtuple y dataclasses, se
__match_args__
genera automáticamente).
class Coordinate:
__match_args__ = ['x', 'y', 'z']
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
oordinate = oordinate(1, 2, 3)
match oordinate:
case oordinate(0, 0, 0):
print('Zero oordinate')
case oordinate(x, y, z) if z == 0:
print('oordinate in the plane Z')
case _:
print('Another oordinate')
De lo contrario, se lanzará una excepción TypeError : Coordinate () acepta 0 subpatrones posicionales (3 dados)
¿Cuál es el resultado final?
De hecho, parece otro azúcar sintáctico junto con el reciente
walrus operator
. La implementación , tal como está, convierte bloques de instrucciones
match
en construcciones equivalentes
if/else
, a saber, código de bytes, que tiene el mismo efecto.
Armin Ronacher, el creador del marco web Flask para Python, describió muy sucintamente el estado actual de la coincidencia de patrones.
Sí, es difícil de argumentar: el código se volverá algo más limpio de lo que sería
if/else
un tercio de la torre de pantalla. Pero tampoco puedes llamarlo algo que produzca un efecto sorpresa. No está mal que se presente: será conveniente usarlo en algunos lugares, pero no en todos. De una forma u otra, lo principal de esta novedad es no exagerar, no correr más rápido para actualizar todos los proyectos a 3.10 y reescribir todo, porque:
Ahora es mejor que nunca. Aunque nunca suele ser mejor que ahora.
¿Lo usarás? ¿Si es así, donde?