Como as funções Python lidam com os tipos de parâmetros que você transmite?

305

A menos que eu esteja enganado, a criação de uma função no Python funciona assim:

def my_func(param1, param2):
    # stuff

No entanto, você não fornece os tipos desses parâmetros. Além disso, se bem me lembro, o Python é uma linguagem fortemente tipada; portanto, parece que o Python não deve permitir que você transmita um parâmetro de um tipo diferente do esperado pelo criador da função. No entanto, como o Python sabe que o usuário da função está passando nos tipos adequados? O programa simplesmente morrerá se for do tipo errado, assumindo que a função realmente use o parâmetro? Você precisa especificar o tipo?

Leif Andersen
fonte
15
Eu acho que a resposta aceita nesta pergunta deve ser atualizada para estar mais alinhada com os recursos atuais que o Python oferece. Eu acho que essa resposta faz o trabalho.
precisa saber é o seguinte

Respostas:

173

O Python é fortemente digitado porque cada objeto tem um tipo, todo objeto conhece seu tipo, é impossível usar acidentalmente ou deliberadamente um objeto de um tipo "como se" fosse um objeto de um tipo diferente e todas as operações elementares no objeto fossem delegado ao seu tipo.

Isso não tem nada a ver com nomes . Um nome no Python não "tem um tipo": se e quando um nome é definido, o nome se refere a um objeto e o objeto tem um tipo (mas isso não força de fato um tipo no nome : a nome é um nome).

Um nome em Python pode perfeitamente se referir a objetos diferentes em momentos diferentes (como na maioria das linguagens de programação, embora não todas) - e não há restrição no nome, de modo que, se ele já se referiu a um objeto do tipo X, é, em seguida, para sempre obrigado a referir-se apenas a outros objetos de X. restrições de tipo sobre nomes não fazem parte do conceito de "tipagem forte", embora alguns entusiastas de estática digitação (onde os nomes que se constrangidos, e em um estático, AKA de compilação tempo, moda também) fazem mau uso do termo dessa maneira.

Alex Martelli
fonte
71
Portanto, parece que a digitação forte não é tão forte, nesse caso em particular, é mais fraca que a estática.IMHO, a restrição de digitação no tempo de compilação no nome / variável / referência é realmente muito importante, portanto, afirmo com ousadia que o python não é tão bom quanto a digitação estática neste aspecto. Por favor me corrija se eu estiver errado.
liang
19
@liang Essa é uma opinião, então você não pode estar certo ou errado. Certamente também é minha opinião, e eu tentei muitas línguas. O fato de não poder usar meu IDE para descobrir o tipo (e, portanto, os membros) dos parâmetros é uma grande desvantagem do python. Se essa desvantagem é mais importante do que as vantagens da digitação com patos, depende da pessoa que você perguntar.
Maarten Bodewes
6
Mas isso não responde a nenhuma das perguntas: "No entanto, como o Python sabe que o usuário da função está passando os tipos adequados? O programa simplesmente morrerá se for do tipo errado, assumindo que a função realmente use o parâmetro? Você precisa especificar o tipo? " ou ..
qPCR4vir 04/03
4
@ qPCR4vir, qualquer objeto pode ser passado como argumento. O erro (uma exceção, o programa não "morrerá" se for codificado para capturá-lo, consulte try/ except) ocorrerá quando e se for tentada uma operação que o objeto não suporta. No Python 3.5, agora você pode opcionalmente "especificar tipos" de argumentos, mas nenhum erro ocorre, per se, se a especificação for violada; a notação de digitação serve apenas para ajudar a separar ferramentas que executam análises, etc., não altera o comportamento do próprio Python.
Alex Martelli
2
@AlexMartelli. Obrigado! Para mim esta é a resposta certa: "O erro (uma exceção, o programa não vai 'morrer' se ele é codificado para pegá-lo, consulte try / excepto) .."
qPCR4vir
753

As outras respostas fizeram um bom trabalho ao explicar a digitação de patos e a resposta simples de tzot :

Python não possui variáveis, como outras linguagens em que variáveis ​​têm um tipo e um valor; possui nomes apontando para objetos, que conhecem seu tipo.

No entanto , uma coisa interessante mudou desde 2010 (quando a pergunta foi feita pela primeira vez), a saber, a implementação do PEP 3107 (implementada no Python 3). Agora você pode realmente especificar o tipo de um parâmetro e o tipo do tipo de retorno de uma função como esta:

def pick(l: list, index: int) -> int:
    return l[index]

Podemos ver aqui que são picknecessários 2 parâmetros, uma lista le um número inteiro index. Também deve retornar um número inteiro.

Então, aqui está implícito que lhá uma lista de números inteiros que podemos ver sem muito esforço, mas para funções mais complexas pode ser um pouco confuso quanto ao que a lista deve conter. Também queremos que o valor padrão indexseja 0. Para resolver isso, você pode optar por escrever pickassim:

def pick(l: "list of ints", index: int = 0) -> int:
    return l[index]

Observe que agora colocamos uma string como o tipo de l, que é sintaticamente permitido, mas não é bom para analisar programaticamente (que voltaremos mais tarde).

É importante observar que o Python não aumentará TypeErrorse você passar um float index, a razão para isso é um dos principais pontos na filosofia de design do Python: "Todos nós somos adultos concordantes aqui" , o que significa que você deve esteja ciente do que você pode passar para uma função e do que não pode. Se você realmente deseja escrever código que lança TypeErrors, pode usar a isinstancefunção para verificar se o argumento passado é do tipo apropriado ou uma subclasse dele como esta:

def pick(l: list, index: int = 0) -> int:
    if not isinstance(l, list):
        raise TypeError
    return l[index]

Mais sobre por que você raramente deve fazer isso e o que deve fazer é discutido na próxima seção e nos comentários.

O PEP 3107 não apenas melhora a legibilidade do código, mas também possui vários casos de uso adequados sobre os quais você pode ler aqui .


A anotação de tipo recebeu muito mais atenção no Python 3.5 com a introdução do PEP 484, que introduz um módulo padrão para dicas de tipo.

Essas dicas de tipo vieram do verificador de tipos mypy ( GitHub ), que agora é compatível com PEP 484 .

O módulo de digitação vem com uma coleção bastante abrangente de dicas de tipo, incluindo:

  • List, Tuple, Set, Map- para list, tuple, sete maprespectivamente.
  • Iterable - útil para geradores.
  • Any - quando poderia ser qualquer coisa.
  • Union- quando poderia ser qualquer coisa dentro de um conjunto especificado de tipos, em oposição a Any.
  • Optional- quando pode ser Nenhum. Taquigrafia para Union[T, None].
  • TypeVar - usado com genéricos.
  • Callable - usado principalmente para funções, mas pode ser usado para outros callables.

Essas são as dicas de tipo mais comuns. Uma lista completa pode ser encontrada na documentação do módulo de digitação .

Aqui está o exemplo antigo usando os métodos de anotação introduzidos no módulo de digitação:

from typing import List

def pick(l: List[int], index: int) -> int:
    return l[index]

Um recurso poderoso é o Callableque permite digitar métodos de anotação que assumem uma função como argumento. Por exemplo:

from typing import Callable, Any, Iterable

def imap(f: Callable[[Any], Any], l: Iterable[Any]) -> List[Any]:
    """An immediate version of map, don't pass it any infinite iterables!"""
    return list(map(f, l))

O exemplo acima pode se tornar mais preciso com o uso de em TypeVarvez de Any, mas isso foi deixado como um exercício para o leitor, pois acredito que já preenchi minha resposta com muitas informações sobre os maravilhosos novos recursos habilitados pela dica de tipo.


Anteriormente, quando um código Python documentado com, por exemplo, Sphinx, algumas das funcionalidades acima podiam ser obtidas escrevendo-se documentos formatados da seguinte forma:

def pick(l, index):
    """
    :param l: list of integers
    :type l: list
    :param index: index at which to pick an integer from *l*
    :type index: int
    :returns: integer at *index* in *l*
    :rtype: int
    """
    return l[index]

Como você pode ver, isso requer várias linhas extras (o número exato depende de quão explícito você deseja ser e de como formata sua string de documento). Mas agora deve ficar claro para você como o PEP 3107 fornece uma alternativa que é, de várias maneiras (superior?) Superior. Isso é especialmente verdadeiro em combinação com o PEP 484 , que, como vimos, fornece um módulo padrão que define uma sintaxe para essas dicas / anotações de tipo que podem ser usadas de tal maneira que sejam inequívocas e precisas, mas flexíveis. combinação poderosa.

Na minha opinião pessoal, esse é um dos maiores recursos do Python de todos os tempos. Mal posso esperar para as pessoas começarem a aproveitar o poder disso. Desculpe pela resposta longa, mas é isso que acontece quando fico animado.


Um exemplo de código Python que usa fortemente dicas de tipo pode ser encontrado aqui .

erb
fonte
2
@rickfoosusa: Eu suspeito que você não esteja executando o Python 3 no qual o recurso foi adicionado.
precisa
26
Espere um minuto! Se a definição de parâmetro e tipo de retorno não gera a TypeError, qual é o sentido de usar pick(l: list, index: int) -> intcomo definição de uma linha? Ou entendi errado, não sei.
Erdin Eray
24
@ Err Erdin: Esse é um mal-entendido comum e não é uma pergunta ruim. Ele pode ser usado para fins de documentação, ajuda os IDEs a obter um melhor preenchimento automático e a encontrar erros antes do tempo de execução usando a análise estática (como o mypy que mencionei na resposta). Há esperanças de que o tempo de execução possa tirar proveito das informações e realmente acelerar os programas, mas isso provavelmente levará muito tempo para ser implementado. Você pode também ser capaz de criar um decorador que lança as TypeErrors para você (a informação é armazenada no __annotations__atributo do objeto de função).
erb
2
@ErdinEray Devo acrescentar que jogar TypeErrors é uma péssima ideia (a depuração nunca é divertida, não importa o quão bem sejam criadas exceções). Mas não tenha medo, a vantagem dos novos recursos descritos em minha resposta permite uma maneira melhor: não confie em nenhuma verificação em tempo de execução, faça tudo antes do tempo de execução com mypy ou use um editor que faça a análise estática para você, como PyCharm .
erb
2
@ Tony: Quando você retorna dois ou mais objetos, na verdade você retorna uma tupla, então você deve usar a anotação do tipo Tupla, ou seja,def f(a) -> Tuple[int, int]:
erb
14

Você não especifica um tipo. O método só falhará (em tempo de execução) se tentar acessar atributos que não estão definidos nos parâmetros passados.

Portanto, esta função simples:

def no_op(param1, param2):
    pass

... não falhará, não importa em que dois argumentos sejam passados.

No entanto, esta função:

def call_quack(param1, param2):
    param1.quack()
    param2.quack()

... falhará em tempo de execução se param1e param2não tiverem atributos que possam ser nomeados quack.

TM.
fonte
+1: Os atributos e métodos não são determinados estaticamente. O conceito de como esse "tipo apropriado" ou "tipo errado" é estabelecido se o tipo funciona ou não corretamente na função.
22410 S.Lott
11

Muitos idiomas têm variáveis, que são de um tipo específico e têm um valor. Python não possui variáveis; possui objetos e você usa nomes para se referir a esses objetos.

Em outros idiomas, quando você diz:

a = 1

então uma variável (normalmente inteira) altera seu conteúdo para o valor 1.

Em Python,

a = 1

significa "use o nome a para se referir ao objeto 1 ". Você pode fazer o seguinte em uma sessão interativa do Python:

>>> type(1)
<type 'int'>

A função typeé chamada com o objeto 1; como todo objeto conhece seu tipo, é fácil typedescobrir o tipo e devolvê-lo.

Da mesma forma, sempre que você define uma função

def funcname(param1, param2):

a função recebe dois objetos e os nomeia param1e param2, independentemente de seus tipos. Se você deseja garantir que os objetos recebidos sejam de um tipo específico, codifique sua função como se eles fossem do (s) tipo (s) necessário (s) e capture as exceções que são lançadas se não forem. As exceções lançadas são geralmente TypeError(você usou uma operação inválida) e AttributeError(você tentou acessar um membro inexistente (os métodos também são membros)).

tzot
fonte
8

O Python não é fortemente digitado no sentido de verificação estática ou do tipo em tempo de compilação.

A maioria dos códigos Python se enquadra no chamado "Duck Typing" - por exemplo, você procura por um método readem um objeto - não se importa se o objeto é um arquivo em disco ou soquete, você só quer ler N bytes dele.

Mark Rushakoff
fonte
21
Python é fortemente tipado. Também é digitado dinamicamente.
Daniel Newby
1
Mas isso não responde a nenhuma das perguntas: "No entanto, como o Python sabe que o usuário da função está passando os tipos adequados? O programa simplesmente morrerá se for do tipo errado, assumindo que a função realmente use o parâmetro? Você precisa especificar o tipo? " ou ..
qPCR4vir 04/03
6

Como Alex Martelli explica ,

A solução preferida normal, pitonica, é quase sempre "digitar pato": tente usar o argumento como se fosse de um determinado tipo desejado, faça-o em uma instrução try / except, capturando todas as exceções que possam surgir se o argumento não for de fato desse tipo (ou qualquer outro tipo que imite o pato ;-) e, na cláusula exceto, tente outra coisa (usando o argumento "como se" fosse de outro tipo).

Leia o restante da postagem para obter informações úteis.

Nick Presta
fonte
5

Python não se importa com o que você passa para suas funções. Quando você chama my_func(a,b), as variáveis ​​param1 e param2 retêm os valores de a e b. O Python não sabe que você está chamando a função com os tipos adequados e espera que o programador cuide disso. Se sua função for chamada com diferentes tipos de parâmetros, você pode quebrar o código acessando-os com blocos try / except e avaliar os parâmetros da maneira que desejar.

Kyle
fonte
11
O Python não possui variáveis, como outras linguagens em que as variáveis ​​têm um tipo e um valor; possui nomes apontando para objetos , que conhecem seu tipo.
tzot
2

Você nunca especifica o tipo; Python tem o conceito de digitação de pato ; basicamente, o código que processa os parâmetros fará certas suposições sobre eles - talvez chamando certos métodos que se espera que um parâmetro implemente. Se o parâmetro for do tipo errado, uma exceção será lançada.

Em geral, depende do seu código garantir que você esteja passando objetos do tipo adequado - não há compilador para impor isso com antecedência.

Justin Ethier
fonte
2

Há uma exceção notória da digitação de pato que vale a pena mencionar nesta página.

Quando a strfunção chama o __str__método de classe, ele sutilmente verifica seu tipo:

>>> class A(object):
...     def __str__(self):
...         return 'a','b'
...
>>> a = A()
>>> print a.__str__()
('a', 'b')
>>> print str(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type tuple)

Como se Guido nos sugerisse qual exceção um programa deveria criar se encontrar um tipo inesperado.

Antony Hatchkins
fonte
1

No Python, tudo tem um tipo. Uma função Python fará o que for solicitado se o tipo de argumento a suportar.

Exemplo: fooadicionará tudo o que pode ser __add__ed;) sem se preocupar muito com seu tipo. Portanto, para evitar falhas, você deve fornecer apenas as coisas que suportam a adição.

def foo(a,b):
    return a + b

class Bar(object):
    pass

class Zoo(object):
    def __add__(self, other):
        return 'zoom'

if __name__=='__main__':
    print foo(1, 2)
    print foo('james', 'bond')
    print foo(Zoo(), Zoo())
    print foo(Bar(), Bar()) # Should fail
Pratik Deoghare
fonte
1

Eu não vi isso mencionado em outras respostas, então vou adicionar isso ao pote.

Como outros já disseram, o Python não aplica o tipo nos parâmetros de função ou método. Supõe-se que você saiba o que está fazendo e que, se realmente precisar saber o tipo de algo que foi repassado, irá verificar e decidir o que fazer por si mesmo.

Uma das principais ferramentas para fazer isso é a função isinstance ().

Por exemplo, se eu escrever um método que espera obter dados binários de texto brutos, em vez das seqüências codificadas utf-8 normais, poderia verificar o tipo de parâmetros no caminho e adaptar-me ao que encontro ou gerar uma exceção para recusar.

def process(data):
    if not isinstance(data, bytes) and not isinstance(data, bytearray):
        raise TypeError('Invalid type: data must be a byte string or bytearray, not %r' % type(data))
    # Do more stuff

O Python também fornece todos os tipos de ferramentas para cavar objetos. Se você é corajoso, pode até usar o importlib para criar seus próprios objetos de classes arbitrárias em tempo real. Eu fiz isso para recriar objetos de dados JSON. Tal coisa seria um pesadelo em uma linguagem estática como C ++.

Medo Quixadhal
fonte
1

Para usar efetivamente o módulo de digitação (novo no Python 3.5), inclua all ( *).

from typing import *

E você estará pronto para usar:

List, Tuple, Set, Map - for list, tuple, set and map respectively.
Iterable - useful for generators.
Any - when it could be anything.
Union - when it could be anything within a specified set of types, as opposed to Any.
Optional - when it might be None. Shorthand for Union[T, None].
TypeVar - used with generics.
Callable - used primarily for functions, but could be used for other callables.

No entanto, ainda é possível usar nomes de tipo como int, list, dict, ...

prosti
fonte
1

Eu implementei um wrapper se alguém quiser especificar tipos de variáveis.

import functools

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for i in range(len(args)):
            v = args[i]
            v_name = list(func.__annotations__.keys())[i]
            v_type = list(func.__annotations__.values())[i]
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        v = result
        v_name = 'return'
        v_type = func.__annotations__['return']
        error_msg = 'Variable `' + str(v_name) + '` should be type ('
        error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
        if not isinstance(v, v_type):
                raise TypeError(error_msg)
        return result

    return check

Use-o como:

@type_check
def test(name : str) -> float:
    return 3.0

@type_check
def test2(name : str) -> str:
    return 3.0

>> test('asd')
>> 3.0

>> test(42)
>> TypeError: Variable `name` should be type (<class 'str'>) but instead is type (<class 'int'>)

>> test2('asd')
>> TypeError: Variable `return` should be type (<class 'str'>) but instead is type (<class 'float'>)

EDITAR

O código acima não funcionará se nenhum dos argumentos (ou retorno) for declarado. A edição a seguir pode ajudar, por outro lado, funciona apenas para kwargs e não verifica argumentos.

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for name, value in kwargs.items():
            v = value
            v_name = name
            if name not in func.__annotations__:
                continue

            v_type = func.__annotations__[name]

            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ') '
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        if 'return' in func.__annotations__:
            v = result
            v_name = 'return'
            v_type = func.__annotations__['return']
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                    raise TypeError(error_msg)
        return result

    return check
Gergely Papp
fonte