Mengapa menggunakan Abstrak Base Classes di Python?


213

Karena saya terbiasa dengan cara lama mengetik bebek dengan Python, saya gagal memahami kebutuhan akan ABC (kelas dasar abstrak). The bantuan baik tentang bagaimana untuk menggunakannya.

Saya mencoba membaca alasan di PEP , tetapi itu terlintas di kepala saya. Jika saya sedang mencari wadah urutan bisa berubah, saya akan memeriksa __setitem__, atau lebih mungkin mencoba menggunakannya ( EAFP ). Saya belum menemukan penggunaan nyata untuk modul angka , yang memang menggunakan ABC, tapi itulah yang paling dekat saya harus mengerti.

Adakah yang bisa menjelaskan alasannya kepada saya?

Jawaban:


162

Versi pendek

ABC menawarkan tingkat kontrak semantik yang lebih tinggi antara klien dan kelas yang diimplementasikan.

Versi panjang

Ada kontrak antara kelas dan peneleponnya. Kelas berjanji untuk melakukan hal-hal tertentu dan memiliki sifat-sifat tertentu.

Ada beberapa level kontrak.

Pada level yang sangat rendah, kontrak mungkin menyertakan nama metode atau jumlah parameternya.

Dalam bahasa yang diketik secara statis, kontrak itu sebenarnya akan diberlakukan oleh kompiler. Dengan Python, Anda bisa menggunakan EAFP atau mengetik introspeksi untuk mengonfirmasi bahwa objek yang tidak diketahui memenuhi kontrak yang diharapkan ini.

Tetapi ada juga janji semantik tingkat tinggi dalam kontrak.

Sebagai contoh, jika ada __str__()metode, diharapkan untuk mengembalikan representasi string objek. Itu bisa menghapus semua konten objek, melakukan transaksi dan mengeluarkan halaman kosong dari printer ... tetapi ada pemahaman umum tentang apa yang harus dilakukan, dijelaskan dalam manual Python.

Itu adalah kasus khusus, di mana kontrak semantik dijelaskan dalam manual. Apa yang harus print()dilakukan metode ini? Haruskah itu menulis objek ke printer atau garis ke layar, atau yang lain? Tergantung - Anda perlu membaca komentar untuk memahami kontrak lengkap di sini. Sepotong kode klien yang hanya memeriksa bahwa print()metode yang ada telah mengkonfirmasi bagian dari kontrak - bahwa panggilan metode dapat dibuat, tetapi tidak ada kesepakatan tentang semantik tingkat tinggi dari panggilan tersebut.

Mendefinisikan Kelas Basis Abstrak (ABC) adalah cara menghasilkan kontrak antara pelaksana kelas dan penelepon. Ini bukan hanya daftar nama metode, tetapi pemahaman bersama tentang apa yang harus dilakukan metode tersebut. Jika Anda mewarisi dari ABC ini, Anda berjanji untuk mengikuti semua aturan yang dijelaskan dalam komentar, termasuk semantik dari print()metode ini.

Mengetik bebek Python memiliki banyak keunggulan dalam fleksibilitas dibandingkan pengetikan statis, tetapi tidak menyelesaikan semua masalah. ABC menawarkan solusi perantara antara bentuk bebas Python dan ikatan-dan-disiplin bahasa yang diketik secara statis.


12
Saya pikir Anda ada benarnya, tapi saya tidak bisa mengikuti Anda. Jadi apa bedanya, dalam hal kontrak, antara kelas yang mengimplementasikan __contains__dan kelas yang mewarisi dari collections.Container? Dalam contoh Anda, dalam Python selalu ada pemahaman bersama tentang __str__. Implementing __str__membuat janji yang sama dengan mewarisi dari beberapa ABC dan kemudian mengimplementasikan __str__. Dalam kedua kasus Anda dapat melanggar kontrak; tidak ada semantik yang dapat dibuktikan seperti yang kita miliki dalam pengetikan statis.
Muhammad Alkarouri

15
collections.Containeradalah kasus yang merosot, yang hanya mencakup \_\_contains\_\_, dan hanya berarti konvensi yang telah ditentukan. Menggunakan ABC tidak menambah banyak nilai dengan sendirinya, saya setuju. Saya menduga itu ditambahkan untuk memungkinkan (misalnya) Setuntuk mewarisinya. Pada saat Anda Settiba, tiba-tiba milik ABC memiliki semantik yang cukup. Item tidak dapat menjadi milik koleksi dua kali. Itu TIDAK terdeteksi oleh adanya metode.
Oddthinking

3
Ya, saya pikir Setadalah contoh yang lebih baik daripada print(). Saya sedang berusaha menemukan nama metode yang maknanya ambigu, dan tidak bisa dikuasai oleh namanya saja, jadi Anda tidak bisa yakin itu akan melakukan hal yang benar hanya dengan namanya dan manual Python.
Oddthinking

3
Apakah ada peluang untuk menuliskan jawaban Setsebagai contoh alih-alih print? Setsangat masuk akal, @Oddthinking.
Ehtesh Choudhury

1
Saya pikir artikel ini menjelaskannya dengan sangat baik: dbader.org/blog/abstract-base-classes-in-python
szabgab

228

@ Jawaban Oddthinking ini tidak salah, tapi saya pikir itu merindukan nyata , praktis alasan Python memiliki ABC di dunia bebek-mengetik.

Metode abstrak rapi, tetapi menurut saya mereka tidak benar-benar mengisi kasus penggunaan yang belum tercakup oleh mengetik bebek. Kekuatan nyata kelas dasar abstrak terletak pada cara mereka memungkinkan Anda untuk menyesuaikan perilaku isinstancedanissubclass . ( __subclasshook__pada dasarnya adalah API ramah di atas Python __instancecheck__dan__subclasscheck__ kait.) Menyesuaikan konstruksi built-in untuk bekerja pada jenis kustom adalah sangat banyak bagian dari filosofi Python.

Kode sumber Python adalah contoh. Berikut adalah bagaimana collections.Containerdidefinisikan di perpustakaan standar (pada saat penulisan):

class Container(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __contains__(self, x):
        return False

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Container:
            if any("__contains__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Definisi ini __subclasshook__mengatakan bahwa setiap kelas dengan __contains__atribut dianggap sebagai subkelas dari Kontainer, bahkan jika itu bukan subkelas secara langsung. Jadi saya bisa menulis ini:

class ContainAllTheThings(object):
    def __contains__(self, item):
        return True

>>> issubclass(ContainAllTheThings, collections.Container)
True
>>> isinstance(ContainAllTheThings(), collections.Container)
True

Dengan kata lain, jika Anda mengimplementasikan antarmuka yang tepat, Anda adalah subkelas! ABC memberikan cara formal untuk mendefinisikan antarmuka dengan Python, sambil tetap setia pada semangat mengetik-bebek. Selain itu, ini bekerja dengan cara yang menghormati Prinsip Terbuka-Tertutup .

Model objek Python terlihat sangat mirip dengan sistem OO yang lebih "tradisional" (yang saya maksudkan Java *) - kita mendapatkan kelas, objek, dan metode Anda - tetapi ketika Anda menggores permukaan, Anda akan menemukan sesuatu yang jauh lebih kaya dan lebih fleksibel. Demikian pula, gagasan Python tentang kelas dasar abstrak dapat dikenali oleh pengembang Java, tetapi dalam praktiknya mereka dimaksudkan untuk tujuan yang sangat berbeda.

Kadang-kadang saya menemukan diri saya menulis fungsi polimorfik yang dapat bertindak pada satu item atau kumpulan item, dan saya menemukan isinstance(x, collections.Iterable)lebih banyak dibaca daripada hasattr(x, '__iter__')atau try...exceptblok yang setara . (Jika Anda tidak tahu Python, yang mana dari ketiganya akan membuat maksud kode menjadi paling jelas?)

Yang mengatakan, saya menemukan bahwa saya jarang perlu menulis ABC saya sendiri dan saya biasanya menemukan kebutuhan untuk itu melalui refactoring. Jika saya melihat fungsi polimorfik melakukan banyak pemeriksaan atribut, atau banyak fungsi membuat pemeriksaan atribut yang sama, bau itu menunjukkan keberadaan ABC yang menunggu untuk diekstraksi.

* tanpa masuk ke perdebatan apakah Jawa adalah sistem OO "tradisional" ...


Tambahan : Meskipun kelas dasar abstrak dapat mengesampingkan perilaku isinstancedan issubclass, itu masih tidak memasukkan MRO dari subkelas virtual. Ini adalah jebakan potensial untuk klien: tidak setiap objek yang isinstance(x, MyABC) == Truememiliki metode didefinisikan MyABC.

class MyABC(metaclass=abc.ABCMeta):
    def abc_method(self):
        pass
    @classmethod
    def __subclasshook__(cls, C):
        return True

class C(object):
    pass

# typical client code
c = C()
if isinstance(c, MyABC):  # will be true
    c.abc_method()  # raises AttributeError

Sayangnya ini salah satu dari perangkap "hanya jangan lakukan itu" (yang hanya dimiliki sedikit oleh Python!): Hindari mendefinisikan ABC dengan __subclasshook__metode a dan non-abstrak. Selain itu, Anda harus membuat definisi Anda __subclasshook__konsisten dengan serangkaian metode abstrak yang didefinisikan oleh ABC Anda.


21
"Jika Anda menerapkan antarmuka yang tepat, Anda adalah subclass" Terima kasih banyak untuk itu. Saya tidak tahu apakah Oddthinking melewatkannya, tapi tentu saja saya tahu. FWIW, isinstance(x, collections.Iterable)lebih jelas bagi saya, dan saya tahu Python.
Muhammad Alkarouri

Pos luar biasa. Terima kasih. Saya berpikir bahwa Addendum, "hanya jangan lakukan itu" perangkap, agak seperti melakukan warisan subclass normal tetapi kemudian memiliki Csubclass menghapus (atau mengacaukan perbaikan) yang abc_method()diwarisi dari MyABC. Perbedaan utama adalah bahwa superclass yang mengacaukan kontrak warisan, bukan subclass.
Michael Scott Cuthbert

Apakah Anda tidak perlu melakukan Container.register(ContainAllTheThings)contoh yang diberikan untuk bekerja?
BoZenKhaa

1
@BoZenKhaa Kode dalam jawaban berfungsi! Cobalah! Arti dari __subclasshook__adalah "kelas apa pun yang memenuhi predikat ini dianggap sebagai subclass untuk keperluan isinstancedan issubclasspemeriksaan, terlepas dari apakah itu terdaftar di ABC , dan terlepas dari apakah itu adalah subclass langsung ". Seperti yang saya katakan dalam jawaban, jika Anda mengimplementasikan antarmuka yang tepat, Anda adalah subclass!
Benjamin Hodgson

1
Anda harus mengubah pemformatan dari miring menjadi huruf tebal. "Jika Anda mengimplementasikan antarmuka yang tepat, Anda adalah subclass!" Penjelasan yang sangat singkat. Terima kasih!
marti

108

Fitur praktis dari ABC adalah bahwa jika Anda tidak menerapkan semua metode (dan properti) yang diperlukan, Anda mendapatkan kesalahan saat instantiasi, dan bukannya AttributeError, berpotensi jauh di kemudian hari, ketika Anda benar-benar mencoba menggunakan metode yang hilang.

from abc import ABCMeta, abstractmethod

# python2
class Base(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

# python3
class Base(object, metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`


c = Concrete()
# TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

Contoh dari https://dbader.org/blog/abstract-base-classes-in-python

Edit: untuk memasukkan sintaks python3, terima kasih @PandasRocks


5
Contoh dan tautannya sangat membantu. Terima kasih!
nalyd88

jika Base didefinisikan dalam file terpisah, Anda harus mewarisi darinya sebagai Base.Base atau ubah baris impor menjadi 'from Base import Base'
Ryan Tennill

Perlu dicatat bahwa dalam Python3 sintaks sedikit berbeda. Lihat jawaban ini: stackoverflow.com/questions/28688784/…
PandasRocks

7
Datang dari C # latar belakang, ini adalah yang alasan untuk menggunakan kelas abstrak. Anda menyediakan fungsionalitas, tetapi menyatakan bahwa fungsionalitas memerlukan implementasi lebih lanjut. Jawaban lain sepertinya melewatkan poin ini.
Josh Noe

18

Ini akan membuat menentukan apakah suatu objek mendukung protokol yang diberikan tanpa harus memeriksa keberadaan semua metode dalam protokol atau tanpa memicu pengecualian jauh di dalam wilayah "musuh" karena non-dukungan jauh lebih mudah.


6

Metode abstrak pastikan bahwa apa pun metode yang Anda panggil di kelas induk harus muncul di kelas anak. Di bawah ini adalah cara biasa memanggil dan menggunakan abstrak. Program ini ditulis dalam python3

Cara panggilan biasa

class Parent:
def methodone(self):
    raise NotImplemented()

def methodtwo(self):
    raise NotImplementedError()

class Son(Parent):
   def methodone(self):
       return 'methodone() is called'

c = Son()
c.methodone()

'Methodone () disebut'

c.methodtwo()

NotImplementedError

Dengan metode abstrak

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'

c = Son()

TypeError: Tidak dapat membuat instance kelas abstrak Son dengan metode abstrak methodtwo.

Karena methodtwo tidak dipanggil di kelas anak kami mendapat kesalahan. Implementasi yang tepat di bawah ini

from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def methodone(self):
        raise NotImplementedError()
    @abstractmethod
    def methodtwo(self):
        raise NotImplementedError()

class Son(Parent):
    def methodone(self):
        return 'methodone() is called'
    def methodtwo(self):
        return 'methodtwo() is called'

c = Son()
c.methodone()

'Methodone () disebut'


1
Terima kasih. Bukankah itu poin yang sama dengan jawaban cerberos di atas?
Muhammad Alkarouri
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.