Làm thế nào để tôi xác định rằng kiểu trả về của một phương thức giống như chính lớp đó?


410

Tôi có đoạn mã sau trong python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

Nhưng biên tập viên của tôi (PyCharm) nói rằng Vị trí tham chiếu không thể được giải quyết (theo __add__phương thức). Làm thế nào tôi nên xác định rằng tôi mong đợi loại trả về là loại Position?

Chỉnh sửa: Tôi nghĩ rằng đây thực sự là một vấn đề PyCharm. Nó thực sự sử dụng thông tin trong các cảnh báo và hoàn thành mã

Nhưng hãy sửa tôi nếu tôi sai và cần sử dụng một số cú pháp khác.

Câu trả lời:


574

TL; DR : nếu bạn đang sử dụng Python 4.0 thì nó chỉ hoạt động. Kể từ hôm nay (2019) trong 3.7+, bạn phải bật tính năng này bằng cách sử dụng câu lệnh tương lai ( from __future__ import annotations) - cho Python 3.6 trở xuống sử dụng chuỗi.

Tôi đoán bạn có ngoại lệ này:

NameError: name 'Position' is not defined

Điều này là do Positionphải được xác định trước khi bạn có thể sử dụng nó trong một chú thích trừ khi bạn đang sử dụng Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 giới thiệu PEP 563: hoãn đánh giá các chú thích . Một mô-đun sử dụng câu lệnh trong tương lai from __future__ import annotationssẽ tự động lưu các chú thích dưới dạng chuỗi:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Điều này được lên kế hoạch để trở thành mặc định trong Python 4.0. Vì Python vẫn là một ngôn ngữ được gõ động nên không có kiểm tra kiểu nào được thực hiện trong thời gian chạy, nên gõ chú thích sẽ không có tác động hiệu suất, phải không? Sai lầm! Trước python 3.7, mô-đun gõ được sử dụng là một trong những mô-đun python chậm nhất trong lõi, vì vậy nếu import typingbạn sẽ thấy hiệu suất tăng gấp 7 lần khi bạn nâng cấp lên 3.7.

Python <3.7: sử dụng chuỗi

Theo PEP 484 , bạn nên sử dụng một chuỗi thay vì chính lớp đó:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Nếu bạn sử dụng khung Django thì điều này có thể quen thuộc vì các mô hình Django cũng sử dụng các chuỗi cho các tham chiếu chuyển tiếp (định nghĩa khóa ngoài trong đó mô hình nước ngoài đang selfhoặc chưa được khai báo). Điều này sẽ làm việc với Pycharm và các công cụ khác.

Nguồn

Các phần có liên quan của PEP 484PEP 563 , để dành cho bạn chuyến đi:

Chuyển tiếp tài liệu tham khảo

Khi một gợi ý kiểu chứa các tên chưa được xác định, định nghĩa đó có thể được biểu thị dưới dạng một chuỗi ký tự, sẽ được giải quyết sau.

Một tình huống trong đó điều này xảy ra phổ biến là định nghĩa của lớp container, trong đó lớp được định nghĩa xảy ra trong chữ ký của một số phương thức. Ví dụ: đoạn mã sau (bắt đầu triển khai cây nhị phân đơn giản) không hoạt động:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Để giải quyết điều này, chúng tôi viết:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

Chuỗi ký tự phải chứa một biểu thức Python hợp lệ (nghĩa là biên dịch (lit, '', 'eval') phải là một đối tượng mã hợp lệ) và nó sẽ đánh giá không có lỗi khi mô-đun đã được tải đầy đủ. Không gian tên cục bộ và toàn cầu trong đó nó được đánh giá phải là cùng một không gian tên trong đó các đối số mặc định cho cùng một hàm sẽ được đánh giá.

và PEP 563:

Trong Python 4.0, các chú thích hàm và biến sẽ không còn được đánh giá tại thời điểm định nghĩa. Thay vào đó, một dạng chuỗi sẽ được bảo tồn trong __annotations__từ điển tương ứng . Trình kiểm tra loại tĩnh sẽ thấy không có sự khác biệt trong hành vi, trong khi các công cụ sử dụng chú thích trong thời gian chạy sẽ phải thực hiện đánh giá bị hoãn.

...

Chức năng được mô tả ở trên có thể được kích hoạt bắt đầu từ Python 3.7 bằng cách nhập đặc biệt sau:

from __future__ import annotations

Thay vào đó, những việc mà bạn có thể muốn làm

A. Xác định một hình nộm Position

Trước định nghĩa lớp, đặt một định nghĩa giả:

class Position(object):
    pass


class Position(object):
    ...

Điều này sẽ thoát khỏi NameErrorvà thậm chí có thể trông ổn:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

Nhưng nó là?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

B. Khỉ vá để thêm chú thích:

Bạn có thể muốn thử một số phép thuật lập trình meta Python và viết một trình trang trí để vá lỗi định nghĩa lớp để thêm chú thích:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

Người trang trí phải chịu trách nhiệm tương đương với điều này:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

Ít nhất thì nó có vẻ đúng:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Có lẽ là quá nhiều rắc rối.

Phần kết luận

Nếu bạn đang sử dụng 3.6 trở xuống, hãy sử dụng một chuỗi ký tự chứa tên lớp, trong 3.7 sử dụng from __future__ import annotationsvà nó sẽ chỉ hoạt động.


2
Đúng vậy, đây không phải là vấn đề PyCharm và hơn nữa là vấn đề Python 3.5 PEP 484. Tôi nghi ngờ bạn sẽ nhận được cảnh báo tương tự nếu bạn chạy nó thông qua công cụ loại mypy.
Paul Everitt

23
> nếu bạn đang sử dụng Python 4.0, nó chỉ hoạt động, bạn đã thấy Sarah Connor chưa? :)
Scrutari

@JoelBerkeley tôi chỉ thử nghiệm nó và các tham số kiểu làm việc cho tôi trên 3.6, chỉ cần đừng quên để nhập khẩu từ typingnhư bất kỳ loại bạn sử dụng phải nằm trong phạm vi khi chuỗi được đánh giá.
Paulo Scardine

à, lỗi của tôi, tôi chỉ đặt ''vòng quanh lớp chứ không phải các tham số kiểu
joelb

5
Lưu ý quan trọng cho bất cứ ai sử dụng from __future__ import annotations- điều này phải được nhập trước tất cả các lần nhập khác.
Artur

16

Chỉ định kiểu là chuỗi là tốt, nhưng luôn luôn cho tôi biết một chút về cơ bản chúng ta đang phá vỡ trình phân tích cú pháp. Vì vậy, tốt hơn hết bạn không nên viết sai bất kỳ một trong những chuỗi nghĩa đen này:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Một biến thể nhỏ là sử dụng một kiểu chữ bị ràng buộc, ít nhất sau đó bạn phải viết chuỗi chỉ một lần khi khai báo kiểu chữ:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)

8
Tôi muốn Python có một typing.Selfđể xác định rõ ràng điều này.
Alexander Huszagh

2
Tôi đến đây để xem nếu một cái gì đó giống như bạn typing.Selftồn tại. Trả về một chuỗi mã hóa cứng không trả về đúng loại khi tận dụng tính đa hình. Trong trường hợp của tôi, tôi muốn thực hiện một phân loại deserialize . Tôi quyết định trả lại một dict (kwargs) và gọi some_class(**some_class.deserialize(raw_data)).
Scott P.

Các chú thích loại được sử dụng ở đây là phù hợp khi thực hiện chính xác điều này để sử dụng các lớp con. Tuy nhiên, việc thực hiện trả về Positionchứ không phải lớp, vì vậy ví dụ trên không đúng về mặt kỹ thuật. Việc thực hiện nên thay thế Position(bằng một cái gì đó như self.__class__(.
Sam Bull

Ngoài ra, các chú thích nói rằng loại trả về phụ thuộc vào other, nhưng hầu hết có lẽ nó thực sự phụ thuộc vào self. Vì vậy, bạn sẽ cần đặt chú thích selfđể mô tả hành vi chính xác (và có lẽ otherchỉ nên thể Positionhiện rằng nó không bị ràng buộc với kiểu trả về). Điều này cũng có thể được sử dụng cho các trường hợp khi bạn chỉ làm việc với self. ví dụdef __aenter__(self: T) -> T:
Sam Bull

15

Tên 'Position' không có sẵn tại thời điểm cơ thể lớp được phân tích cú pháp. Tôi không biết bạn đang sử dụng các khai báo kiểu như thế nào, nhưng PEP 484 của Python - đó là cách mà hầu hết các chế độ nên sử dụng nếu sử dụng các gợi ý gõ này nói rằng bạn có thể chỉ cần đặt tên dưới dạng một chuỗi tại thời điểm này:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Kiểm tra https://www.python.org/dev/peps/pep-0484/#forward-references - các công cụ phù hợp với điều đó sẽ biết để mở tên lớp từ đó và sử dụng nó. lưu ý rằng chính ngôn ngữ Python không làm gì trong các chú thích này - chúng thường có nghĩa là để phân tích mã tĩnh hoặc người ta có thể có một thư viện / khung để kiểm tra kiểu trong thời gian chạy - nhưng bạn phải đặt rõ ràng điều đó).

cập nhật Ngoài ra, kể từ Python 3.8, hãy kiểm tra pep-563 - kể từ Python 3.8, có thể viết from __future__ import annotationsđể trì hoãn việc đánh giá các chú thích - các lớp tham chiếu chuyển tiếp sẽ hoạt động đơn giản.


9

Khi một gợi ý loại dựa trên chuỗi được chấp nhận, __qualname__mục này cũng có thể được sử dụng. Nó giữ tên của lớp và nó có sẵn trong phần thân của định nghĩa lớp.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

Bằng cách này, đổi tên lớp không có nghĩa là sửa đổi các gợi ý loại. Nhưng cá nhân tôi không mong đợi các biên tập viên mã thông minh sẽ xử lý tốt biểu mẫu này.


1
Điều này đặc biệt hữu ích vì nó không mã hóa tên lớp, vì vậy nó tiếp tục hoạt động trong các lớp con.
Florian Brucker

Tôi không chắc liệu điều này có hoạt động với việc đánh giá các chú thích bị hoãn (PEP 563) hay không, vì vậy tôi đã hỏi một câu hỏi cho điều đó .
Florian Brucker
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.