Tại sao nên sử dụng các lớp cơ sở trừu tượng trong Python?


213

Bởi vì tôi đã quen với những cách gõ vịt cũ trong Python, tôi không hiểu được sự cần thiết của ABC (các lớp cơ sở trừu tượng). Sự giúp đỡ là tốt về cách sử dụng chúng.

Tôi đã cố gắng đọc lý do trong PEP , nhưng nó đã đi qua đầu tôi. Nếu tôi đang tìm kiếm một bộ chứa trình tự có thể thay đổi, tôi sẽ kiểm tra __setitem__hoặc nhiều khả năng sẽ thử sử dụng nó ( EAFP ). Tôi đã không bắt gặp một cách sử dụng thực tế cho mô-đun số , sử dụng ABC, nhưng đó là cách gần nhất mà tôi phải hiểu.

Bất cứ ai có thể giải thích lý do cho tôi, xin vui lòng?

Câu trả lời:


162

Phiên bản ngắn

ABC cung cấp một mức độ cao hơn của hợp đồng ngữ nghĩa giữa các khách hàng và các lớp được thực hiện.

Phiên bản dài

Có một hợp đồng giữa một lớp và người gọi nó. Lớp học hứa sẽ làm những việc nhất định và có những tính chất nhất định.

Có nhiều cấp độ khác nhau trong hợp đồng.

Ở mức rất thấp, hợp đồng có thể bao gồm tên của một phương thức hoặc số lượng tham số của nó.

Trong một ngôn ngữ gõ tĩnh, hợp đồng đó thực sự sẽ được thực thi bởi trình biên dịch. Trong Python, bạn có thể sử dụng EAFP hoặc nhập nội quan để xác nhận rằng đối tượng chưa biết đáp ứng hợp đồng dự kiến ​​này.

Nhưng cũng có những lời hứa cấp cao hơn, ngữ nghĩa trong hợp đồng.

Ví dụ, nếu có một __str__()phương thức, dự kiến ​​sẽ trả về một chuỗi đại diện của đối tượng. Nó có thể xóa tất cả nội dung của đối tượng, thực hiện giao dịch và nhổ một trang trống ra khỏi máy in ... nhưng có một sự hiểu biết chung về những gì nó nên làm, được mô tả trong hướng dẫn Python.

Đó là một trường hợp đặc biệt, trong đó hợp đồng ngữ nghĩa được mô tả trong hướng dẫn. Cần gì print()phương pháp làm gì? Nó nên ghi đối tượng vào một máy in hoặc một dòng lên màn hình, hoặc cái gì khác? Nó phụ thuộc - bạn cần đọc các ý kiến ​​để hiểu hợp đồng đầy đủ ở đây. Một đoạn mã khách hàng chỉ đơn giản kiểm tra xem print()phương thức tồn tại đã xác nhận một phần của hợp đồng - rằng một cuộc gọi phương thức có thể được thực hiện, nhưng không có thỏa thuận về ngữ nghĩa cấp cao hơn của cuộc gọi.

Xác định một lớp cơ sở trừu tượng (ABC) là một cách tạo ra hợp đồng giữa những người thực hiện lớp và người gọi. Nó không chỉ là một danh sách các tên phương thức, mà là sự hiểu biết chung về những gì các phương thức đó nên làm. Nếu bạn thừa hưởng từ ABC này, bạn hứa sẽ tuân theo tất cả các quy tắc được mô tả trong các nhận xét, bao gồm cả ngữ nghĩa của print()phương pháp.

Gõ vịt của Python có nhiều ưu điểm về tính linh hoạt so với gõ tĩnh, nhưng nó không giải quyết được tất cả các vấn đề. ABCs cung cấp một giải pháp trung gian giữa dạng Python tự do và sự ràng buộc và kỷ luật của một ngôn ngữ được gõ tĩnh.


12
Tôi nghĩ rằng bạn có một điểm ở đó, nhưng tôi không thể theo bạn. Vì vậy, sự khác biệt, về mặt hợp đồng, giữa một lớp thực hiện __contains__và một lớp kế thừa là collections.Containergì? Trong ví dụ của bạn, trong Python luôn có sự hiểu biết chung __str__. Việc thực hiện __str__đưa ra những lời hứa giống như kế thừa từ một số ABC và sau đó thực hiện __str__. Trong cả hai trường hợp, bạn có thể phá vỡ hợp đồng; không có ngữ nghĩa có thể chứng minh được như những gì chúng ta có trong gõ tĩnh.
Muhammad Alkarouri

15
collections.Containerlà một trường hợp suy biến, chỉ bao gồm \_\_contains\_\_và chỉ có nghĩa là quy ước được xác định trước. Sử dụng ABC không tự thêm nhiều giá trị, tôi đồng ý. Tôi nghi ngờ nó đã được thêm vào để cho phép (ví dụ) Setkế thừa từ nó. Vào thời điểm bạn đến Set, đột nhiên thuộc về ABC có ngữ nghĩa đáng kể. Một mặt hàng không thể thuộc về bộ sưu tập hai lần. Điều đó KHÔNG thể được phát hiện bởi sự tồn tại của các phương thức.
Oddthinking ngày

3
Vâng, tôi nghĩ Setlà một ví dụ tốt hơn print(). Tôi đã cố gắng tìm một tên phương thức có ý nghĩa mơ hồ và không thể bị nhầm lẫn bởi tên đó, vì vậy bạn không thể chắc chắn rằng nó sẽ thực hiện đúng theo tên của nó và hướng dẫn sử dụng Python.
Oddthinking ngày

3
Bất kỳ cơ hội để viết lại câu trả lời với Setví dụ thay vì print? Setrất có ý nghĩa, @Oddthinking.
Ehtesh Choudhury

1
Tôi nghĩ rằng bài viết này giải thích nó rất tốt: dbader.org/blog/abab-base-groupes-in-python
szabgab

228

@ Câu trả lời Oddthinking là không sai, nhưng tôi nghĩ rằng nó bỏ lỡ thực , thực tế nguyên nhân Python có kiến thức cơ bản trong một thế giới của vịt-gõ.

Các phương pháp trừu tượng là gọn gàng, nhưng theo tôi, chúng không thực sự lấp đầy bất kỳ trường hợp sử dụng nào chưa được bao phủ bởi cách gõ vịt. Sức mạnh thực sự của các lớp cơ sở trừu tượng nằm ở cách chúng cho phép bạn tùy chỉnh hành vi của isinstanceissubclass . ( __subclasshook__về cơ bản là một API thân thiện hơn so với Python __instancecheck____subclasscheck__ hook.) Việc điều chỉnh các cấu trúc dựng sẵn để hoạt động trên các loại tùy chỉnh là một phần rất lớn trong triết lý của Python.

Mã nguồn của Python là mẫu mực. Đây là cách collections.Containerđịnh nghĩa trong thư viện chuẩn (tại thời điểm viết):

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

Định nghĩa này __subclasshook__nói rằng bất kỳ lớp nào có __contains__thuộc tính đều được coi là một lớp con của Container, ngay cả khi nó không phân lớp trực tiếp. Vì vậy, tôi có thể viết này:

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

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

Nói cách khác, nếu bạn thực hiện đúng giao diện, bạn là một lớp con! ABC cung cấp một cách chính thức để xác định các giao diện trong Python, trong khi vẫn đúng với tinh thần gõ vịt. Bên cạnh đó, điều này hoạt động theo cách tôn vinh Nguyên tắc đóng mở .

Mô hình đối tượng của Python trông bề ngoài tương tự như hệ thống OO "truyền thống" hơn (ý tôi là Java *) - chúng ta có các lớp yer, đối tượng yer, phương thức yer - nhưng khi bạn nhìn vào bề mặt, bạn sẽ thấy thứ gì đó phong phú hơn và Linh hoạt hơn. Tương tự như vậy, khái niệm của Python về các lớp cơ sở trừu tượng có thể được nhà phát triển Java nhận ra, nhưng trong thực tế, chúng được dành cho một mục đích rất khác.

Đôi khi tôi thấy mình viết các hàm đa hình có thể hoạt động trên một vật phẩm hoặc một bộ sưu tập các vật phẩm và tôi thấy isinstance(x, collections.Iterable)nó dễ đọc hơn nhiều so với hasattr(x, '__iter__')hoặc một try...exceptkhối tương đương . (Nếu bạn không biết Python, cái nào trong ba cái đó sẽ làm cho ý định của mã rõ ràng nhất?)

Điều đó nói rằng, tôi thấy rằng tôi hiếm khi cần phải viết ABC của riêng mình và tôi thường phát hiện ra sự cần thiết thông qua tái cấu trúc. Nếu tôi thấy một hàm đa hình thực hiện nhiều kiểm tra thuộc tính hoặc nhiều hàm thực hiện kiểm tra thuộc tính tương tự, thì mùi đó cho thấy sự tồn tại của ABC đang chờ được trích xuất.

* mà không cần phải tranh luận về việc liệu Java có phải là một hệ thống OO "truyền thống" không ...


Phụ lục : Mặc dù một lớp cơ sở trừu tượng có thể ghi đè hành vi của isinstanceissubclass, nhưng nó vẫn không nhập MRO của lớp con ảo. Đây là một cạm bẫy tiềm tàng cho khách hàng: không phải mọi đối tượng isinstance(x, MyABC) == Truecó các phương thức được xác định trên 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

Thật không may, đây là một trong những cái bẫy "chỉ không làm như vậy" (trong đó Python có khá ít!): Tránh xác định ABC bằng cả hai __subclasshook__phương thức và không trừu tượng. Hơn nữa, bạn nên làm cho định nghĩa của bạn __subclasshook__phù hợp với tập hợp các phương thức trừu tượng mà ABC định nghĩa.


21
"Nếu bạn triển khai đúng giao diện, bạn là một lớp con" Cảm ơn rất nhiều vì điều đó. Tôi không biết nếu Oddthinking bỏ lỡ nó, nhưng tôi chắc chắn đã làm. FWIW, isinstance(x, collections.Iterable)rõ ràng hơn đối với tôi và tôi biết Python.
Muhammad Alkarouri

Tuyệt vời bài. Cảm ơn bạn. Tôi nghĩ rằng cái bổ sung, "đừng làm vậy", giống như thực hiện kế thừa lớp con bình thường nhưng sau đó Cxóa lớp con (hoặc vặn vít ra ngoài sửa chữa) abc_method()được thừa kế từ đó MyABC. Sự khác biệt chính là đó là siêu lớp đang làm hỏng hợp đồng thừa kế, không phải là lớp con.
Michael Scott Cuthbert

Bạn sẽ không phải làm Container.register(ContainAllTheThings)cho ví dụ đã cho để làm việc?
BoZenKhaa

1
@BoZenKhaa Mã trong câu trả lời hoạt động! Thử nó! Ý nghĩa của __subclasshook__"bất kỳ lớp nào thỏa mãn vị từ này được coi là một lớp con cho mục đích isinstanceissubclasskiểm tra, bất kể nó có được đăng ký với ABC hay không, bất kể đó là lớp con trực tiếp ". Như tôi đã nói trong câu trả lời, nếu bạn thực hiện đúng giao diện, bạn là một lớp con!
Benjamin Hodgson

1
Bạn nên thay đổi định dạng từ chữ nghiêng sang đậm. "nếu bạn triển khai đúng giao diện, bạn là một lớp con!" Giải thích rất súc tích. Cảm ơn bạn!
marti

108

Một tính năng hữu ích của ABC là nếu bạn không thực hiện tất cả các phương thức (và thuộc tính) cần thiết, bạn sẽ gặp lỗi khi khởi tạo, thay vì AttributeError, có khả năng muộn hơn nhiều, khi bạn thực sự cố gắng sử dụng phương thức bị thiếu.

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"

Ví dụ từ https://dbader.org/blog/abauge-base-groupes-in-python

Chỉnh sửa: để bao gồm cú pháp python3, cảm ơn @PandasRocks


5
Ví dụ và liên kết đều rất hữu ích. Cảm ơn!
nalyd88

nếu Base được xác định trong một tệp riêng biệt, bạn phải kế thừa từ nó dưới dạng Base.Base hoặc thay đổi dòng nhập thành 'từ Base nhập Base'
Ryan Tennill

Cần lưu ý rằng trong Python3 cú pháp hơi khác nhau. Xem câu trả lời này: stackoverflow.com/questions/28688784/ từ
PandasRocks

7
Xuất thân từ một nền C #, đây là những lý do để sử dụng lớp trừu tượng. Bạn đang cung cấp chức năng, nhưng nói rằng chức năng đó yêu cầu triển khai thêm. Các câu trả lời khác dường như bỏ lỡ điểm này.
Josh Noe

18

Nó sẽ giúp xác định xem một đối tượng có hỗ trợ một giao thức nhất định mà không phải kiểm tra sự hiện diện của tất cả các phương thức trong giao thức hay không mà không gây ra ngoại lệ sâu trong lãnh thổ của "kẻ thù" do không hỗ trợ dễ dàng hơn nhiều.


6

Phương thức trừu tượng đảm bảo rằng những gì bạn đang gọi trong lớp cha phải xuất hiện trong lớp con. Dưới đây là cách gọi noraml và sử dụng trừu tượng. Chương trình viết bằng python3

Cách gọi thông thường

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 gọi là'

c.methodtwo()

Chưa thực hiệnError

Với phương pháp Trừu tượng

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: Không thể khởi tạo lớp trừu tượng Son bằng các phương thức trừu tượng phương thức.

Vì phương thức này không được gọi trong lớp con nên chúng tôi gặp lỗi. Việc thực hiện đúng dưới đây

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 () được gọi là'


1
Cảm ơn. Không phải đó là điểm giống nhau trong câu trả lời của cerberos ở trên sao?
Muhammad Alkarouri
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.