Numpy: Criação "controlada" de matrizes de objetos

Criado em 4 dez. 2019  ·  45Comentários  ·  Fonte: numpy/numpy

A criação automática de matrizes de objetos tornou-se recentemente obsoleta na numpy. Eu concordo com a mudança, mas parece um pouco difícil escrever certos tipos de código genérico que determinam se um argumento fornecido pelo usuário pode ser convertido em uma matriz sem objeto.

Reproduzindo exemplo de código:

Matplotlib contém o seguinte snippet:

    # <named ("string") colors are handled earlier>
    # tuple color.
    c = np.array(c)
    if not np.can_cast(c.dtype, float, "same_kind") or c.ndim != 1:
        # Test the dtype explicitly as `map(float, ...)`, `np.array(...,
        # float)` and `np.array(...).astype(float)` all convert "0.5" to 0.5.
        # Test dimensionality to reject single floats.
        raise ValueError(f"Invalid RGBA argument: {orig_c!r}")

mas às vezes a função é chamada com uma matriz de cores em vários formatos (por exemplo, ["red", (0.5, 0.5, 0.5), "blue"] ) - pegamos o ValueError e convertemos cada item um de cada vez.

Agora, a chamada para np.array (c) emitirá um DeprecationWarning. Como podemos contornar isso? Mesmo algo como np.min_scalar_type(c) emite um aviso (o que eu acho que não deveria?), Então não é óbvio para mim como verificar "se convertêssemos isso em um array, qual seria o dtype?"

Informações sobre a versão Numpy / Python:


1.19.0.dev0 + bd1adc3 3.8.0 (padrão, 6 de novembro de 2019, 21:49:08)
[GCC 7.3.0]

57 - Close?

Comentários muito úteis

Alguém poderia apontar para o exemplo operator.mod ?

Quanto ao operador == , o que vi estava fazendo algo como np.array(vals, dtype=object) == vals onde vals=[1, [2, 3]] (parafraseando o código), então a solução é criar proativamente o array à direita lado.

Muitas das falhas scipy parecem ter a forma np.array([0.25, np.array([0.3])]) , em que misturar escalares e ndarray com shape==(1,) entrará em conflito com a descoberta da dimensão e criará uma matriz de objetos. xref gh-15075

Todos 45 comentários

Uma opção seria
`` `python
Experimente:
# saia na frente do jogo e promova a suspensão de uso para um erro que o substituirá
com warnings.catch_warnings ():
warnings.filterwarnings ('aumentar', DeprecationWarning, mensagem = "...")
c_arr = np.asarray (c)
exceto (DeprecationWarning, ValueError):
# o que quer que você faça atualmente para ValueError

Eu acho que isso, e o teste de falha mencionado em gh-15045 são instâncias em que a emissão de DeprecationWarning por alguns anos em vez de emitir diretamente ValueError causa mais rotatividade de código do que o necessário.

Observe que warnings.catch_warnings não é seguro para threads. Isso torna a solução alternativa um pouco suscetível a problemas de acompanhamento no futuro.

Eu acho que o código-churn vale o período de depreciação.

Matplotlib executa seu conjunto de testes com avisos-como-falhas para detectar exatamente esse tipo de mudança no início, então parece que o sistema está funcionando para mim :).

Mas AFAICT não há nem mesmo uma solução razoavelmente fácil (como apontado acima, a correção proposta não é threadsafe) para isso: /

Acho que vejo o ponto de @anntzer aqui. Estamos em uma confusão em que a biblioteca downstream deseja falhar rapidamente para que possam tentar outra coisa, enquanto os usuários deveriam receber uma mensagem mais gentil.

O problema é que hoje não há como o autor da biblioteca perguntar "isso emitiria um aviso" sem realmente ... emitir o aviso e suprimi-lo não é thread-safe.

Com relação ao aviso de thread-safety: https://bugs.python.org/issue37604

AFAIK, a descontinuação está no ramo de lançamento. Queremos reverter isso? Caso contrário, as correções precisarão de backports. Ainda não estou claro por que os avisos não foram levantados nas rodas do branch de liberação e não apareceram nas compilações noturnas até as duas últimas compilações. Eu não mudei nada após o branch e nada parece muito suspeito nos commits desde então no branch master exceto, possivelmente, # 15040.

IMHO (e de acordo com o ponto de @mattip acima) é o tipo de mudança que seria muito mais fácil de lidar no downstream se a mudança para aumento acontecesse sem um período de depreciação. Não tenho certeza se isso é uma opção: /

Ou, possivelmente, ramos de tratamento multibuild são diferentes do master.

FWIW Eu sempre fui pelo menos -1 nessa mudança, especialmente como um usuário perspicaz de estruturas de dados irregulares, mas de qualquer maneira agora eu preciso descobrir o que fazer com as centenas de falhas de teste para o SciPy 1.4.0rc2 prep em https://github.com/scipy/scipy/pull/11161

agora preciso descobrir o que fazer com as centenas de falhas de teste

Uma opção fácil seria:

  • Suprima o aviso em sua configuração de pytest
  • Abra um problema para corrigi-lo mais tarde

O objetivo principal de usarmos DeprecationWarning vez de ValueError era dar aos projetos downstream e aos usuários um período de carência para fazer exatamente isso.

AFAIK, a descontinuação está no ramo de lançamento. Queremos reverter isso?

Acho que sim, está chovendo problemas. Agora temos uma lista do que está acontecendo no Pandas, Matplotlib, SciPy, dentro de numpy.testing e ufuncs NumPy, == , etc. Acho que devemos reverter a mudança agora e avaliar / corrigir todos aqueles coisas e, em seguida, reintroduza a depreciação.

Podemos nos comprometer com um aviso de suspensão pendente?

Dessa forma, os projetos posteriores podem adicioná-lo às suas listas de ignorados e, quando voltarmos para o DeprecationWarning, eles poderão tomar a decisão novamente.

Parece que divergimos da questão original, que parece ser "dada uma sequência de valores, como pode matplotlib determinar se são uma única cor ou uma lista de cores". Acho que deve haver uma solução que não exija converter os valores para um ndarray e verificar o dtype desse array. Algum tipo de função is_a_color() recursiva pode ser uma solução melhor.

Eu reverti a mudança para 1.18.x em # 15053.

O sentimento é que quebrar o CI do scipy e do pandas é irritante o suficiente para reverter temporariamente no master também. Eu gostaria que ele voltasse basicamente programado (digamos dentro de um mês). Podemos precisar encontrar uma solução, no entanto. Além disso, os consertos que os pandas estão fazendo são um pouco preocupantes para mim, já que eles usam catch_warnings .

Se realmente não houver nenhuma maneira, e precisarmos de supressão de aviso thread-safe. np.seterr pode ter um slot para ele: /.

Parece que divergimos da questão original, que parece ser "dada uma sequência de valores, como pode matplotlib determinar se são uma única cor ou uma lista de cores".

Eu acho que o problema que @anntzer traz à tona é mais geral. Trata-se de escrever uma função que recebe muitos tipos de entrada, com lógicas como:

  • criar ndarray(flexible_input)
  • if `new_ndarray.dtype.kind == 'O': lidar com isso
  • outro: use_the_array

uma vez que não se pode adicionar dtype=object a tal código, o que deve ser feito?

Além disso, os consertos que os pandas estão fazendo são um pouco preocupantes para mim, já que eles usam catch_warnings .

@seberg não era suppress_warnings melhor com isso?

@rgommers não, suppress_warnings resolveu o problema da supressão de avisos ser permanente quando não deveria ser. Isso foi corrigido nas versões mais recentes do python, de modo que realmente não precisamos mais dele (ele tem propriedades melhores, uma vez que oferece suporte a aninhamento, mas não oferece suporte à segurança de thread. Não tenho certeza se isso é possível fora do python, e até se fosse, provavelmente não é desejável)

Não tenho certeza se os casos problemáticos são executados contra a intenção original (https://numpy.org/neps/nep-0034.html) de que simplesmente não estamos previstos.

De qualquer forma, uma saída seria habilitar explicitamente o comportamento antigo ao longo das linhas de "apreciamos sua preocupação, mas queremos explicitamente o tipo de objeto dependente do contexto e nós mesmos trataremos da entrada problemática". Algo como um de

~~~
np.array (dados, dtype = 'allow_object')

np.array (dados, allow_object_dtype = True)

com np.array_create_allow_object_dtype ():
np.array (dados)
~~~

tudo não muito bonito e com nomenclatura para com certeza ser melhorado. Mas isso oferece uma saída limpa para bibliotecas que confiaram no comportamento e desejam mantê-lo (pelo menos por enquanto).

Não é o caso matplotlib, na verdade:

with np.forbid_ragged_arrays_immediately():
    np.array(data)

já que você realmente deseja detectar o erro, ao invés de obter um tipo de objeto?

Não há reversão da suspensão de uso atualmente pendente para o mestre. Não acho que deva ser revertido no atacado como era no 1.18, porque isso também removeu as correções, que acho que queremos manter. @mattip Uma reversão mais direcionada seria apreciada até decidirmos o que fazer a longo prazo.

FWIW Eu acho que a maioria dos lugares no mpl que atingem isso podem ser consertados (com mais ou menos reestruturação - em um caso, o código fica muito mais rápido depois ...).
Eu acho que a API proposta de @timhoffm seria melhor do que with np.forbid_ragged_arrays_immediately: porque a última pode ser facilmente escrita em termos da primeira (aumento se np.array(..., allow_object=True).dtype == object ) enquanto o oposto ( try: with np.forbid: ... except ValueError: ... ) seria menos eficiente se ainda quiséssemos criar um array de objetos depois de tudo. Mas um CM (apenas "movendo localmente após o período de depreciação") seria melhor do que nada.

(Novamente, acho que a mudança é boa, é apenas uma questão de como ela é executada.)

Sim, só precisamos descobrir como a API deve ser. Como apontado por muitos, existem atualmente dois problemas principais:

  1. Confundindo object e "allow ragged" . Se os objetos tiverem um tipo razoável (digamos Decimal ), você realmente deseja obter o aviso / erro, mas também pode precisar passar dtype=object
  2. Não há uma maneira de optar pelo novo comportamento ou continuar usando o antigo (sem um aviso). Parece que pelo menos o Opt-In é provavelmente necessário para uso interno. Se não o fornecermos, basicamente presumimos que (possivelmente indiretamente) são apenas os usuários finais que se deparam com esses casos.

Finalmente, temos que descobrir como enfiá-lo em nosso código :). ndmin pode ser outro alvo para enfiar sinalizadores controlando pelo menos o comportamento irregular.

Não há reversão da suspensão de uso atualmente pendente para o mestre. Não acho que deva ser revertido no atacado como era no 1.18, porque isso também removeu as correções, que acho que queremos manter. @mattip Uma reversão mais direcionada seria apreciada até decidirmos o que fazer a longo prazo.

Não vejo problema em reverter totalmente e depois reintroduzir todas as partes que fazem sentido agora. Novamente, reverter algo não é um julgamento de valor sobre o que é bom ou ruim, é apenas uma maneira pragmática de desvendar um monte de coisas que acabamos de quebrar pressionando o botão de mesclagem. Há claramente um impacto e problemas não resolvidos que não foram previstos no NEP, portanto, reverter primeiro é a coisa certa a fazer.

Um argumento para não reverter ainda - enquanto a mudança está no mestre, podemos aproveitar as execuções de CI downstream para tentar descobrir como seriam as soluções alternativas

O CI downstream é vermelho, isso é _muito_ inútil. Agora temos a lista de falhas deles, não precisamos manter seus ICs vermelhos para facilitar um pouco nossa vida aqui.

E pelo menos o CI do Matplotlib está sendo executado em pip install --pre não no branch master

E pelo menos o CI do Matplotlib está sendo executado em pip install --pre não no branch master

Isso está puxando das rodas noturnas, pelo que parece. A mudança já foi revertida para 1.18.0rc1, então você não deveria vê-la se fosse instalar com --pre do PyPI.

Alguns dos comentários acima significam repensar as mudanças propostas no NEP 34. Não tenho certeza se este tópico é o lugar apropriado para continuar esta discussão, mas aqui vai. (Não há problema se for discutido em outro lugar - copiar e colar comentários é fácil.: Smile: Além disso, alguns de vocês viram uma variação desses comentários em uma discussão sobre o slack.)

Depois de pensar sobre isso recentemente, acabei com a mesma ideia da primeira sugestão de @timhoffm (e a ideia provavelmente foi proposta em outras ocasiões nos últimos meses): definir uma string específica ou objeto singleton que, quando dado como o argumento dtype para array , permite que a função trate a entrada em formato irregular criando uma matriz de objeto 1-d. Na verdade, isso habilita o comportamento pré-NEP-34 de dtype=None no qual a entrada irregular é automaticamente convertida em um array de objetos. Se qualquer outro valor para dtype for fornecido (incluindo None ou object ), um aviso de depreciação será dado se a entrada tiver formato irregular. Em uma versão futura do NumPy, esse aviso será convertido em um erro.

Acho que está claro agora que usar dtype=object para permitir o manuseio de entrada irregular não é uma boa solução para o problema. Idealmente, desacoplaríamos as noções de "array de objetos" de "array irregular". Mas não podemos desacoplá-los completamente, porque quando queremos lidar com um array irregular, a única escolha que temos é criar um array de objetos. Por outro lado, às vezes queremos um array de objetos, mas não queremos a conversão automática de entrada de formato irregular em um array de sequências de objetos.

Por exemplo (cf. item 1 no último comentário de @seberg ), suponha que f1 , f2 , f3 e f4 são Fraction objects, e estou trabalhando com matrizes de objetos de Fraction s. Não estou interessado em criar uma matriz irregular. Se eu acidentalmente escrever a = np.array([f1, f2, [f3, f4]], dtype=object) , eu _quero_ que gere um erro, por todas as razões que temos NEP 34. Com NEP 34, entretanto, isso criará um array 1-d de comprimento 3.

Alternativas que adicionam um novo argumento de palavra-chave, como a segunda sugestão de @timhoffm , parecem mais complicadas do que o necessário. O problema que estamos tentando resolver é a "metralhadora", em que a entrada irregular é automaticamente convertida em uma matriz de objetos 1-d. O problema só surge quando dtype=None é passado para array . Exigir que os usuários substituam dtype=None por dtype=<special-value-that-enables-ragged-handling> para manter o antigo comportamento problemático é uma mudança simples na API que é fácil de explicar. Nós realmente precisamos de mais do que isso?

Acho que está claro agora que usar dtype=object para permitir o manuseio de entrada irregular não é uma boa solução para o problema. Idealmente, desacoplaríamos as noções de "array de objetos" de "array irregular".

Parece razoável, talvez. Também é bom salientar que não existe um conceito real de "matriz irregular" no NumPy . É algo que basicamente não suportamos (pesquise por "ragged" nos documentos, no rastreador de problemas ou na lista de e-mails para confirmar se quiser), é algo que DyND e XND suportam, e só começamos a falar para ter um resumo frase para discutir "queremos remover o comportamento np.array([1, [2, 3]]) que atrapalha os usuários". Conseqüentemente, preparar em "matrizes irregulares" como uma nova coisa de API deve ser feito com extremo cuidado, não é absolutamente algo que queremos promover. Portanto, seria bom deixar isso claro na nomenclatura de qualquer dtype=some_workaround que possamos adicionar.

Parece que a opinião geral está se unindo em torno de uma solução para estender a depreciação (talvez indefinidamente) permitindo np.array(vals, dtype=special) que se comportará como antes do NEP 34. Prefiro um singleton em vez de uma string, pois isso significa que o uso de bibliotecas pode fazer special = getattr(np.special, None) e seu código funcionará em todas as versões.

Agora precisamos decidir sobre o nome e onde deve ser exposto. Talvez never_fail ou guess_dimensions ? Quanto a onde expô-lo, eu preferiria não pendurá-lo em np vez de algum outro módulo interno, talvez com um _ para indicar que é realmente uma interface privada.

Acho que o caminho a seguir é emendar o NEP 34 e, em seguida, expor a discussão na lista de discussão.

Observe que também houve alguns relatórios de problemas com o uso de operadores ( == e operator.mod pelo menos). Você está propondo ignorar isso ou, de alguma forma, armazenar esse estado no array?

Em quase todos os casos, provavelmente se sabe que um dos operandos é uma matriz numpy. Portanto, provavelmente será possível obter um comportamento bem definido convertendo manualmente para um array numpy.

Alguém poderia apontar para o exemplo operator.mod ?

Quanto ao operador == , o que vi estava fazendo algo como np.array(vals, dtype=object) == vals onde vals=[1, [2, 3]] (parafraseando o código), então a solução é criar proativamente o array à direita lado.

Muitas das falhas scipy parecem ter a forma np.array([0.25, np.array([0.3])]) , em que misturar escalares e ndarray com shape==(1,) entrará em conflito com a descoberta da dimensão e criará uma matriz de objetos. xref gh-15075

Alguém poderia apontar para o exemplo operator.mod ?

Vi isso no Pandas PR de @jbrockmendel , mas acho que mudou desde então (não vejo mais um operator.mod explícito nos comentários).

Quanto ao operador == , o que vi estava fazendo algo como np.array(vals, dtype=object) == vals onde vals=[1, [2, 3]] (parafraseando o código), então a solução é criar proativamente o array à direita lado.

Nesse ponto, torna-se np.array(vals, dtype=object) == np.array(vals, dtype=object) , então é melhor apenas excluir o teste :)

@mattip escreveu:

Eu prefiro um singleton em vez de uma string, pois isso significa que os usos da biblioteca podem fazer special = getattr (np.special, None) e seu código funcionará em todas as versões.

Isso soa bem para mim.

Agora precisamos decidir sobre o nome e onde deve ser exposto. Talvez never_fail ou guess_dimensions ? Quanto a onde expô-lo, eu preferiria não suspendê-lo em vez de algum outro módulo interno, talvez com um _ para indicar que é realmente uma interface privada.

Meu nome de trabalho atual para isso é legacy_auto_dtype , mas provavelmente há muitos outros nomes dos quais eu não teria nenhuma reclamação.

Não tenho certeza se o nome deve ser privado. Por qualquer definição prática de _privado_ e _public_, este será um objeto _public_. Ele fornece aos usuários os meios de preservar o comportamento legado de, por exemplo, array(data) reescrevendo como array(data, dtype=legacy_auto_dtype) . Imagino que o NEP atualizado explicará que é assim que o código deve ser modificado para manter o comportamento legado (para aqueles que precisam fazer isso). Se for esse o caso, o objeto definitivamente não é privado. Na verdade, parece que é um objeto público que permanecerá no NumPy indefinidamente. Mas talvez meu entendimento de como o NEP 34 modificado funcionará esteja errado.

Concordado com a descrição de público / privado de @WarrenWeckesser ; ou é público ou não deve ser usado por ninguém fora do NumPy.

Renomear: escolha um nome que descreva a funcionalidade. Coisas como "legado" quase nunca são uma boa ideia.

escolha um nome que descreva a funcionalidade.

auto_object , auto_dtype , auto ?

Pensando alto um pouco ...

O que este objeto faz?

Atualmente, quando NumPy é fornecido um objeto Python que contém subsequências cujos comprimentos não são consistentes com um nd array regular, NumPy criará um array com object tipo de dados, com os objetos no primeiro nível onde ocorre a inconsistência da forma deixados como objetos Python. Por exemplo, array([[1, 2], [1, 2, 3]]) tem forma (2,) , np.array([[1, 2], [3, [99]]]) tem forma (2, 2) , etc. Com NEP 34, estamos descontinuando esse comportamento, portanto, tentando criar um array com entrada "irregular" eventualmente resultará em um erro, a menos que seja explicitamente habilitado. O valor especial de que estamos falando permite o comportamento antigo.

Qual é um bom nome para isso ? ragged_as_object ? inconsistent_shapes_as_object ?

Nesse ponto, torna-se np.array(vals, dtype=object) == np.array(vals, dtype=object) , então é melhor apenas excluir o teste :)

Bem, eu estava parafraseando. O teste real é mais como my_func(vals) == vals deve se tornar my_func(vals) == np.array(vals, dtype=object)

Vou propor uma extensão para NEP 34 para permitir um valor especial para dtype.

Observe que parece que o scipy não precisa desse sentinela para passar nos testes com scipy / scipy # 11310 e scipy / scipy # 11308

O gh-15119 foi mesclado, o que reimplementou o NEP. Se não for revertido, podemos fechar este problema

Vou encerrar isso, já que não fizemos o acompanhamento antes do lançamento do 1.19. E pelo menos espero que a razão para isso seja porque a discussão morreu, uma vez que todos os grandes projetos foram capazes de encontrar soluções razoáveis ​​para os problemas por eles criados.
Por favor, corrija-me se eu estiver errado, especialmente se ainda estiver sujeito a problemas com pandas, matplotlib, etc. Mas presumo que teríamos ouvido falar disso durante o ciclo de candidato a lançamento 1.19.x.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

marcocaccin picture marcocaccin  ·  4Comentários

astrofrog picture astrofrog  ·  4Comentários

MareinK picture MareinK  ·  3Comentários

Levstyle picture Levstyle  ·  3Comentários

dcsaba89 picture dcsaba89  ·  3Comentários