Làm thế nào để các hàm Python xử lý các loại tham số mà bạn truyền vào?


305

Trừ khi tôi nhầm, việc tạo một hàm trong Python hoạt động như thế này:

def my_func(param1, param2):
    # stuff

Tuy nhiên, bạn không thực sự đưa ra các loại tham số đó. Ngoài ra, nếu tôi nhớ, Python là một ngôn ngữ được gõ mạnh, vì vậy, có vẻ như Python không nên cho phép bạn chuyển một tham số thuộc loại khác với trình tạo hàm dự kiến. Tuy nhiên, làm thế nào để Python biết rằng người dùng của hàm đang chuyển qua các loại thích hợp? Chương trình sẽ chết nếu nó là loại sai, giả sử hàm thực sự sử dụng tham số? Bạn có phải chỉ định loại?


15
Tôi nghĩ rằng câu trả lời được chấp nhận trong câu hỏi này nên được cập nhật để phù hợp hơn với các khả năng hiện tại mà Python cung cấp. Tôi nghĩ rằng câu trả lời này làm công việc.
code_dredd

Câu trả lời:


173

Python được gõ mạnh bởi vì mọi đối tượng đều có một loại, mọi đối tượng đều biết loại của nó, không thể vô tình hay cố ý sử dụng một đối tượng thuộc loại "như thể" nó là một đối tượng thuộc loại khác và tất cả các hoạt động cơ bản trên đối tượng là ủy thác cho loại của nó.

Điều này không có gì để làm với tên . Một tên trong Python không "có một loại": nếu và khi một tên được xác định, tên đó đề cập đến một đối tượngđối tượng đó có một loại (nhưng thực tế điều đó không buộc một loại trên tên : a tên là một cái tên).

Một tên trong Python hoàn toàn có thể tham chiếu đến các đối tượng khác nhau vào các thời điểm khác nhau (như trong hầu hết các ngôn ngữ lập trình, mặc dù không phải tất cả) - và không có ràng buộc nào đối với tên đó, nếu nó đã từng đề cập đến một đối tượng loại X, nó thì mãi mãi bị hạn chế để chỉ đề cập đến các đối tượng khác kiểu ràng buộc X. trên tên không nằm trong khái niệm "đánh mạnh", mặc dù một số người đam mê tĩnh đánh máy (nơi tên làm được hạn chế, và trong một tĩnh, AKA compile- thời gian, thời trang cũng vậy) sử dụng sai thuật ngữ theo cách này.


71
Vì vậy, có vẻ như gõ mạnh không quá mạnh, trong trường hợp cụ thể này, nó yếu hơn gõ tĩnh.IMHO, biên dịch ràng buộc thời gian gõ tên / biến / tham chiếu thực sự khá quan trọng, do đó tôi mạnh dạn tuyên bố python không tốt như gõ tĩnh trên khía cạnh này. Vui long sửa cho tôi nêu tôi sai.
liang

19
@liang Đó là một ý kiến, vì vậy bạn không thể đúng hay sai. Đó chắc chắn cũng là ý kiến ​​của tôi, và tôi đã thử nhiều ngôn ngữ. Việc tôi không thể sử dụng IDE của mình để tìm ra loại (và do đó là thành viên) của các tham số là một nhược điểm lớn của python. Nếu nhược điểm này quan trọng hơn những lợi thế của việc gõ vịt phụ thuộc vào người bạn yêu cầu.
Maarten Bodewes

6
Nhưng điều này không trả lời bất kỳ câu hỏi nào: "Tuy nhiên, làm thế nào Python biết rằng người dùng hàm đang truyền đúng loại? Chương trình sẽ chết nếu đó là loại sai, giả sử hàm thực sự sử dụng tham số? Bạn có phải chỉ định loại? " hoặc ..
qPCR4vir

4
@ qPCR4vir, bất kỳ đối tượng nào cũng có thể được thông qua dưới dạng đối số. Lỗi (một ngoại lệ, chương trình sẽ không "chết" nếu được mã hóa để bắt nó, xem try/ except) sẽ xảy ra khi và nếu một hoạt động được thử mà đối tượng không hỗ trợ. Trong Python 3.5, bây giờ bạn có thể tùy ý "chỉ định loại" của các đối số, nhưng không có lỗi xảy ra, mỗi lần, nếu thông số đó bị vi phạm; ký hiệu gõ chỉ nhằm mục đích giúp các công cụ riêng biệt thực hiện phân tích, v.v., nó không làm thay đổi hành vi của chính Python.
Alex Martelli

2
@AlexMartelli. Cảm tạ! Đối với tôi đây là câu trả lời đúng: "Lỗi (một ngoại lệ, chương trình sẽ không" chết "nếu được mã hóa để bắt nó, xem thử / ngoại trừ) .."
qPCR4vir

753

Các câu trả lời khác đã làm rất tốt trong việc giải thích cách gõ vịt và câu trả lời đơn giản của tzot :

Python không có biến, giống như các ngôn ngữ khác trong đó các biến có loại và giá trị; Nó có tên chỉ vào các đối tượng, biết loại của chúng.

Tuy nhiên , một điều thú vị đã thay đổi kể từ năm 2010 (khi câu hỏi được đặt ra lần đầu tiên), đó là việc triển khai PEP 3107 (được triển khai trong Python 3). Bây giờ bạn thực sự có thể chỉ định loại tham số và loại trả về của hàm như thế này:

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

Ở đây chúng ta có thể thấy rằng pickcó 2 tham số, một danh sách lvà một số nguyên index. Nó cũng sẽ trả về một số nguyên.

Vì vậy, ở đây có ngụ ý rằng đó llà một danh sách các số nguyên mà chúng ta có thể thấy mà không cần nỗ lực nhiều, nhưng đối với các hàm phức tạp hơn, có thể hơi khó hiểu về danh sách nên chứa. Chúng tôi cũng muốn giá trị mặc định indexlà 0. Để giải quyết điều này, bạn có thể chọn viết picknhư thế này:

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

Lưu ý rằng bây giờ chúng ta đặt một chuỗi là loại l, được phép theo cú pháp, nhưng nó không tốt cho việc phân tích cú pháp theo chương trình (mà chúng ta sẽ quay lại sau).

Điều quan trọng cần lưu ý là Python sẽ không nâng cao TypeErrornếu bạn vượt qua index, lý do cho điều này là một trong những điểm chính trong triết lý thiết kế của Python: "Tất cả chúng ta đều đồng ý ở đây" , có nghĩa là bạn được mong đợi lưu ý về những gì bạn có thể chuyển đến một chức năng và những gì bạn không thể. Nếu bạn thực sự muốn viết mã ném TypeErrors, bạn có thể sử dụng isinstancehàm để kiểm tra xem đối số được truyền có thuộc loại thích hợp hay một lớp con của nó như thế này không:

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

Thông tin thêm về lý do tại sao bạn hiếm khi nên làm điều này và những gì bạn nên làm thay vào đó được nói đến trong phần tiếp theo và trong các bình luận.

PEP 3107 không chỉ cải thiện khả năng đọc mã mà còn có một số trường hợp sử dụng phù hợp mà bạn có thể đọc về đây .


Chú thích kiểu đã thu hút sự chú ý nhiều hơn trong Python 3.5 với việc giới thiệu PEP 484 , giới thiệu một mô-đun chuẩn cho gợi ý về kiểu.

Những gợi ý loại này đến từ trình kiểm tra loại mypy ( GitHub ), hiện đang tuân thủ PEP 484 .

Với mô-đun gõ đi kèm với một bộ sưu tập gợi ý khá toàn diện, bao gồm:

  • List, Tuple, Set, Map- cho list, tuple, setmaptương ứng.
  • Iterable - hữu ích cho máy phát điện.
  • Any - khi nó có thể là bất cứ điều gì.
  • Union- khi nó có thể là bất cứ thứ gì trong một tập hợp các loại được chỉ định, trái ngược với Any.
  • Optional- khi nó có thể là Không có. Viết tắt cho Union[T, None].
  • TypeVar - được sử dụng với thuốc generic.
  • Callable - được sử dụng chủ yếu cho các chức năng, nhưng có thể được sử dụng cho các tên gọi khác.

Đây là những gợi ý loại phổ biến nhất. Một danh sách đầy đủ có thể được tìm thấy trong tài liệu cho mô-đun gõ .

Dưới đây là ví dụ cũ sử dụng các phương thức chú thích được giới thiệu trong mô-đun gõ:

from typing import List

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

Một tính năng mạnh mẽ là Callablecho phép bạn nhập các phương thức chú thích lấy một hàm làm đối số. Ví dụ:

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

Ví dụ trên có thể trở nên chính xác hơn với việc sử dụng TypeVarthay vì Any, nhưng điều này đã được để lại cho người đọc vì tôi tin rằng tôi đã điền vào câu trả lời của mình với quá nhiều thông tin về các tính năng mới tuyệt vời được kích hoạt bằng cách gõ gợi ý.


Trước đây khi một tài liệu mã Python với Sphinx, ví dụ, một số chức năng trên có thể có được bằng cách viết các tài liệu được định dạng như sau:

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]

Như bạn có thể thấy, điều này cần một số dòng bổ sung (số chính xác phụ thuộc vào mức độ rõ ràng bạn muốn và cách bạn định dạng chuỗi tài liệu của mình). Nhưng bây giờ bạn cần phải rõ ràng về cách PEP 3107 cung cấp một giải pháp thay thế vượt trội hơn nhiều (tất cả?). Điều này đặc biệt đúng khi kết hợp với PEP 484 , như chúng ta đã thấy, cung cấp một mô-đun chuẩn xác định cú pháp cho các gợi ý / chú thích loại này có thể được sử dụng theo cách rõ ràng và chính xác nhưng linh hoạt, tạo ra một sự kết hợp mạnh mẽ.

Theo ý kiến ​​cá nhân của tôi, đây là một trong những tính năng tuyệt vời nhất trong Python từ trước đến nay. Tôi không thể chờ đợi mọi người bắt đầu khai thác sức mạnh của nó. Xin lỗi vì câu trả lời dài, nhưng đây là những gì xảy ra khi tôi cảm thấy phấn khích.


Một ví dụ về mã Python sử dụng nhiều gợi ý kiểu có thể được tìm thấy ở đây .


2
@rickfoosusa: Tôi nghi ngờ bạn không chạy Python 3 trong đó tính năng này đã được thêm vào.
erb

26
Đợi tí! Nếu việc xác định tham số và kiểu trả về không tăng a TypeError, thì điểm sử dụng pick(l: list, index: int) -> intnhư định nghĩa một dòng là gì? Hoặc tôi đã hiểu sai, tôi không biết.
Erdin Eray

24
@Eray Erdin: Đó là một sự hiểu lầm phổ biến và hoàn toàn không phải là một câu hỏi tồi. Nó có thể được sử dụng cho mục đích tài liệu, giúp IDE thực hiện tự động hoàn thành tốt hơn và tìm ra lỗi trước thời gian chạy bằng cách sử dụng phân tích tĩnh (giống như mypy mà tôi đã đề cập trong câu trả lời). Có nhiều hy vọng rằng thời gian chạy có thể tận dụng thông tin và thực sự tăng tốc các chương trình nhưng điều đó có thể sẽ mất rất nhiều thời gian để được triển khai. Bạn có thể cũng có thể tạo ra một trang trí mà ném TypeErrors cho bạn (các thông tin được lưu trữ trong các __annotations__thuộc tính của đối tượng chức năng).
erb

2
@ErdinEray Tôi nên thêm rằng ném TypeErrors là một ý tưởng tồi (gỡ lỗi không bao giờ là niềm vui, cho dù các trường hợp ngoại lệ có chủ ý được đưa ra như thế nào). Nhưng đừng sợ, lợi thế của các tính năng mới được mô tả trong câu trả lời của tôi cho phép một cách tốt hơn: không dựa vào bất kỳ kiểm tra nào trong thời gian chạy, làm mọi thứ trước thời gian chạy với mypy hoặc sử dụng trình chỉnh sửa phân tích tĩnh cho bạn như PyCharm .
erb

2
@Tony: Khi bạn trả lại hai hoặc nhiều đối tượng bạn thực sự trả lại một tuple, vì vậy bạn nên sử dụng chú thích loại Tuple, tức làdef f(a) -> Tuple[int, int]:
erb

14

Bạn không chỉ định một loại. Phương thức sẽ chỉ thất bại (trong thời gian chạy) nếu nó cố truy cập các thuộc tính không được xác định trên các tham số được truyền vào.

Vì vậy, chức năng đơn giản này:

def no_op(param1, param2):
    pass

... sẽ không thất bại cho dù hai đối số được thông qua.

Tuy nhiên, chức năng này:

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

... sẽ thất bại trong thời gian chạy nếu param1param2cả hai đều không có thuộc tính có thể gọi được quack.


+1: Các thuộc tính và phương thức không được xác định tĩnh. Khái niệm về cách thức "loại thích hợp" hoặc "loại sai" này được thiết lập bằng cách loại đó có hoạt động đúng trong chức năng hay không.
S.Lott

11

Nhiều ngôn ngữ có các biến, thuộc loại cụ thể và có giá trị. Python không có biến; nó có các đối tượng và bạn sử dụng tên để chỉ các đối tượng này.

Trong các ngôn ngữ khác, khi bạn nói:

a = 1

sau đó một biến (thường là số nguyên) thay đổi nội dung của nó thành giá trị 1.

Trong Python

a = 1

có nghĩa là sử dụng tên a để chỉ đối tượng 1 . Bạn có thể thực hiện các thao tác sau trong phiên Python tương tác:

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

Hàm typeđược gọi với đối tượng 1; vì mọi đối tượng đều biết loại của nó, thật dễ dàng typeđể tìm ra loại đã nói và trả lại nó.

Tương tự như vậy, bất cứ khi nào bạn xác định một chức năng

def funcname(param1, param2):

hàm nhận hai đối tượng và đặt tên cho chúng param1param2bất kể kiểu của chúng là gì. Nếu bạn muốn đảm bảo các đối tượng nhận được thuộc một loại cụ thể, hãy mã hóa chức năng của bạn như thể chúng thuộc loại cần thiết và bắt các ngoại lệ được ném nếu chúng không có. Các ngoại lệ được ném thường là TypeError(bạn đã sử dụng một thao tác không hợp lệ) và AttributeError(bạn đã cố gắng truy cập một thành viên không tồn tại (các phương thức cũng là thành viên)).


8

Python không được gõ mạnh theo nghĩa kiểm tra kiểu tĩnh hoặc thời gian biên dịch.

Hầu hết mã Python nằm trong cái gọi là "Duck Typing" - ví dụ: bạn tìm phương thức readtrên một đối tượng - bạn không quan tâm nếu đối tượng là một tệp trên đĩa hoặc ổ cắm, bạn chỉ muốn đọc N byte từ nó.


21
Python được gõ mạnh. Nó cũng được gõ động.
Daniel Newby

1
Nhưng điều này không trả lời bất kỳ câu hỏi nào: "Tuy nhiên, làm thế nào Python biết rằng người dùng hàm đang truyền đúng loại? Chương trình sẽ chết nếu đó là loại sai, giả sử hàm thực sự sử dụng tham số? Bạn có phải chỉ định loại? " hoặc ..
qPCR4vir

6

Như Alex Martelli giải thích ,

Giải pháp ưa thích, bình thường của Pythonic gần như là "gõ vịt": thử sử dụng đối số như thể đó là một loại mong muốn nhất định, thực hiện trong một thử / ngoại trừ câu bắt được tất cả các ngoại lệ có thể xảy ra nếu thực tế không phải là đối số thuộc loại đó (hoặc bất kỳ loại nào khác giống như vịt bắt chước nó ;-) và trong mệnh đề ngoại trừ, hãy thử một cái gì đó khác (sử dụng đối số "như thể" nó thuộc loại khác).

Đọc phần còn lại của bài viết của mình để biết thông tin hữu ích.


5

Python không quan tâm những gì bạn truyền vào các chức năng của nó. Khi bạn gọi my_func(a,b), các biến param1 và param2 sau đó sẽ giữ các giá trị của a và b. Python không biết rằng bạn đang gọi hàm với các kiểu thích hợp và mong muốn lập trình viên quan tâm đến điều đó. Nếu chức năng của bạn sẽ được gọi với các loại tham số khác nhau, bạn có thể bọc mã truy cập chúng bằng các khối thử / ngoại trừ và đánh giá các tham số theo bất kỳ cách nào bạn muốn.


11
Python không có biến, giống như các ngôn ngữ khác trong đó các biến có loại và giá trị; Nó có tên chỉ vào các đối tượng , biết loại của chúng.
tzot

2

Bạn không bao giờ chỉ định loại; Python có khái niệm gõ vịt ; về cơ bản mã xử lý các tham số sẽ đưa ra các giả định nhất định về chúng - có lẽ bằng cách gọi các phương thức nhất định mà một tham số dự kiến ​​sẽ thực hiện. Nếu tham số là loại sai, thì một ngoại lệ sẽ được ném ra.

Nói chung, tùy thuộc vào mã của bạn để đảm bảo rằng bạn đang chuyển xung quanh các đối tượng thuộc loại thích hợp - không có trình biên dịch để thực thi điều này trước thời hạn.


2

Có một ngoại lệ khét tiếng từ cách gõ vịt đáng được đề cập trên trang này.

Khi strhàm gọi __str__phương thức lớp, nó tinh tế xác định kiểu của nó:

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

Như thể Guido gợi ý cho chúng ta ngoại lệ nào nên đưa ra một chương trình nếu gặp phải loại không mong muốn.


1

Trong Python mọi thứ đều có một loại. Hàm Python sẽ làm bất cứ điều gì nó được yêu cầu làm nếu loại đối số hỗ trợ nó.

Ví dụ: foosẽ thêm mọi thứ có thể __add__ed;) mà không phải lo lắng nhiều về loại của nó. Vì vậy, điều đó có nghĩa là, để tránh thất bại, bạn chỉ nên cung cấp những thứ hỗ trợ thêm.

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

Tôi đã không thấy điều này được đề cập trong các câu trả lời khác, vì vậy tôi sẽ thêm nó vào nồi.

Như những người khác đã nói, Python không thực thi kiểu trên các tham số hàm hoặc phương thức. Giả định rằng bạn biết bạn đang làm gì và nếu bạn thực sự cần biết loại thứ gì đó được truyền vào, bạn sẽ kiểm tra nó và quyết định nên làm gì cho chính mình.

Một trong những công cụ chính để thực hiện điều này là hàm isinstance ().

Ví dụ: nếu tôi viết một phương thức mong muốn nhận được dữ liệu văn bản nhị phân thô, thay vì các chuỗi được mã hóa utf-8 thông thường, tôi có thể kiểm tra loại tham số trên đường và thích nghi với những gì tôi tìm thấy hoặc nâng cao ngoại lệ để từ chối.

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 cũng cung cấp tất cả các loại công cụ để đào sâu vào các đối tượng. Nếu bạn dũng cảm, bạn thậm chí có thể sử dụng importlib để tạo các đối tượng của riêng bạn về các lớp tùy ý, một cách nhanh chóng. Tôi đã làm điều này để tạo lại các đối tượng từ dữ liệu JSON. Một điều như vậy sẽ là một cơn ác mộng trong một ngôn ngữ tĩnh như C ++.


1

Để sử dụng hiệu quả mô-đun gõ (mới trong Python 3.5) bao gồm tất cả ( *).

from typing import *

Và bạn sẽ sẵn sàng để sử dụng:

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.

Tuy nhiên, bạn vẫn có thể sử dụng tên kiểu như int, list, dict, ...


1

Tôi đã triển khai một trình bao bọc nếu có ai muốn chỉ định các loại biến.

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

Sử dụng nó như:

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

BIÊN TẬP

Mã ở trên không hoạt động nếu bất kỳ loại đối số nào (hoặc trả về) không được khai báo. Chỉnh sửa sau đây có thể giúp, mặt khác, nó chỉ hoạt động cho kwargs và không kiểm tra 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.