Bagaimana fungsi Python menangani tipe-tipe parameter yang Anda lewati?

305

Kecuali saya salah, membuat fungsi dalam Python bekerja seperti ini:

def my_func(param1, param2):
    # stuff

Namun, Anda tidak benar-benar memberikan jenis parameter tersebut. Juga, jika saya ingat, Python adalah bahasa yang sangat diketik, karena itu, sepertinya Python seharusnya tidak membiarkan Anda memasukkan parameter tipe yang berbeda dari yang diharapkan oleh pembuat fungsi. Namun, bagaimana Python tahu bahwa pengguna fungsi ini lewat jenis yang tepat? Apakah program hanya akan mati jika itu tipe yang salah, dengan asumsi fungsi tersebut benar-benar menggunakan parameter? Apakah Anda harus menentukan jenisnya?

Leif Andersen
sumber
15
Saya pikir jawaban yang diterima dalam pertanyaan ini harus diperbarui agar lebih sejalan dengan kemampuan saat ini yang ditawarkan Python. Saya pikir jawaban ini berhasil.
code_dredd

Jawaban:

173

Python diketik dengan kuat karena setiap objek memiliki tipe, setiap objek tahu tipenya, tidak mungkin untuk secara tidak sengaja atau sengaja menggunakan objek tipe "seolah-olah" itu adalah objek dari tipe yang berbeda , dan semua operasi elementer pada objek adalah didelegasikan ke jenisnya.

Ini tidak ada hubungannya dengan nama . Sebuah nama dengan Python tidak "memiliki tipe": jika dan ketika suatu nama didefinisikan, nama tersebut merujuk pada suatu objek , dan objek tersebut memang memiliki suatu tipe (tetapi itu sebenarnya tidak memaksa suatu tipe pada nama : a nama adalah nama).

Sebuah nama dengan Python dapat dengan baik merujuk ke objek yang berbeda pada waktu yang berbeda (seperti dalam kebanyakan bahasa pemrograman, meskipun tidak semua) - dan tidak ada batasan pada nama sedemikian rupa, jika pernah merujuk ke objek tipe X, itu kemudian sampai selama-lamanya dibatasi untuk merujuk hanya untuk benda-benda lain dari jenis Kendala X. atas nama bukan bagian dari konsep "mengetik kuat", meskipun beberapa penggemar dari statis mengetik (di mana nama-nama yang bisa dibatasi, dan statis, AKA compile- waktu, fashion, juga) menyalahgunakan istilah dengan cara ini.

Alex Martelli
sumber
71
Jadi sepertinya pengetikan yang kuat tidak terlalu kuat, dalam kasus khusus ini, pengetikan lebih lemah daripada pengetikan statis. pada aspek ini. Harap perbaiki saya jika saya salah.
liang
19
@liang Itu pendapat, jadi Anda tidak mungkin benar atau salah. Ini tentu juga pendapat saya, dan saya sudah mencoba banyak bahasa. Fakta bahwa saya tidak dapat menggunakan IDE saya untuk mengetahui tipe (dan dengan demikian anggota) dari parameter adalah kelemahan utama python. Jika kelemahan ini lebih penting daripada keuntungan mengetik bebek tergantung pada orang yang Anda tanyakan.
Maarten Bodewes
6
Tetapi ini tidak menjawab salah satu pertanyaan: "Namun, bagaimana Python tahu bahwa pengguna fungsi lewat dalam tipe yang tepat? Apakah program akan mati jika itu tipe yang salah, dengan asumsi fungsi tersebut benar-benar menggunakan parameter? Apakah Anda harus menentukan jenisnya? " atau ..
qPCR4vir
4
@ qPCR4vir, objek apa pun dapat diteruskan sebagai argumen. Kesalahan (pengecualian, program tidak akan "mati" jika dikodekan untuk menangkapnya, lihat try/ except) akan terjadi ketika dan jika operasi dicoba bahwa objek tidak mendukung. Dalam Python 3.5 Anda sekarang dapat secara opsional "menentukan jenis" dari argumen, tetapi tidak ada kesalahan, per se, jika spesifikasi dilanggar; notasi pengetikan hanya dimaksudkan untuk membantu alat terpisah yang melakukan analisis dll, itu tidak mengubah perilaku Python itu sendiri.
Alex Martelli
2
@AlexMartelli. Terima kasih! Bagi saya ini adalah jawaban yang tepat: "Kesalahan (pengecualian, program tidak akan" mati "jika dikodekan untuk menangkapnya, lihat coba / kecuali) .."
qPCR4vir
753

Jawaban lain telah melakukan pekerjaan yang baik dalam menjelaskan mengetik bebek dan jawaban sederhana oleh tzot :

Python tidak memiliki variabel, seperti bahasa lain di mana variabel memiliki tipe dan nilai; memiliki nama yang menunjuk ke objek, yang tahu tipenya.

Namun , satu hal yang menarik telah berubah sejak 2010 (ketika pertanyaan pertama kali ditanyakan), yaitu implementasi PEP 3107 (diimplementasikan dalam Python 3). Anda sekarang dapat benar-benar menentukan jenis parameter dan jenis jenis pengembalian fungsi seperti ini:

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

Di sini kita dapat melihat bahwa pickdibutuhkan 2 parameter, daftar ldan bilangan bulat index. Ini juga harus mengembalikan integer.

Jadi di sini tersirat bahwa ladalah daftar bilangan bulat yang dapat kita lihat tanpa banyak usaha, tetapi untuk fungsi yang lebih kompleks dapat sedikit membingungkan mengenai apa yang harus dimasukkan dalam daftar. Kami juga ingin nilai default indexmenjadi 0. Untuk menyelesaikan ini, Anda dapat memilih untuk menulis pickseperti ini sebagai gantinya:

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

Perhatikan bahwa kita sekarang memasukkan string sebagai tipe l, yang diizinkan secara sintaksis, tetapi tidak baik untuk parsing secara terprogram (yang akan kita bahas nanti).

Penting untuk dicatat bahwa Python tidak akan menaikkan TypeErrorjika Anda melayangkan float ke dalam index, alasan untuk ini adalah salah satu poin utama dalam filosofi desain Python: "Kita semua menyetujui orang dewasa di sini" , yang berarti Anda diharapkan untuk Waspadai apa yang bisa Anda berikan ke suatu fungsi dan apa yang tidak bisa Anda lakukan. Jika Anda benar-benar ingin menulis kode yang melempar TypeErrors Anda dapat menggunakan isinstancefungsi untuk memeriksa bahwa argumen yang dikirimkan adalah tipe yang tepat atau subkelasnya seperti ini:

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

Lebih lanjut tentang mengapa Anda jarang melakukan ini dan apa yang harus Anda lakukan malah dibicarakan di bagian selanjutnya dan di komentar.

PEP 3107 tidak hanya meningkatkan keterbacaan kode tetapi juga memiliki beberapa kasus penggunaan pas yang dapat Anda baca di sini .


Jenis anotasi mendapat lebih banyak perhatian di Python 3.5 dengan pengenalan PEP 484 yang memperkenalkan modul standar untuk petunjuk jenis.

Petunjuk tipe ini berasal dari tipe checker mypy ( GitHub ), yang sekarang sesuai dengan PEP 484 .

Dengan modul pengetikan hadir dengan koleksi petunjuk jenis yang cukup komprehensif, termasuk:

  • List, Tuple, Set, Map- untuk list, tuple, setdan mapmasing-masing.
  • Iterable - berguna untuk generator.
  • Any - saat itu bisa apa saja.
  • Union- ketika itu bisa berupa apa saja di dalam set jenis tertentu, sebagai lawan Any.
  • Optional- saat itu mungkin Tidak Ada. Singkatan untuk Union[T, None].
  • TypeVar - Digunakan dengan obat generik.
  • Callable - Digunakan terutama untuk fungsi, tetapi dapat digunakan untuk callable lainnya.

Ini adalah petunjuk jenis yang paling umum. Daftar lengkap dapat ditemukan dalam dokumentasi untuk modul pengetikan .

Berikut ini adalah contoh lama menggunakan metode penjelasan yang diperkenalkan dalam modul pengetikan:

from typing import List

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

Salah satu fitur yang kuat adalah Callableyang memungkinkan Anda mengetik metode anotasi yang mengambil fungsi sebagai argumen. Sebagai contoh:

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

Contoh di atas bisa menjadi lebih tepat dengan penggunaan TypeVaralih - alih Any, tetapi ini telah dibiarkan sebagai latihan bagi pembaca karena saya yakin saya sudah mengisi jawaban saya dengan terlalu banyak informasi tentang fitur-fitur baru yang luar biasa yang diaktifkan dengan mengetikkan petunjuk.


Sebelumnya ketika seseorang mendokumentasikan kode Python dengan Sphinx misalnya beberapa fungsi di atas dapat diperoleh dengan menulis dokumen yang diformat seperti ini:

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]

Seperti yang Anda lihat, ini membutuhkan sejumlah baris tambahan (jumlah pastinya tergantung pada seberapa eksplisit Anda ingin menjadi dan bagaimana Anda memformat dokumen Anda). Tetapi sekarang harus jelas bagi Anda bagaimana PEP 3107 memberikan alternatif yang dalam banyak hal (semua?) Lebih unggul. Hal ini terutama berlaku dalam kombinasi dengan PEP 484 yang, seperti telah kita lihat, menyediakan modul standar yang mendefinisikan sintaks untuk jenis petunjuk / anotasi yang dapat digunakan sedemikian rupa sehingga tidak ambigu dan tepat namun fleksibel, membuat untuk kombinasi yang kuat.

Menurut pendapat pribadi saya, ini adalah salah satu fitur terbesar di Python. Saya tidak sabar menunggu orang mulai memanfaatkan kekuatannya. Maaf untuk jawaban yang panjang, tapi inilah yang terjadi ketika saya bersemangat.


Contoh kode Python yang banyak menggunakan tipe hinting dapat ditemukan di sini .

erb
sumber
2
@rickfoosusa: Saya curiga Anda tidak menjalankan Python 3 di mana fitur itu ditambahkan.
erb
26
Tunggu sebentar! Jika mendefinisikan parameter dan jenis kembali tidak meningkatkan TypeError, apa gunanya menggunakan pick(l: list, index: int) -> intseperti mendefinisikan satu baris? Atau saya salah, saya tidak tahu.
Erdin Eray
24
@Eray Erdin: Itu kesalahpahaman umum dan sama sekali bukan pertanyaan buruk. Ini dapat digunakan untuk keperluan dokumentasi, membantu IDE melakukan pelengkapan otomatis yang lebih baik dan menemukan kesalahan sebelum runtime dengan menggunakan analisis statis (seperti mypy yang saya sebutkan dalam jawaban). Ada harapan bahwa runtime dapat mengambil keuntungan dari informasi dan benar-benar mempercepat program tetapi itu mungkin akan memakan waktu sangat lama untuk diimplementasikan. Anda mungkin juga dapat membuat dekorator yang melemparkan TypeErrors untuk Anda (informasi disimpan dalam __annotations__atribut objek fungsi).
erb
2
@ErdinEray Saya harus menambahkan bahwa melemparkan TypeErrors adalah ide yang buruk (debugging tidak pernah menyenangkan, tidak peduli seberapa baik pengecualian yang dimaksudkan ditingkatkan). Tapi jangan takut, keuntungan dari fitur-fitur baru yang dijelaskan dalam jawaban saya memungkinkan cara yang lebih baik: jangan mengandalkan pengecekan saat runtime, lakukan segalanya sebelum runtime dengan mypy atau gunakan editor yang melakukan analisis statis untuk Anda seperti PyCharm .
erb
2
@Tony: Ketika Anda mengembalikan dua atau lebih objek Anda benar-benar mengembalikan tuple, jadi Anda harus menggunakan anotasi jenis Tuple, yaitudef f(a) -> Tuple[int, int]:
erb
14

Anda tidak menentukan tipe. Metode ini hanya akan gagal (saat runtime) jika mencoba mengakses atribut yang tidak ditentukan pada parameter yang diteruskan.

Jadi fungsi sederhana ini:

def no_op(param1, param2):
    pass

... tidak akan gagal tidak peduli apa argumen yang dilewati.

Namun, fungsi ini:

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

... akan gagal saat runtime jika param1dan param2keduanya tidak memiliki atribut yang dapat dipanggil bernama quack.

TM.
sumber
+1: Atribut dan metode tidak ditentukan secara statis. Konsep tentang bagaimana "tipe yang tepat" atau "tipe yang salah" ini ditentukan oleh apakah tipe tersebut berfungsi dengan baik atau tidak dalam fungsi tersebut.
S.Lott
11

Banyak bahasa memiliki variabel, yang merupakan tipe tertentu dan memiliki nilai. Python tidak memiliki variabel; memiliki objek, dan Anda menggunakan nama untuk merujuk ke objek-objek ini.

Dalam bahasa lain, saat Anda mengatakan:

a = 1

kemudian variabel (biasanya integer) mengubah isinya ke nilai 1.

Dengan Python,

a = 1

berarti “gunakan nama a untuk merujuk ke objek 1 ”. Anda dapat melakukan hal berikut dalam sesi Python interaktif:

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

Fungsi typeini disebut dengan objek 1; karena setiap objek tahu tipenya, mudah untuk typemengetahui tipe kata dan mengembalikannya.

Demikian juga, setiap kali Anda mendefinisikan suatu fungsi

def funcname(param1, param2):

fungsi menerima dua objek, dan menamainya , param1dan param2terlepas dari jenisnya. Jika Anda ingin memastikan objek yang diterima dari jenis tertentu, kode fungsi Anda seolah-olah mereka dari jenis yang diperlukan (s) dan menangkap pengecualian yang dilemparkan jika tidak. Pengecualian yang dilontarkan biasanya TypeError(Anda menggunakan operasi yang tidak valid) dan AttributeError(Anda mencoba mengakses anggota yang tidak ada (metode adalah anggota juga)).

tzot
sumber
8

Python tidak diketik dalam arti pengecekan tipe statis atau waktu kompilasi.

Sebagian besar kode Python berada di bawah apa yang disebut "Bebek Mengetik" - misalnya, Anda mencari metode readpada objek - Anda tidak peduli jika objek tersebut adalah file pada disk atau soket, Anda hanya ingin membaca N byte darinya.

Mark Rushakoff
sumber
21
Python adalah sangat diketik. Ini juga diketik secara dinamis.
Daniel Newby
1
Tetapi ini tidak menjawab salah satu pertanyaan: "Namun, bagaimana Python tahu bahwa pengguna fungsi lewat dalam tipe yang tepat? Apakah program akan mati jika itu tipe yang salah, dengan asumsi fungsi tersebut benar-benar menggunakan parameter? Apakah Anda harus menentukan jenisnya? " atau ..
qPCR4vir
6

Seperti yang dijelaskan Alex Martelli ,

Solusi normal, Pythonic, yang disukai hampir selalu "mengetik bebek": coba gunakan argumen seolah-olah itu dari jenis yang diinginkan, lakukan dalam pernyataan coba / kecuali menangkap semua pengecualian yang bisa muncul jika argumen itu tidak sebenarnya dari tipe itu (atau tipe lain dengan baik menirunya ;-), dan dalam klausa kecuali, coba sesuatu yang lain (menggunakan argumen "seolah-olah" itu dari jenis lain).

Baca sisa jabatannya untuk informasi bermanfaat.

Nick Presta
sumber
5

Python tidak peduli apa yang Anda berikan pada fungsinya. Saat Anda memanggil my_func(a,b), variabel param1 dan param2 kemudian akan menyimpan nilai a dan b. Python tidak tahu bahwa Anda memanggil fungsi dengan tipe yang tepat, dan mengharapkan programmer untuk mengatasinya. Jika fungsi Anda akan dipanggil dengan berbagai jenis parameter, Anda dapat membungkus kode mengaksesnya dengan mencoba / kecuali blok dan mengevaluasi parameter dengan cara apa pun yang Anda inginkan.

Kyle
sumber
11
Python tidak memiliki variabel, seperti bahasa lain di mana variabel memiliki tipe dan nilai; memiliki nama yang menunjuk ke objek , yang tahu tipenya.
tzot
2

Anda tidak pernah menentukan jenisnya; Python memiliki konsep mengetik bebek ; pada dasarnya kode yang memproses parameter akan membuat asumsi tertentu tentang mereka - mungkin dengan memanggil metode tertentu yang diharapkan dapat diimplementasikan oleh parameter. Jika parameternya salah ketik, maka eksepsi akan dilempar.

Secara umum terserah pada kode Anda untuk memastikan bahwa Anda membagikan objek dengan tipe yang tepat - tidak ada kompiler yang menerapkan ini sebelumnya.

Justin Ethier
sumber
2

Ada satu pengecualian terkenal dari nilai ketikan-bebek yang disebutkan di halaman ini.

Ketika strfungsi memanggil __str__metode kelas, secara halus pilih jenisnya:

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

Seolah Guido memberi tahu kita pengecualian mana yang harus dimunculkan oleh suatu program jika menemui jenis yang tidak terduga.

Antony Hatchkins
sumber
1

Dalam Python semuanya memiliki tipe. Fungsi Python akan melakukan apa saja yang diminta untuk dilakukan jika jenis argumen mendukungnya.

Contoh: fooakan menambahkan semua yang bisa __add__diedit;) tanpa perlu khawatir tentang jenisnya. Jadi itu berarti, untuk menghindari kegagalan, Anda harus menyediakan hanya hal-hal yang mendukung penambahan.

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
sumber
1

Saya tidak melihat ini disebutkan dalam jawaban lain, jadi saya akan menambahkan ini ke pot.

Seperti yang orang lain katakan, Python tidak menerapkan tipe pada parameter fungsi atau metode. Diasumsikan bahwa Anda tahu apa yang Anda lakukan, dan bahwa jika Anda benar-benar perlu mengetahui jenis sesuatu yang disahkan, Anda akan memeriksanya dan memutuskan apa yang harus dilakukan untuk diri sendiri.

Salah satu alat utama untuk melakukan ini adalah fungsi isinstance ().

Sebagai contoh, jika saya menulis metode yang mengharapkan untuk mendapatkan data teks biner mentah, daripada string yang dikodekan utf-8 normal, saya bisa memeriksa jenis parameter dalam perjalanan dan baik beradaptasi dengan apa yang saya temukan, atau meningkatkan pengecualian untuk menolak.

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 juga menyediakan semua jenis alat untuk menggali objek. Jika Anda berani, Anda bahkan dapat menggunakan importlib untuk membuat objek sendiri dari kelas sewenang-wenang. Saya melakukan ini untuk membuat ulang objek dari data JSON. Hal seperti itu akan menjadi mimpi buruk dalam bahasa statis seperti C ++.

Takut Quixadhal
sumber
1

Untuk secara efektif menggunakan modul pengetikan (baru dengan Python 3.5) sertakan semua ( *).

from typing import *

Dan Anda akan siap untuk digunakan:

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.

Namun, Anda masih bisa menggunakan nama jenis seperti int, list, dict, ...

prosti
sumber
1

Saya telah menerapkan pembungkus jika ada yang ingin menentukan jenis variabel.

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

Gunakan sebagai:

@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

Kode di atas tidak berfungsi jika salah satu tipe argumen (atau return) tidak dinyatakan. Hasil edit berikut dapat membantu, di sisi lain, ini hanya berfungsi untuk kwargs dan tidak memeriksa argumen.

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
Papp Gergely
sumber