Jenis Python yang mengisyaratkan tanpa impor siklik

110

Saya mencoba untuk membagi kelas besar saya menjadi dua; baik, pada dasarnya ke dalam kelas "utama" dan campuran dengan fungsi tambahan, seperti:

main.py mengajukan:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py mengajukan:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

Sekarang, sementara ini berfungsi dengan baik, petunjuk jenis MyMixin.func2tentu saja tidak dapat berfungsi. Saya tidak bisa mengimpor main.py, karena saya mendapatkan impor siklik dan tanpa petunjuk, editor saya (PyCharm) tidak tahu apaself itu.

Saya menggunakan Python 3.4, bersedia pindah ke 3.5 jika solusi tersedia di sana.

Adakah cara untuk membagi kelas saya menjadi dua file dan menyimpan semua "koneksi" sehingga IDE saya masih menawarkan penyelesaian otomatis & semua hal lain yang berasal darinya dengan mengetahui jenisnya?

velis
sumber
2
Saya tidak berpikir Anda biasanya perlu memberi anotasi pada tipe self, karena ini akan selalu menjadi subclass dari kelas saat ini (dan sistem pengecekan tipe apa pun harus dapat mengetahuinya sendiri). Apakah func2mencoba menelepon func1, yang tidak ditentukan dalam MyMixin? Mungkin harus (sebagai abstractmethod, mungkin)?
Blckknght
juga perhatikan bahwa umumnya kelas yang lebih spesifik (misalnya mixin Anda) harus berada di sebelah kiri kelas dasar dalam definisi kelas class Main(MyMixin, SomeBaseClass)sehingga metode dari kelas yang lebih spesifik dapat menimpa yang dari kelas dasar
Anentropic
3
Saya tidak yakin bagaimana komentar ini berguna, karena mereka bersinggungan dengan pertanyaan yang diajukan. velis tidak meminta tinjauan kode.
Jacob Lee
Petunjuk jenis Python dengan metode kelas yang diimpor memberikan solusi elegan untuk masalah Anda.
Ben Mares

Jawaban:

168

Sayangnya, tidak ada cara yang sangat elegan untuk menangani siklus impor secara umum. Pilihan Anda adalah mendesain ulang kode Anda untuk menghapus ketergantungan siklik, atau jika tidak memungkinkan, lakukan sesuatu seperti ini:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    def func2(self, some_param: 'Main'):
        ...

The TYPE_CHECKINGkonstan selalu Falsepada saat runtime, sehingga impor tidak akan dievaluasi, tapi mypy (dan jenis-memeriksa alat-alat lain) akan mengevaluasi isi dari blok itu.

Kita juga perlu membuat Mainanotasi tipe menjadi string, yang secara efektif meneruskan mendeklarasikannya karena Mainsimbol tidak tersedia pada waktu proses.

Jika Anda menggunakan Python 3.7+, setidaknya kita dapat melewati keharusan memberikan anotasi string eksplisit dengan memanfaatkan PEP 563 :

# some_file.py

from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    # Hooray, cleaner annotations!
    def func2(self, some_param: Main):
        ...

The from __future__ import annotationsimpor akan membuat semua petunjuk jenis be string dan melewatkan mengevaluasi mereka. Ini dapat membantu membuat kode kami di sini sedikit lebih ergonomis.

Semua yang mengatakan, menggunakan mixin dengan mypy kemungkinan akan membutuhkan lebih banyak struktur daripada yang Anda miliki saat ini. Mypy merekomendasikan pendekatan yang pada dasarnya decezemendeskripsikan - untuk membuat ABC yang diwarisi oleh Anda Maindan MyMixinkelas. Saya tidak akan terkejut jika Anda akhirnya perlu melakukan hal serupa untuk membuat pemeriksa Pycharm senang.

Michael0x2a
sumber
4
Terima kasih untuk ini. Python 3.4 saya saat ini tidak memilikinya typing, tetapi PyCharm juga cukup senang if False:.
velis
Satu-satunya masalah adalah ia tidak mengenali MyObject sebagai model Django. Model dan dengan demikian mengomel tentang atribut instance yang didefinisikan di luar__init__
velis
Berikut adalah semangat yang sesuai untuk typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor
25

Untuk orang-orang yang berjuang dengan impor siklik saat mengimpor kelas hanya untuk pemeriksaan Jenis: Anda mungkin ingin menggunakan Referensi Teruskan (PEP 484 - Petunjuk Jenis):

Jika petunjuk tipe berisi nama yang belum ditentukan, definisi tersebut dapat diekspresikan sebagai string literal, untuk diselesaikan nanti.

Jadi, alih-alih:

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

Anda melakukan:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right
Tomasz Bartkowiak
sumber
Mungkin PyCharm. Apakah Anda menggunakan versi terbaru? Apakah Anda mencoba File -> Invalidate Caches?
Tomasz Bartkowiak
Terima kasih. Maaf, saya telah menghapus komentar saya. Telah disebutkan bahwa ini berfungsi, tetapi PyCharm mengeluh. Saya menyelesaikannya menggunakan peretasan jika Salah yang disarankan oleh Velis . Memvalidasi cache tidak menyelesaikannya. Ini mungkin masalah PyCharm.
Jacob Lee
1
@JacobLee Selain if False:Anda juga bisa from typing import TYPE_CHECKINGdan if TYPE_CHECKING:.
luckydonald
11

Masalah yang lebih besar adalah tipe Anda tidak waras untuk memulai. MyMixinmembuat asumsi hardcode bahwa itu akan dicampur Main, sedangkan itu bisa dicampur ke sejumlah kelas lain, dalam hal ini mungkin akan rusak. Jika mixin Anda di-hardcode untuk dicampur ke dalam satu kelas tertentu, Anda juga dapat menulis metode langsung ke dalam kelas itu alih-alih memisahkannya.

Untuk melakukan ini dengan benar dengan pengetikan yang waras, MyMixinharus dikodekan dengan antarmuka , atau kelas abstrak dalam bahasa Python:

import abc


class MixinDependencyInterface(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass


class MyMixin:
    def func2(self: MixinDependencyInterface, xxx):
        self.foo()  # ← mixin only depends on the interface


class Main(MixinDependencyInterface, MyMixin):
    def foo(self):
        print('bar')
menipu
sumber
1
Yah, saya tidak mengatakan solusi saya bagus. Itu hanya apa yang saya coba lakukan untuk membuat kode lebih mudah diatur. Saran Anda mungkin lolos, tetapi ini sebenarnya berarti hanya memindahkan seluruh kelas Utama ke antarmuka dalam kasus khusus saya .
velis
3

Ternyata upaya awal saya cukup dekat dengan solusinya juga. Inilah yang saya gunakan saat ini:

# main.py
import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...


# mymixin.py
if False:
    from main import Main

class MyMixin(object):
    def func2(self: 'Main', xxx):  # <--- note the type hint
        ...

Perhatikan pernyataan import dalam if Falseyang tidak pernah diimpor (tetapi IDE mengetahuinya) dan menggunakan Mainkelas sebagai string karena tidak dikenal pada waktu proses.

velis
sumber
Saya berharap ini menyebabkan peringatan tentang kode mati.
Phil
@Phil: ya, waktu itu saya pakai Python 3.4. Sekarang ada mengetik.TYPE_CHECKING
velis
-4

Saya pikir cara terbaik adalah dengan mengimpor semua kelas dan dependensi dalam sebuah file (seperti __init__.py) dan kemudian from __init__ import *di semua file lainnya.

Dalam hal ini Anda

  1. menghindari banyak referensi ke file dan kelas tersebut dan
  2. juga hanya perlu menambahkan satu baris di setiap file lainnya dan
  3. yang ketiga adalah pycharm yang mengetahui tentang semua kelas yang mungkin Anda gunakan.
AmirHossein
sumber
1
ini berarti Anda memuat semuanya di mana-mana, jika Anda memiliki pustaka yang cukup berat, itu berarti untuk setiap impor Anda perlu memuat seluruh pustaka. + referensi akan bekerja sangat lambat.
Omer Shacham
> itu berarti Anda memuat semuanya di mana-mana. >>>> sama sekali tidak jika Anda memiliki banyak " init .py" atau file lain, dan menghindarinya import *, namun Anda tetap dapat memanfaatkan pendekatan yang mudah ini
Sławomir Lenart