Editar 18/10/2024:
Uma reprodução ainda mais trivial do problema é mostrada abaixo.
mypy_arg_type.py
:
import numpy as np
from numpy.typing import NDArray
import random
def winner(_: NDArray[np.bytes_]) -> bytes | None:
return b"." if bool(random.randint(0, 1)) else None
board = np.full((2, 2), ".", "|S1")
for w in np.apply_along_axis(winner, 0, board):
print(w)
>> python mypy_arg_type.py
b'.'
None
>> mypy mypy_arg_type.py
mypy_arg_type.py:9: error: Argument 1 to "apply_along_axis" has incompatible type "Callable[[ndarray[Any, dtype[bytes_]]], bytes | None]"; expected "Callable[[ndarray[Any, dtype[Any]]], _SupportsArray[dtype[Never]] | _NestedSequence[_SupportsArray[dtype[Never]]]]" [arg-type]
mypy_arg_type.py:9: note: This is likely because "winner" has named arguments: "_". Consider marking them positional-only
Found 1 error in 1 file (checked 1 source file)
Pergunta original:
Estou trabalhando em um problema para determinar o vencedor de um jogo Connect Four , dada a posição das peças no tabuleiro. O tabuleiro tem tamanho 6x7, e cada coluna é marcada com uma letra de A
a G
. O vencedor, se houver, terá 4 peças de cor idêntica em uma linha, coluna, diagonal ou antidiagonal.
Exemplo:
Entrada:["A_Red", "B_Yellow", "A_Red", "B_Yellow", "A_Red", "B_Yellow", "G_Red", "B_Yellow"]
Quadro:
R Y . . . . R
R Y . . . . .
R Y . . . . .
. Y . . . . .
. . . . . . .
. . . . . . .
Ganhador:Yellow
O código a seguir determina um vencedor.
import itertools
import numpy as np
from numpy.typing import NDArray
def who_is_winner(pieces: list[str]) -> str:
def parse_board() -> NDArray[np.bytes_]:
m, n = 6, 7
indices = [0] * n
# https://numpy.org/doc/stable/user/basics.strings.html#fixed-width-data-types
# One-byte encoding, the byteorder is ‘|’ (not applicable)
board = np.full((m, n), ".", "|S1")
for p in pieces:
col = ord(p[0]) - ord("A")
board[indices[col], col] = p[2]
indices[col] += 1
return board
def winner(arr: NDArray[np.bytes_]) -> np.bytes_ | None:
i = len(arr)
xs = next(
(xs for j in range(i - 3) if (xs := set(arr[j : j + 4])) < {b"R", b"Y"}),
{None},
)
return xs.pop()
def axis(x: int) -> np.bytes_ | None:
# https://numpy.org/doc/2.0/reference/generated/numpy.apply_along_axis.html#numpy-apply-along-axis
# Axis 0 is column-wise, 1 is row-wise.
return next(
(w for w in np.apply_along_axis(winner, x, board) if w is not None), None
)
def diag(d: int) -> np.bytes_ | None:
# https://numpy.org/doc/stable/reference/generated/numpy.diagonal.html#numpy-diagonal
# Diagonal number is w.r.t. the main diagonal.
b = board if bool(d) else np.fliplr(board)
return next(
(w for d in range(-3, 4) if (w := winner(b.diagonal(d))) is not None), None
)
board = parse_board()
match next(
(
w
for f, i in itertools.product((axis, diag), (0, 1))
if (w := f(i)) is not None
),
None,
):
case b"Y":
return "Yellow"
case b"R":
return "Red"
case _:
return "Draw"
Entretanto, isso gera uma violação mypy da seguinte forma:
error: Argument 1 to "apply_along_axis" has incompatible type "Callable[[ndarray[Any, dtype[bytes_]]], bytes_ | None]"; expected "Callable[[ndarray[Any, dtype[Any]]], _SupportsArray[dtype[bytes_]] | _NestedSequence[_SupportsArray[dtype[bytes_]]]]" [arg-type]
note: This is likely because "winner" has named arguments: "arr". Consider marking them positional-only
De acordo com a documentação de apply_along_axis , ele deve retornar um único valor, o que é consistente com o código acima.
Como consertar essa violação? Tornar a função winner
somente posicional não faz diferença, exceto que a sugestão desapareceu.
Estou usando Python 3.12.5 com mypy 1.11.2.
Ao estudar as assinaturas sobrecarregadas de
apply_along_axis
, cheguei à conclusão de que ele não está definido para retornarNone
, causando a violação mypy. Não há nenhuma razão real para não retornarNone
, e eu abri um ticket mypy sobre isso. Vamos ver se ele será chutado para numpy.Sobrecarga 1:
Sobrecarga 2:
Modifiquei a função
winner
para retornar uma string de byte nulo (b""
) em vez deNone
, e substituí todos os tipos de retorno denp.bytes_
parabytes
. Isso resolveu o problema.Adicione uma anotação de tipo à sua função personalizada: O Mypy frequentemente fica confuso quando não conhece os tipos da sua entrada/saída. Ao anotar explicitamente sua função personalizada, você pode dar alguma ajuda ao Mypy.
Por exemplo:
Use # type: ignore para a linha que causa os avisos: Se você não conseguir se livrar do aviso e estiver confiante de que o código funciona como esperado, você pode dizer ao Mypy para ignorá-lo adicionando # type: ignore a essa linha. Não é ideal para uso a longo prazo, mas é uma correção rápida:
Considere um plugin Mypy para NumPy: Há muitos esforços para melhorar o entendimento do Mypy sobre bibliotecas como NumPy. Você pode querer dar uma olhada em plugins de terceiros ou stubs de digitação que oferecem melhor suporte, embora eles ainda sejam um trabalho em andamento.