Як функції Python обробляють типи параметрів, які ви передаєте?


305

Якщо я не помиляюся, створення функції в Python працює так:

def my_func(param1, param2):
    # stuff

Однак типи цих параметрів ви фактично не даєте. Крім того, якщо я пам’ятаю, Python є сильно набраною мовою, тому, схоже, Python не повинен дозволяти вам передавати параметр іншого типу, ніж очікував творець функції. Однак як Python знає, що користувач функції передає належні типи? Чи просто програма помре, якщо вона неправильного типу, якщо функція фактично використовує параметр? Чи потрібно вказати тип?


15
Я думаю, що прийняту відповідь у цьому питанні слід оновити, щоб вона відповідала сучасним можливостям, які пропонує Python. Я думаю, що ця відповідь робить свою роботу.
code_dredd

Відповіді:


172

Python сильно набраний, тому що кожен об’єкт має тип, кожен об'єкт знає його тип, неможливо випадково чи навмисно використовувати об’єкт типу "як би" це був об'єкт іншого типу, і всі елементарні операції над об'єктом є делегований своєму типу.

Це не має нічого спільного з іменами . Ім'я в Python не "мають тип»: якщо і коли це визначено ім'я, назва відноситься до об'єкту , а об'єкт має тип (але це не насправді сили типу на ім'я : A назва - ім’я).

Ім'я в Python може чудово посилатися на різні об'єкти в різний час (як і в більшості мов програмування, хоча і не в усіх) - і в імені немає обмежень, щоб, якщо він колись посилався на об'єкт типу X, це те віки віків змушені посилатися тільки на інші об'єкти X. Обмеження типу на імена не є частиною концепції «сильної типізації», хоча деякі ентузіасти статичної типізації (де імена дійсно отримати стримуються, і в статичному, AKA compile- час, мода теж) неправильно вживають термін.


71
Тож здається, що сильна введення тексту не настільки сильна, в даному конкретному випадку вона слабша, ніж статична типізація. з цього аспекту. Будь ласка, виправте мене, якщо я помиляюся.
laang

19
@liang Це думка, тому ви не можете бути правильними чи неправильними. Це, звичайно, також моя думка, і я перепробував багато мов. Те, що я не можу використовувати свій IDE, щоб дізнатися тип (і, отже, члени) параметрів, є головним недоліком python. Якщо цей недолік важливіший, ніж переваги набору качок, залежить від людини, яку ви просите.
Maarten Bodewes

6
Але це не відповідає на жодне з питань: "Однак, як Python знає, що користувач функції передає належні типи? Чи просто програма помре, якщо вона неправильного типу, якщо припустити, що функція фактично використовує параметр? Чи потрібно вказати тип? " або ..
qPCR4vir

4
@ qPCR4vir, будь-який об'єкт може бути переданий як аргумент. Помилка (виняток, програма не "помре", якщо її закодовано, щоб побачити її, див. try/ except) Виникне, коли і якщо буде зроблена спроба операції, яку об'єкт не підтримує. У Python 3.5 тепер ви можете необов'язково "задавати типи" аргументів, але помилка сама по собі не виникає, якщо специфікація порушена; позначення введення тексту призначене лише для того, щоб допомогти окремим інструментам, які виконують аналіз тощо. Це не змінює поведінку самого Python.
Алекс Мартеллі

2
@AlexMartelli. Спасибі! Для мене це правильна відповідь: "Помилка (виняток, програма не" помре ", якщо її закодовано, щоб її зловити, див. Спробувати / крім)".
qPCR4vir

753

Інші відповіді зробили гарну роботу, пояснивши типи качок та просту відповідь 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як однорядкове визначення тоді? Або я неправильно зрозумів, не знаю.
Ердін Ерай

24
@Eray Erdin: Це звичайне непорозуміння і зовсім не погане питання. Він може бути використаний для цілей документації, допомагає IDE робити кращі автоматичні завершення та знаходити помилки перед початком виконання, використовуючи статичний аналіз (як і мій, про який я згадував у відповіді). Є сподівання, що час виконання може скористатись інформацією та фактично прискорити програми, але це, швидше за все, потребуватиме багато часу, щоб реалізувати. Можливо, ви також зможете створити декоратор, який викидає для вас TypeErrors (інформація зберігається в __annotations__атрибуті об'єкта функції).
erb

2
@ErdinEray Я мушу додати, що кидати TypeErrors - погана ідея (налагодження ніколи не є задоволенням, незалежно від того, наскільки добре висунуті винятки). Але не бійтеся, перевага нових функцій, описаних у моїй відповіді, дозволяє краще: не покладайтеся на будь-яку перевірку під час виконання, робіть усе до початку роботи з mypy або використовуйте редактор, який робить статичний аналіз для вас, наприклад, PyCharm .
erb

2
@Tony: Коли ви повертаєте два або більше об'єктів, ви фактично повертаєте кортеж, тож вам слід використовувати анотацію типу def f(a) -> Tuple[int, int]:
Tuple

14

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

Отже ця проста функція:

def no_op(param1, param2):
    pass

... не провалиться, незалежно від того, які два аргументи передані.

Однак ця функція:

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

... не вдасться виконати, якщо param1і param2обидва не мають атрибутів, що викликаються quack.


+1: Атрибути та методи не визначаються статично. Поняття про те, як би цей "належний тип" чи "неправильний тип" встановлюються тим, чи працює він належним чином у функції.
С.Лотт

11

Багато мов мають змінні, які є певного типу та мають значення. 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(ви намагалися отримати доступ до неіснуючого члена (методи теж є членами)).


8

Python не сильно набраний у сенсі статичної перевірки або перевірки типу компіляції.

Більшість кодів Python потрапляє під так званий "Duck Typing" - наприклад, ви шукаєте методread на об'єкті - вам не байдуже, чи об'єкт є файлом на диску чи сокеті, ви просто хочете прочитати N байти з нього.


21
Python є строго типізований. Він також динамічно набирається.
Даніель Ньюбі

1
Але це не відповідає жодному із запитань: "Однак, як Python знає, що користувач функції передає належні типи? Чи просто програма помре, якщо вона неправильного типу, якщо припустити, що функція фактично використовує параметр? Чи потрібно вказати тип? " або ..
qPCR4vir

6

Як пояснює Алекс Мартеллі ,

Нормальне, пітонічне вподобане рішення майже незмінно "набирає качку": спробуйте використовувати аргумент так, ніби він був певного бажаного типу, зробіть це у спробі / за винятком заяви, що містить усі винятки, які можуть виникнути, якщо аргумент насправді не був. цього типу (або будь-який інший тип, який добре качкує, імітуючи його ;-), і за винятком пункту, спробуйте щось інше (використовуючи аргумент "так, ніби" це було іншого типу).

Прочитайте решту його публікації для корисної інформації.


5

Python не хвилює, що ти передаєш його функції. Коли ви викликаєте my_func(a,b), змінні param1 та param2 утримуватимуть значення a і b. Python не знає, що ви викликаєте функцію відповідними типами, і очікує, що програміст подбає про це. Якщо ваша функція буде викликана з різними типами параметрів, ви можете обгорнути код, який отримує доступ до них, за допомогою блоків спробу / крім, і оцінити параметри будь-яким способом.


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 не застосовує тип параметрів функції чи методу. Передбачається, що ви знаєте, що робите, і що якщо вам дійсно потрібно знати тип того, що було передано, ви перевірите це і вирішите, що робити для себе.

Одним з головних інструментів для цього є функція isin substance ().

Наприклад, якщо я пишу метод, який очікує отримання необроблених бінарних текстових даних, а не звичайні закодовані рядки 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 ++.


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

EDIT

Код, наведений вище, не працює, якщо будь-який з типів аргументів (або повернення) не оголошено. Наступне редагування може допомогти, з іншого боку, воно працює лише для kwargs і не перевіряє аргументи.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.