Как функции Python обрабатывают типы параметров, которые вы передаете?

307

Если я не ошибаюсь, создание функции в Python работает так:

def my_func(param1, param2):
    # stuff

Однако вы на самом деле не указываете типы этих параметров. Кроме того, если я помню, Python является строго типизированным языком, поэтому кажется, что Python не должен позволять вам передавать параметр другого типа, чем ожидал создатель функции. Однако как Python узнает, что пользователь функции передает правильные типы? Будет ли программа просто умирать, если это неправильный тип, предполагая, что функция действительно использует параметр? Вы должны указать тип?

Лейф Андерсен
источник
15
Я думаю, что принятый ответ на этот вопрос должен быть обновлен, чтобы соответствовать текущим возможностям, которые предлагает Python. Я думаю, что этот ответ делает работу.
code_dredd

Ответы:

174

Python строго типизирован, потому что каждый объект имеет тип, каждый объект знает его тип, невозможно случайно или преднамеренно использовать объект типа «как если бы» это был объект другого типа, и все элементарные операции над объектом делегирован по своему типу.

Это не имеет ничего общего с именами . Имя в Python не «имеют тип»: если и когда это определено имя, название относится к объекту , а объект имеет тип (но это не на самом деле силы типа на имя : A имя есть имя).

Имя в Python может превосходно ссылаться на разные объекты в разное время (как в большинстве языков программирования, хотя и не на всех) - и нет никаких ограничений на имя, так что, если оно когда-то ссылалось на объект типа X, тогда он навсегда ограничен ссылками только на другие объекты типа X. Ограничения на имена не являются частью концепции «строгой типизации», хотя некоторые энтузиасты статической типизации (где имена действительно ограничены, и в статической компиляции AKA время, мода тоже) не употребляйте термин таким образом.

Алекс Мартелли
источник
71
Поэтому кажется, что строгая типизация не так сильна, в данном конкретном случае она слабее, чем статическая. Имхо, ограничение времени компиляции для имени / переменной / ссылки на самом деле очень важно, поэтому я смело заявляю, что python не так хорош, как статическая типизация по этому аспекту. Пожалуйста, поправьте меня, если я ошибаюсь.
Лян
19
@liang Это мнение, так что вы не можете быть правы или неправы. Это, конечно же, мое мнение, и я пробовал много языков. Тот факт, что я не могу использовать свою IDE для определения типа (и, следовательно, членов) параметров, является основным недостатком Python. Если этот недостаток важнее, чем преимущества утки, зависит от человека, которого вы спрашиваете.
Maarten Bodewes
6
Но это не отвечает ни на один из вопросов: «Однако, как Python узнает, что пользователь функции передает правильные типы? Будет ли программа просто умирать, если это неправильный тип, предполагая, что функция действительно использует параметр? Вы должны указать тип? " или ..
qPCR4vir
4
@ qPCR4vir, любой объект может быть передан в качестве аргумента. Ошибка (исключение: программа не «умрет», если она закодирована, чтобы поймать ее, см. try/ except) Произойдет, когда и если будет предпринята попытка выполнить операцию, которую объект не поддерживает. В Python 3.5 теперь вы можете по желанию «указать типы» аргументов, но, по сути, ошибки не возникает, если спецификация нарушена; нотация ввода предназначена только для того, чтобы помочь отдельным инструментам, которые выполняют анализ и т. д., она не изменяет поведение самого Python
Алекс Мартелли
2
@AlexMartelli. Спасибо! Для меня это правильный ответ: «Ошибка (исключение, программа не« умрет », если она закодирована, чтобы поймать ее, см.
Try
754

Другие ответы хорошо объяснили типизацию утки и простой ответ от tzot :

Python не имеет переменных, как и другие языки, где переменные имеют тип и значение; у него есть имена, указывающие на объекты, которые знают их тип.

Однако одна интересная вещь изменилась с 2010 года (когда вопрос был задан впервые), а именно реализация PEP 3107 (реализована в Python 3). Теперь вы можете указать тип параметра и тип возвращаемого типа функции, например:

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

Здесь мы видим, что pickпринимает 2 параметра, список lи целое число index. Также должно возвращаться целое число.

Таким образом, здесь подразумевается, что lэто список целых чисел, который мы можем увидеть без особых усилий, но для более сложных функций это может немного сбить с толку относительно того, что этот список должен содержать. Мы также хотим, чтобы значение по умолчанию indexбыло равно 0. Чтобы решить эту проблему, вы можете написать pickтак:

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

Обратите внимание, что теперь мы помещаем строку как тип l, который синтаксически разрешен, но он не подходит для программного анализа (к которому мы вернемся позже).

Важно отметить, что Python не будет поднимать a, TypeErrorесли вы передадите поплавок index, причина этого - один из основных моментов в философии дизайна Python: «Мы все здесь взрослые по обоюдному согласию» , что означает, что вы должны знать, что вы можете передать функции, а что нет. Если вы действительно хотите написать код, который выдает TypeErrors, вы можете использовать isinstanceфункцию, чтобы проверить, что переданный аргумент имеет правильный тип или его подкласс, например:

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

Подробнее о том, почему вы должны делать это редко, и о том, что вы должны делать вместо этого, говорится в следующем разделе и в комментариях.

PEP 3107 не только улучшает читабельность кода, но также имеет несколько подходящих вариантов использования, о которых вы можете прочитать здесь .


Аннотации типов получили гораздо больше внимания в Python 3.5 с введением PEP 484, который представляет стандартный модуль для подсказок типов.

Эти подсказки о типах пришли из проверки типов Mypy ( GitHub ), которая теперь соответствует PEP 484 .

С модулем набора текста поставляется довольно полный набор подсказок типа, в том числе:

  • List, Tuple, Set, Map- для list, tuple, setи mapсоответственно.
  • Iterable - полезно для генераторов.
  • Any - когда это может быть что угодно.
  • Union- когда это может быть что-либо в указанном наборе типов, в отличие от Any.
  • Optional- когда это может быть Нет. Сокращение для Union[T, None].
  • TypeVar - используется с дженериками.
  • Callable - используется в основном для функций, но может использоваться для других функций.

Это наиболее распространенные типовые подсказки. Полный список можно найти в документации для модуля ввода .

Вот старый пример с использованием методов аннотации, введенных в модуле ввода:

from typing import List

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

Одной из мощных функций является Callableвозможность ввода аннотированных методов, которые принимают функцию в качестве аргумента. Например:

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))

Приведенный выше пример может стать более точным с использованием TypeVarвместо Any, но это было оставлено читателю как упражнение, так как я считаю, что я уже наполнил свой ответ слишком большим количеством информации о замечательных новых функциях, включаемых хинтингом типов.


Ранее, когда один документированный код Python с, например, Sphinx, некоторые из вышеперечисленных функциональных возможностей можно было получить, написав строки документов в следующем формате:

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]

Как видите, для этого требуется ряд дополнительных строк (точное число зависит от того, насколько явно вы хотите быть и как вы форматируете строку документации). Но теперь вам должно быть ясно, как PEP 3107 предоставляет альтернативу, которая во многих (всех?) Отношениях лучше. Это особенно верно в сочетании с PEP 484, который, как мы видели, предоставляет стандартный модуль, определяющий синтаксис для этих подсказок / аннотаций типов, который можно использовать таким образом, чтобы он был однозначным и точным, но при этом гибким, что мощная комбинация.

По моему личному мнению, это одна из величайших возможностей Python. Я не могу ждать, пока люди начнут использовать его силу. Извините за длинный ответ, но это то, что происходит, когда я волнуюсь.


Пример кода Python, который интенсивно использует хинтинг типов, можно найти здесь .

эрб
источник
2
@rickfoosusa: Я подозреваю, что вы не используете Python 3, в который была добавлена ​​эта функция.
erb
26
Подожди минуту! Если определение параметра и возвращаемого типа не вызывает a TypeError, какой смысл использовать определение в pick(l: list, index: int) -> intвиде одной строки? Или я неправильно понял, я не знаю.
Erdin Eray
24
@Eray Erdin: Это распространенное недоразумение и совсем не плохой вопрос. Он может быть использован для целей документирования, помогает IDE лучше выполнять автозаполнение и находить ошибки раньше времени выполнения с помощью статического анализа (так же, как mypy, о котором я упоминал в ответе). Есть надежда, что среда выполнения сможет воспользоваться информацией и фактически ускорить программы, но это, вероятно, займет очень много времени для реализации. Вы могли бы также быть в состоянии создать декоратор , который бросает TypeErrors для вас (информация хранится в __annotations__атрибуте объекта функции).
erb
2
@ErdinEray Я должен добавить, что выбрасывать TypeErrors - плохая идея (отладка никогда не бывает веселой, независимо от того, насколько хорошо вымышленные исключения возникают). Но не бойтесь, преимущество новых функций, описанных в моем ответе, позволяет лучше: не полагаться на какие-либо проверки во время выполнения, делать все заранее во время выполнения с mypy или использовать редактор, который выполняет статический анализ для вас, такой как PyCharm ,
erb
2
@Tony: когда вы возвращаете два или более объектов, вы на самом деле возвращаете кортеж, поэтому вы должны использовать аннотацию типа кортежа, т.е.def f(a) -> Tuple[int, int]:
erb
14

Вы не указываете тип. Метод завершится ошибкой (во время выполнения), только если попытается получить доступ к атрибутам, которые не определены в передаваемых параметрах.

Итак, эта простая функция:

def no_op(param1, param2):
    pass

... не потерпит неудачу независимо от того, какие два аргумента передаются.

Однако эта функция:

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

... потерпит неудачу во время выполнения, если param1и param2оба не имеют именуемых атрибутов quack.

TM.
источник
+1: атрибуты и методы не определяются статически. Концепция того, как этот «правильный тип» или «неправильный тип» определяется тем, работает ли тип должным образом в функции.
S.Lott
12

Многие языки имеют переменные, которые имеют определенный тип и имеют значение. Python не имеет переменных; у него есть объекты, и вы используете имена для ссылки на эти объекты.

На других языках, когда вы говорите:

a = 1

затем переменная (обычно целочисленная) меняет свое содержимое на значение 1.

В Python

a = 1

означает «использовать имя a для обозначения объекта 1 ». Вы можете сделать следующее в интерактивном сеансе Python:

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

Функция typeвызывается с объектом 1; Поскольку каждый объект знает свой тип, его легко typeнайти и вернуть.

Аналогично, всякий раз, когда вы определяете функцию

def funcname(param1, param2):

функция получает два объекта и называет их param1и param2независимо от их типов. Если вы хотите убедиться, что полученные объекты относятся к определенному типу, закодируйте свою функцию так, как если бы они были нужного типа (типов), и перехватите исключения, которые выдают, если это не так. Обычно TypeErrorгенерируются исключения (вы использовали недопустимую операцию) и AttributeError(вы пытались получить доступ к несуществующему члену (методы тоже являются членами)).

tzot
источник
8

Python не является строго типизированным в смысле статической проверки или проверки типов во время компиляции.

Большая часть кода Python подпадает под так называемую «Duck Typing» - например, вы ищете метод readдля объекта - вам все равно, является ли объект файлом на диске или сокетом, вы просто хотите прочитать N байтов из него.

Марк Рушаков
источник
21
Python является строго типизированным. Это также динамически набирается.
Даниэль Ньюби
1
Но это не отвечает ни на один из вопросов: «Однако, как Python узнает, что пользователь функции передает правильные типы? Будет ли программа просто умирать, если это неправильный тип, предполагая, что функция действительно использует параметр? Вы должны указать тип? " или ..
qPCR4vir
6

Как объясняет Алекс Мартелли ,

Нормальное, предпочтительное для Pythonic решение - это почти всегда «типизированная утка»: попробуйте использовать аргумент, как если бы он был определенного желаемого типа, сделайте это в операторе try / исключением, перехватывающем все исключения, которые могут возникнуть, если аргумент фактически не был этого типа (или любого другого типа, прекрасно имитирующего утку ;-), и в предложении исключением, попробуйте что-нибудь еще (используя аргумент "как будто", это был какой-то другой тип).

Прочитайте остальную часть его поста для полезной информации.

Ник преста
источник
5

Python не заботится о том, что вы передаете его функциям. При вызове my_func(a,b)переменные param1 и param2 будут содержать значения a и b. Python не знает, что вы вызываете функцию с правильными типами, и ожидает, что программист позаботится об этом. Если ваша функция будет вызываться с различными типами параметров, вы можете обернуть код, обращаясь к ним, с помощью блоков try / кроме и оценить параметры любым удобным для вас способом.

рукав моря
источник
11
Python не имеет переменных, как и другие языки, где переменные имеют тип и значение; у него есть имена, указывающие на объекты , которые знают их тип.
tzot
2

Вы никогда не указываете тип; Python имеет концепцию типизации утки ; в основном код, который обрабатывает параметры, делает определенные предположения о них - возможно, вызывая определенные методы, которые предполагается реализовать параметром. Если параметр имеет неправильный тип, то будет сгенерировано исключение.

В целом, ваш код должен гарантировать, что вы передаете объекты нужного типа - нет компилятора, который обеспечивал бы это заранее.

Джастин этир
источник
2

Есть одно печально известное исключение из печати утки, о которой стоит упомянуть на этой странице.

Когда strфункция вызывает __str__метод класса, она тонко проверяет свой тип:

>>> 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)

Как будто Гвидо намекает нам, какое исключение должно вызвать программа, если она встречает неожиданный тип.

Энтони Хэтчкинс
источник
1

В Python все имеет тип. Функция Python будет делать все, что от нее требуется, если тип аргументов это поддерживает.

Пример: fooдобавит все, что можно __add__отредактировать;), не беспокоясь о его типе. Это означает, что во избежание неудачи вы должны предоставлять только те вещи, которые поддерживают сложение.

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
Пратик Деогхаре
источник
1

Я не видел, чтобы это упоминалось в других ответах, поэтому я добавлю это в банк.

Как уже говорили другие, Python не устанавливает тип для параметров функции или метода. Предполагается, что вы знаете, что делаете, и что если вам действительно нужно знать тип чего-то, что было передано, вы проверите это и решите, что делать для себя.

Одним из основных инструментов для этого является функция isinstance ().

Например, если я напишу метод, который ожидает получить необработанные двоичные текстовые данные, а не обычные строки в кодировке utf-8, я могу проверить тип параметров на входе и либо адаптироваться к тому, что я найду, либо вызвать исключение, чтобы отказаться.

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

Python также предоставляет все виды инструментов для копания в объектах. Если вы храбры, вы можете даже использовать importlib для создания ваших собственных объектов произвольных классов на лету. Я сделал это, чтобы воссоздать объекты из данных JSON. Такая вещь была бы кошмаром на статическом языке, таком как C ++.

Страх Quixadhal
источник
1

Чтобы эффективно использовать модуль ввода (новый в Python 3.5), включите все ( *).

from typing import *

И вы будете готовы к использованию:

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.

Тем не менее, по- прежнему можно использовать имена типов , как int, list, dict, ...

прости
источник
1

Я реализовал оболочку, если кто-то хотел бы указать типы переменных.

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

Используйте это как:

@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'>)

РЕДАКТИРОВАТЬ

Приведенный выше код не работает, если не объявлен тип аргументов (или возвращаемых). Следующее редактирование может помочь, с другой стороны, оно работает только для kwargs и не проверяет args.

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
Гергели Папп
источник