ฉันจะระบุได้อย่างไรว่าชนิดการคืนค่าของเมธอดนั้นเหมือนกับคลาสของตัวเอง?


410

ฉันมีรหัสต่อไปนี้ในหลาม 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)

แต่บรรณาธิการของฉัน (PyCharm) บอกว่าตำแหน่งอ้างอิงไม่สามารถแก้ไขได้ (ใน__add__วิธีการ) ฉันจะระบุได้อย่างไรว่าฉันต้องการประเภทการส่งคืนเป็นประเภทPositionใด

แก้ไข: ฉันคิดว่านี่เป็นปัญหา PyCharm จริง ๆ แล้วมันใช้ข้อมูลในการเตือนและการทำให้โค้ดเสร็จสมบูรณ์

แต่แก้ไขให้ฉันถ้าฉันผิดและจำเป็นต้องใช้ไวยากรณ์อื่น ๆ

คำตอบ:


574

TL; DR : ถ้าคุณใช้ Python 4.0 มันใช้งานได้ ณ วันนี้ (2019) ใน 3.7+ คุณต้องเปิดใช้คุณสมบัตินี้โดยใช้คำสั่งในอนาคต ( from __future__ import annotations) - สำหรับ Python 3.6 หรือต่ำกว่าใช้สตริง

ฉันเดาว่าคุณได้รับข้อยกเว้นนี้:

NameError: name 'Position' is not defined

นี่เป็นเพราะPositionต้องถูกกำหนดไว้ก่อนที่คุณจะสามารถใช้มันในการเพิ่มความคิดเห็นเว้นแต่ว่าคุณกำลังใช้ Python 4

Python 3.7+: from __future__ import annotations

งูหลาม 3.7 แนะนำPEP 563: การประเมินผลการเลื่อนออกไปคำอธิบายประกอบ โมดูลที่ใช้คำสั่งในอนาคตfrom __future__ import annotationsจะเก็บบันทึกย่อเป็นสตริงโดยอัตโนมัติ:

from __future__ import annotations

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

สิ่งนี้ถูกกำหนดให้เป็นค่าเริ่มต้นใน Python 4.0 เนื่องจาก Python ยังคงเป็นภาษาที่พิมพ์แบบไดนามิกดังนั้นจึงไม่มีการตรวจสอบชนิดที่รันไทม์การพิมพ์คำอธิบายประกอบจึงไม่มีผลกระทบต่อประสิทธิภาพใช่ไหม ไม่ถูกต้อง! ก่อนที่ไพ ธ อน 3.7 โมดูลการพิมพ์เคยเป็นหนึ่งในโมดูลหลามที่ช้าที่สุดในแกนดังนั้นถ้าimport typingคุณจะเห็นประสิทธิภาพเพิ่มขึ้นถึง 7 เท่าเมื่อคุณอัพเกรดเป็น 3.7

Python <3.7: ใช้สตริง

ตาม PEP 484คุณควรใช้สตริงแทนคลาส:

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

หากคุณใช้กรอบ Django นี้อาจจะคุ้นเคยเป็นแบบจำลอง Django ยังใช้สตริงอ้างอิงข้างหน้า (คำจำกัดความที่สำคัญต่างประเทศที่รูปแบบต่างประเทศselfหรือไม่ได้ประกาศยัง) สิ่งนี้ควรทำงานกับ Pycharm และเครื่องมืออื่น ๆ

แหล่งที่มา

ชิ้นส่วนที่เกี่ยวข้องของPEP 484และPEP 563เพื่อสำรองการเดินทางของคุณ:

ส่งต่อการอ้างอิง

เมื่อคำใบ้ประเภทมีชื่อที่ยังไม่ได้กำหนดนิยามอาจจะแสดงเป็นตัวอักษรสตริงที่จะได้รับการแก้ไขในภายหลัง

สถานการณ์ที่สิ่งนี้เกิดขึ้นโดยทั่วไปคือคำจำกัดความของคลาสคอนเทนเนอร์ซึ่งคลาสที่ถูกนิยามนั้นเกิดขึ้นในลายเซ็นต์ของบางเมธอด ตัวอย่างเช่นรหัสต่อไปนี้ (การเริ่มต้นของการนำต้นไม้ไบนารีแบบง่าย) ไม่ทำงาน:

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

เพื่อเขียนสิ่งนี้เราเขียน:

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

สตริงตัวอักษรควรประกอบด้วยนิพจน์ Python ที่ถูกต้อง (เช่นคอมไพล์ (lit, '', 'eval') ควรเป็นอ็อบเจกต์โค้ดที่ถูกต้อง) และควรประเมินโดยไม่มีข้อผิดพลาดเมื่อโมดูลโหลดเต็มแล้ว เนมสเปซท้องถิ่นและทั่วโลกที่ได้รับการประเมินควรเป็นเนมสเปซเดียวกันซึ่งจะมีการประเมินอาร์กิวเมนต์เริ่มต้นสำหรับฟังก์ชันเดียวกัน

และ PEP 563:

ใน Python 4.0 คำอธิบายประกอบของฟังก์ชันและตัวแปรจะไม่ได้รับการประเมินอีกต่อไปตามเวลาที่กำหนด รูปแบบสตริงจะถูกเก็บไว้ใน__annotations__พจนานุกรมที่เกี่ยวข้องแทน ตัวตรวจสอบชนิดคงที่จะไม่เห็นความแตกต่างในพฤติกรรมในขณะที่เครื่องมือที่ใช้คำอธิบายประกอบที่รันไทม์จะต้องทำการประเมินผลที่เลื่อนออกไป

...

ฟังก์ชั่นที่อธิบายไว้ข้างต้นสามารถเปิดใช้งานได้ตั้งแต่ Python 3.7 โดยใช้การนำเข้าพิเศษต่อไปนี้:

from __future__ import annotations

สิ่งที่คุณอาจถูกล่อลวงให้ทำแทน

A. กำหนดหุ่นจำลอง Position

ก่อนการกำหนดคลาสให้วางนิยามดัมมี:

class Position(object):
    pass


class Position(object):
    ...

สิ่งนี้จะกำจัดNameErrorและยังอาจดูโอเค:

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

แต่มันคืออะไร

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

B. Monkey-patch เพื่อเพิ่มหมายเหตุประกอบ:

คุณอาจต้องการลองใช้เวทมนตร์การเขียนโปรแกรมเมตางูหลามและเขียนมัณฑนากรเพื่อปรับแต่งคำจำกัดความของคลาสเพื่อเพิ่มคำอธิบายประกอบ:

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

มัณฑนากรควรรับผิดชอบสิ่งนี้:

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

อย่างน้อยก็ดูเหมือนว่าถูก:

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

อาจเป็นปัญหามากเกินไป

ข้อสรุป

หากคุณใช้ 3.6 หรือต่ำกว่าให้ใช้สตริงตัวอักษรที่มีชื่อคลาสในการใช้ 3.7 from __future__ import annotationsและมันจะใช้งานได้


2
ใช่นี่เป็นปัญหา PyCharm น้อยลงและปัญหา Python 3.5 PEP 484 อีกมาก ฉันสงสัยว่าคุณจะได้รับคำเตือนแบบเดียวกันหากคุณใช้เครื่องมือประเภท mypy
Paul Everitt

23
> ถ้าคุณใช้ Python 4.0 มันใช้งานได้จริงคุณเคยเห็น Sarah Connor ไหม? :)
scrutari

@ JoelBerkeley ฉันเพิ่งทดสอบและพิมพ์พารามิเตอร์ที่เหมาะกับฉันในวันที่ 3.6 อย่าลืมที่จะนำเข้าจากtypingประเภทใด ๆ ที่คุณใช้จะต้องอยู่ในขอบเขตเมื่อประเมินสตริง
เปาโล Scardine

อาผิดพลาดของฉันฉันแค่ใส่''ชั้นเรียนไม่ใช่พารามิเตอร์ประเภท
joelb

5
หมายเหตุสำคัญสำหรับทุกคนที่ใช้from __future__ import annotations- จะต้องนำเข้าก่อนการนำเข้าอื่นทั้งหมด
Artur

16

การระบุชนิดเป็นสายอักขระเป็นสิ่งที่ดี แต่ให้ฉันเล็กน้อยที่เราจะหลีกเลี่ยง parser ดังนั้นคุณไม่ควรสะกดคำใด ๆ ของสตริงตัวอักษรเหล่านี้:

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

รูปแบบที่แตกต่างเล็กน้อยคือการใช้ typevar ที่ถูกผูกไว้อย่างน้อยที่สุดคุณต้องเขียนสตริงเพียงครั้งเดียวเมื่อประกาศ typevar:

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
ฉันหวังว่าหลามtyping.Selfจะต้องระบุสิ่งนี้อย่างชัดเจน
Alexander Huszagh

2
ฉันมาที่นี่เพื่อดูว่ามีอะไรเหมือนคุณtyping.Selfอยู่หรือไม่ การส่งคืนสตริงฮาร์ดโค้ดที่ล้มเหลวในการส่งคืนชนิดที่ถูกต้องเมื่อใช้ประโยชน์จากความหลากหลาย ในกรณีของผมผมอยากจะใช้deserialize classmethod ฉันตัดสินกลับ Dict (kwargs) some_class(**some_class.deserialize(raw_data))และเรียก
สกอตต์พี.

คำอธิบายประกอบประเภทที่ใช้ที่นี่เหมาะสมเมื่อใช้สิ่งนี้อย่างถูกต้องเพื่อใช้คลาสย่อย อย่างไรก็ตามการใช้งานจะส่งคืนPositionไม่ใช่คลาสดังนั้นตัวอย่างข้างต้นไม่ถูกต้องทางเทคนิค การดำเนินการควรเปลี่ยนกับสิ่งที่ต้องการPosition( self.__class__(
Sam Bull

นอกจากนี้คำอธิบายประกอบบอกว่าประเภทผลตอบแทนขึ้นอยู่กับแต่ส่วนใหญ่อาจเป็นจริงขึ้นอยู่กับother selfดังนั้นคุณจะต้องใส่คำอธิบายประกอบselfเพื่ออธิบายพฤติกรรมที่ถูกต้อง (และอาจotherเป็นเพียงPositionการแสดงให้เห็นว่ามันไม่ได้ผูกกับประเภทที่ส่งคืน) สามารถใช้กับกรณีที่คุณทำงานด้วยselfเท่านั้น เช่นdef __aenter__(self: T) -> T:
Sam Bull

15

ชื่อ 'ตำแหน่ง' ไม่สามารถใช้งานได้ในเวลาที่แยกวิเคราะห์เนื้อหาของคลาส ฉันไม่รู้ว่าคุณใช้การประกาศประเภทอย่างไร แต่ PEPON PEP 484 - ซึ่งเป็นโหมดส่วนใหญ่ที่ควรใช้หากใช้คำแนะนำการพิมพ์เหล่านี้บอกว่าคุณสามารถใส่ชื่อเป็นสตริงได้ ณ จุดนี้:

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

ตรวจสอบhttps://www.python.org/dev/peps/pep-0484/#forward-references - เครื่องมือที่สอดคล้องกับที่จะรู้ว่าจะแกะชื่อคลาสออกจากที่นั่นและใช้ประโยชน์จากมัน (เป็นสิ่งสำคัญเสมอที่จะต้องมี โปรดทราบว่าภาษา Python ไม่ได้ทำหมายเหตุประกอบเหล่านี้ - โดยทั่วไปจะใช้สำหรับการวิเคราะห์รหัสคงที่หรืออาจมีไลบรารี / กรอบงานสำหรับการตรวจสอบชนิดในเวลาทำงาน - แต่คุณต้องกำหนดอย่างชัดเจน)

updateเช่นเดียวกับ Python 3.8 ตรวจสอบpep-563 - จาก Python 3.8 เป็นไปได้ที่จะเขียนfrom __future__ import annotationsเพื่อเลื่อนการประเมินผลของคำอธิบายประกอบ - คลาสการอ้างอิงไปข้างหน้าควรทำงานได้อย่างตรงไปตรงมา


9

เมื่อคำใบ้ประเภทสตริงเป็นที่ยอมรับ__qualname__รายการก็สามารถใช้ มันถือชื่อของชั้นเรียนและมีอยู่ในร่างกายของคำจำกัดความของชั้นเรียน

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

โดยการทำเช่นนี้การเปลี่ยนชื่อคลาสไม่ได้หมายความถึงการแก้ไขคำใบ้ประเภท แต่โดยส่วนตัวฉันไม่คาดหวังว่าผู้แก้ไขโค้ดอัจฉริยะจะสามารถจัดการกับฟอร์มนี้ได้ดี


1
สิ่งนี้มีประโยชน์เป็นพิเศษเพราะมันไม่ได้เข้ารหัสชื่อคลาสให้ทำงานในคลาสย่อย
Florian Brucker

ผมไม่แน่ใจว่าเรื่องนี้จะทำงานร่วมกับการประเมินผลการเลื่อนออกไปคำอธิบายประกอบ (PEP 563) ดังนั้นฉันได้ถามคำถามว่า
Florian Brucker
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.