เหตุใดจึงต้องใช้ Abstract Base Classes ใน Python


213

เนื่องจากฉันเคยชินกับการพิมพ์เป็ดแบบเก่าใน Python ฉันจึงไม่เข้าใจความต้องการ ABC (คลาสฐานนามธรรม) ความช่วยเหลือดีในการใช้งาน

ฉันพยายามอ่านเหตุผลในPEPแต่มันไปทั่วหัวของฉัน หากฉันกำลังมองหาคอนเทนเนอร์ลำดับที่ไม่แน่นอนฉันจะตรวจสอบ__setitem__หรือลองใช้ ( EAFP ) มากกว่า ฉันยังไม่เจอการใช้งานจริงสำหรับโมดูลตัวเลขซึ่งใช้ ABCs แต่นั่นเป็นสิ่งที่ใกล้เคียงที่สุดที่ฉันต้องเข้าใจ

ใครช่วยอธิบายเหตุผลให้ฉันหน่อยได้ไหม

คำตอบ:


162

เวอร์ชั่นสั้น

ABCs เสนอระดับความหมายที่สูงขึ้นของสัญญาระหว่างลูกค้าและชั้นเรียนที่ดำเนินการ

รุ่นยาว

มีสัญญาระหว่างชั้นเรียนกับผู้โทร ชั้นสัญญาว่าจะทำบางสิ่งและมีคุณสมบัติบางอย่าง

สัญญามีหลายระดับ

ในระดับที่ต่ำมากสัญญาอาจรวมชื่อของวิธีการหรือจำนวนพารามิเตอร์

ในภาษาที่พิมพ์แบบคงที่สัญญานั้นจะถูกบังคับใช้โดยคอมไพเลอร์ ใน Python คุณสามารถใช้EAFPหรือพิมพ์วิปัสสนาเพื่อยืนยันว่าวัตถุที่ไม่รู้จักตรงกับสัญญาที่คาดไว้นี้

แต่ก็มีสัญญาระดับความหมายที่สูงกว่าในสัญญา

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

นั่นเป็นกรณีพิเศษที่สัญญาความหมายถูกอธิบายไว้ในคู่มือ print()วิธีการนี้ควรทำอย่างไร? มันควรจะเขียนวัตถุไปยังเครื่องพิมพ์หรือสายไปที่หน้าจอหรืออย่างอื่น? ขึ้นอยู่กับ - คุณต้องอ่านความคิดเห็นเพื่อทำความเข้าใจสัญญาฉบับเต็มที่นี่ รหัสลูกค้าที่ตรวจสอบว่าprint()วิธีการนั้นมีอยู่แล้วได้ยืนยันส่วนหนึ่งของสัญญา - ว่าสามารถทำการเรียกใช้เมธอดได้ แต่ไม่ใช่ว่ามีข้อตกลงเกี่ยวกับความหมายระดับสูงของการโทร

การกำหนด Abstract Base Class (ABC) เป็นวิธีการสร้างสัญญาระหว่างผู้ใช้งานในคลาสและผู้โทร มันไม่ได้เป็นเพียงรายการของชื่อเมธอด แต่เป็นความเข้าใจร่วมกันว่าวิธีการเหล่านั้นควรทำอย่างไร หากคุณได้รับมรดกจาก ABC นี้คุณสัญญาว่าจะปฏิบัติตามกฎทั้งหมดที่อธิบายไว้ในความคิดเห็นรวมถึงความหมายของprint()วิธีการ

การพิมพ์เป็ด Python มีข้อดีหลายประการในด้านความยืดหยุ่นมากกว่าการพิมพ์แบบสแตติก แต่ก็ไม่สามารถแก้ปัญหาทั้งหมดได้ ABCs นำเสนอวิธีแก้ปัญหาระดับกลางระหว่าง Python ที่มีรูปแบบอิสระกับความเป็นทาสและมีระเบียบวินัยของภาษาที่พิมพ์แบบคงที่


12
ฉันคิดว่าคุณมีประเด็นอยู่ที่นั่น แต่ฉันไม่สามารถติดตามคุณได้ ดังนั้นในแง่ของสัญญาระหว่างความแตกต่างระหว่างคลาสที่ใช้__contains__กับคลาสที่สืบทอดมาcollections.Containerคืออะไร? __str__ในตัวอย่างของคุณในหลามมีเสมอความเข้าใจร่วมกัน การดำเนินการทำสัญญาเช่นเดียวกับการสืบทอดจากบางส่วนเอบีซีและจากนั้นการดำเนินการ__str__ __str__ในทั้งสองกรณีคุณสามารถทำลายสัญญาได้ ไม่มีความหมายที่พิสูจน์ได้เช่นสิ่งที่เรามีในการพิมพ์แบบคงที่
Muhammad Alkarouri

15
collections.Containerเป็นกรณีที่เลวลงซึ่งรวม\_\_contains\_\_เฉพาะและหมายถึงอนุสัญญาที่กำหนดไว้ล่วงหน้าเท่านั้น การใช้ ABC ไม่ได้เพิ่มมูลค่ามาก ๆ ด้วยตัวเองฉันเห็นด้วย ฉันสงสัยว่ามันถูกเพิ่มเข้ามาเพื่อให้ (เช่น) Setได้รับมรดกจากมัน เมื่อถึงเวลาที่คุณSetเป็นเจ้าของ ABC ก็มีความหมายมากมาย ไอเท็มไม่สามารถเป็นของคอลเล็กชันได้สองครั้ง ที่ไม่สามารถตรวจพบได้โดยการมีอยู่ของวิธีการ
Oddthinking

3
ใช่ผมคิดว่าเป็นตัวอย่างที่ดีกว่าSet print()ฉันพยายามค้นหาชื่อเมธอดที่มีความหมายคลุมเครือและไม่สามารถถูกจับโดยชื่อเพียงอย่างเดียวดังนั้นคุณไม่แน่ใจว่ามันจะทำสิ่งที่ถูกต้องโดยใช้ชื่อและคู่มือ Python
คิดมาก

3
มีโอกาสที่จะเขียนคำตอบด้วยSetตัวอย่างแทนprintหรือไม่? Setทำให้รู้สึกมาก @Oddthinking
Ehtesh Choudhury

1
ฉันคิดว่าบทความนี้อธิบายได้ดีมาก: dbader.org/blog/abstract-base-classes-in-python
szabgab

228

@ คำตอบของ Oddthinking ไม่ผิด แต่ฉันคิดว่ามันพลาด จริง , การปฏิบัติเหตุผลหลามมี ABCs ในโลกของเป็ดพิมพ์

วิธีการที่เป็นนามธรรมมีความเรียบร้อย แต่ในความคิดของฉันพวกเขาไม่ได้ใช้กรณีที่ไม่ได้กล่าวถึงในการพิมพ์เป็ด พลังที่แท้จริงของคลาสฐานบทคัดย่ออยู่ในวิธีที่พวกเขาอนุญาตให้คุณปรับแต่งพฤติกรรมของisinstanceissubclassและ ( __subclasshook__โดยทั่วไปคือ API ที่เป็นมิตรกว่าบน Python __instancecheck__และ__subclasscheck__ hooks) การปรับโครงสร้างภายในเพื่อให้ทำงานกับประเภทที่กำหนดเองเป็นส่วนหนึ่งของปรัชญาของ Python

รหัสที่มาของไพ ธ อนเป็นแบบอย่าง นี่คือวิธีที่collections.Containerกำหนดไว้ในไลบรารีมาตรฐาน (ณ เวลาที่เขียน):

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

คำจำกัดความของ __subclasshook__บอกว่าคลาสใด ๆ ที่มีแอ__contains__ททริบิวต์ถือเป็นคลาสย่อยของคอนเทนเนอร์แม้ว่ามันจะไม่ได้คลาสย่อยโดยตรงก็ตาม ดังนั้นฉันสามารถเขียนสิ่งนี้:

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

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

ในคำอื่น ๆ ถ้าคุณใช้อินเทอร์เฟซที่ถูกต้องคุณเป็นคลาสย่อย! ABCs ให้วิธีการอย่างเป็นทางการในการกำหนดส่วนต่อประสานใน Python ในขณะที่ยังคงยึดมั่นในจิตวิญญาณของการพิมพ์แบบเป็ด นอกจากนี้การทำงานในลักษณะที่เกียรตินิยมเปิดปิดหลักการ

แบบจำลองวัตถุของ Python มีลักษณะคล้ายกับระบบ OO แบบ "ดั้งเดิม" มากขึ้น (ซึ่งฉันหมายถึง Java *) - เรามีคลาส yer วัตถุ yer วิธี yer - แต่เมื่อคุณเกาพื้นผิวคุณจะพบบางสิ่งที่สมบูรณ์ยิ่งขึ้นและ ยืดหยุ่นมากขึ้น ในทำนองเดียวกันแนวคิดของ Python เกี่ยวกับคลาสพื้นฐานที่เป็นนามธรรมอาจเป็นที่รู้จักสำหรับนักพัฒนา Java แต่ในทางปฏิบัติพวกเขามีจุดประสงค์เพื่อจุดประสงค์ที่แตกต่างกันมาก

บางครั้งฉันพบว่าตัวเองกำลังเขียนฟังก์ชั่น polymorphic ที่สามารถใช้กับสิ่งของชิ้นเดียวหรือของสะสมได้และฉันก็พบว่า isinstance(x, collections.Iterable)สามารถอ่านได้มากกว่าhasattr(x, '__iter__')หรือtry...exceptบล็อกที่เทียบเท่า (ถ้าคุณไม่รู้จัก Python สามในนั้นที่จะทำให้รหัสชัดเจนที่สุด)

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

* โดยไม่มีการอภิปรายว่า Java เป็นระบบ OO แบบ "ดั้งเดิม" หรือไม่ ...


ภาคผนวก : แม้ว่าชั้นฐานนามธรรมสามารถแทนที่ลักษณะการทำงานของisinstanceและissubclassก็ยังไม่เข้าสู่MROของ subclass เสมือน นี่คืออันตรายที่อาจเกิดขึ้นสำหรับลูกค้า: ไม่วัตถุที่ทุกคนมีวิธีการที่กำหนดไว้ในisinstance(x, MyABC) == TrueMyABC

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

น่าเสียดายที่หนึ่งในนั้น "อย่าทำแบบนั้น" กับดัก (ซึ่ง Python มีจำนวนไม่มาก!): หลีกเลี่ยงการกำหนด ABC ด้วยวิธีการทั้งแบบที่__subclasshook__ไม่ใช่นามธรรม ยิ่งกว่านั้นคุณควรทำให้คำจำกัดความของคุณ__subclasshook__สอดคล้องกับชุดของวิธีนามธรรมที่ ABC ของคุณกำหนด


21
"ถ้าคุณใช้อินเทอร์เฟซที่ถูกต้องคุณเป็นคลาสย่อย" ขอบคุณมากสำหรับสิ่งนั้น ฉันไม่รู้ว่า Oddthinking คิดถึงหรือไม่ แต่ฉันก็ทำอย่างนั้น FWIW isinstance(x, collections.Iterable)ชัดเจนสำหรับฉันและฉันรู้ Python
Muhammad Alkarouri

โพสต์ที่ยอดเยี่ยม ขอบคุณ. ผมคิดว่าภาคผนวก "ก็ไม่ได้ทำอย่างนั้น" กับดักเป็นบิตเช่นทำมรดก subclass ปกติ แต่แล้วมีCsubclass ลบ (หรือสกรูขึ้นเกินกว่าจะซ่อม) เดอะสืบทอดมาจากabc_method() MyABCข้อแตกต่างที่สำคัญคือมันเป็นซูเปอร์คลาสที่ไขสัญญาการสืบทอดไม่ใช่คลาสย่อย
Michael Scott Cuthbert

คุณไม่ต้องทำContainer.register(ContainAllTheThings)เพื่อให้ตัวอย่างที่ให้มาทำงาน?
BoZenKhaa

1
@BoZenKhaa รหัสในคำตอบใช้งานได้! ลองมัน! ความหมายของ__subclasshook__"คลาสใดที่ตรงกับคำกริยานี้ถือว่าเป็นคลาสย่อยสำหรับวัตถุประสงค์isinstanceและการissubclassตรวจสอบไม่ว่าจะลงทะเบียนกับ ABCหรือไม่และไม่ว่ามันจะเป็นคลาสย่อยโดยตรงหรือไม่ " อย่างที่ฉันพูดในคำตอบถ้าคุณใช้อินเทอร์เฟซที่ถูกต้องคุณเป็นคลาสย่อย!
Benjamin Hodgson

1
คุณควรเปลี่ยนการจัดรูปแบบจากตัวเอียงเป็นตัวหนา "ถ้าคุณใช้อินเทอร์เฟซที่ถูกต้องคุณเป็นคลาสย่อย!" คำอธิบายที่กระชับมาก ขอบคุณ!
marti

108

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

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"

ตัวอย่างจากhttps://dbader.org/blog/abstract-base-classes-in-python

แก้ไข: เพื่อรวมไวยากรณ์ของ python3 ขอบคุณ @PandasRocks


5
ตัวอย่างและลิงค์มีประโยชน์มาก ขอบคุณ!
nalyd88

ถ้า Base ถูกกำหนดในไฟล์แยกต่างหากคุณจะต้องสืบทอดจากมันเป็น Base.Base หรือเปลี่ยน line import เป็น 'from Base import base'
Ryan Tennill

ควรสังเกตว่าใน Python3 ไวยากรณ์จะแตกต่างกันเล็กน้อย ดูคำตอบนี้: stackoverflow.com/questions/28688784/…
PandasRocks

7
มาจากพื้นหลัง C # นี้เป็นเหตุผลที่จะใช้คลาสนามธรรม คุณกำลังจัดหาฟังก์ชันการทำงาน แต่ระบุว่าฟังก์ชันนั้นต้องการการใช้งานเพิ่มเติม คำตอบอื่น ๆ ดูเหมือนจะพลาดจุดนี้
Josh Noe

18

มันจะทำการพิจารณาว่าวัตถุรองรับโพรโทคอลที่กำหนดโดยไม่ต้องตรวจสอบว่ามีวิธีการทั้งหมดในโพรโทคอลหรือไม่


6

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

วิธีการโทรปกติ

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 () เรียกว่า'

c.methodtwo()

NotImplementedError

ด้วยวิธีการบทคัดย่อ

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: ไม่สามารถสร้างอินสแตนซ์คลาสคลาส Son ด้วยเมธอด abstract เมธอดสอง

เนื่องจาก methodTwo ไม่ถูกเรียกใช้ในคลาสย่อยเราจึงพบข้อผิดพลาด การใช้งานที่เหมาะสมอยู่ด้านล่าง

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 () เรียกว่า'


1
ขอบคุณ นั่นคือจุดเดียวกันที่เกิดขึ้นในคำตอบของ cerberos ใช่หรือไม่?
Muhammad Alkarouri
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.