Mục đích của các lớp bên trong của python là gì?


98

Các lớp bên trong / lồng nhau của Python khiến tôi bối rối. Có điều gì không thể hoàn thành nếu không có chúng? Nếu vậy, đó là thứ gì?

Câu trả lời:


85

Trích dẫn từ http://www.geekinterview.com/question_details/64739 :

Ưu điểm của lớp bên trong:

  • Nhóm các lớp một cách hợp lý : Nếu một lớp chỉ hữu ích cho một lớp khác thì sẽ hợp lý để nhúng nó vào lớp đó và giữ hai lớp lại với nhau. Việc lồng các "lớp trợ giúp" như vậy làm cho gói của chúng được sắp xếp hợp lý hơn.
  • Tăng tính đóng gói : Xem xét hai lớp cấp cao nhất A và B trong đó B cần truy cập vào các thành viên của A mà nếu không sẽ được khai báo là riêng tư. Bằng cách ẩn lớp B trong lớp AA, các thành viên của AA có thể được khai báo là riêng tư và B có thể truy cập chúng. Ngoài ra bản thân B có thể bị ẩn khỏi thế giới bên ngoài.
  • Mã dễ đọc hơn, dễ bảo trì hơn : Việc lồng các lớp nhỏ trong các lớp cấp cao nhất sẽ đặt mã gần hơn với nơi nó được sử dụng.

Ưu điểm chính là tổ chức. Bất cứ điều gì có thể được hoàn thành với các lớp bên trong đều có thể hoàn thành mà không cần chúng.


50
Đối số đóng gói tất nhiên không áp dụng cho Python.
bobince

30
Điểm đầu tiên cũng không áp dụng cho Python. Bạn có thể xác định bao nhiêu lớp trong một tệp mô-đun tùy thích, do đó giữ chúng cùng nhau và tổ chức gói cũng không bị ảnh hưởng. Điểm cuối cùng là rất chủ quan và tôi không tin rằng nó có giá trị. Tóm lại, tôi không tìm thấy bất kỳ đối số nào ủng hộ việc sử dụng các lớp bên trong bằng Python trong câu trả lời này.
Chris Arndt

17
Tuy nhiên, đây là những lý do mà các lớp bên trong được sử dụng trong lập trình. Bạn chỉ đang cố gắng bắn ra một câu trả lời cạnh tranh. Câu trả lời mà anh chàng này đưa ra ở đây là chắc chắn.
Inversus

16
@Inversus: Tôi không đồng ý. Đây không phải là một câu trả lời, nó là một trích dẫn mở rộng từ câu trả lời của người khác về một ngôn ngữ khác (cụ thể là Java). Bị phản đối và tôi hy vọng những người khác cũng làm như vậy.
Kevin

4
Tôi đồng ý với câu trả lời này và không đồng ý với những phản đối. Mặc dù các lớp lồng nhau không phải là các lớp bên trong của Java nhưng chúng rất hữu ích. Mục đích của một lớp lồng nhau là tổ chức. Thực tế, bạn đang đặt một lớp dưới không gian tên của lớp khác. Khi làm như vậy hợp lý, đây Pythonic: "Không gian tên là một ý tưởng tuyệt vời - hãy làm nhiều hơn nữa!". Ví dụ, hãy xem xét một DataLoaderlớp có thể ném một CacheMissngoại lệ. Lồng ngoại lệ dưới lớp chính DataLoader.CacheMisscó nghĩa là bạn có thể chỉ nhập DataLoadernhưng vẫn sử dụng ngoại lệ.
cbarrick

50

Có điều gì không thể hoàn thành nếu không có chúng?

Không. Chúng hoàn toàn tương đương với việc xác định lớp thông thường ở cấp cao nhất, và sau đó sao chép một tham chiếu đến nó vào lớp ngoài.

Tôi không nghĩ rằng có bất kỳ lý do đặc biệt nào mà các lớp lồng nhau được 'cho phép', ngoại trừ việc 'không cho phép' chúng một cách rõ ràng cũng không có ý nghĩa gì.

Nếu bạn đang tìm kiếm một lớp tồn tại trong vòng đời của đối tượng bên ngoài / chủ sở hữu và luôn có tham chiếu đến một thể hiện của lớp bên ngoài - các lớp bên trong như Java thực hiện - thì các lớp lồng nhau của Python không phải là điều đó. Nhưng bạn có thể hack thứ gì đó tương tự như vậy:

import weakref, new

class innerclass(object):
    """Descriptor for making inner classes.

    Adds a property 'owner' to the inner class, pointing to the outer
    owner instance.
    """

    # Use a weakref dict to memoise previous results so that
    # instance.Inner() always returns the same inner classobj.
    #
    def __init__(self, inner):
        self.inner= inner
        self.instances= weakref.WeakKeyDictionary()

    # Not thread-safe - consider adding a lock.
    #
    def __get__(self, instance, _):
        if instance is None:
            return self.inner
        if instance not in self.instances:
            self.instances[instance]= new.classobj(
                self.inner.__name__, (self.inner,), {'owner': instance}
            )
        return self.instances[instance]


# Using an inner class
#
class Outer(object):
    @innerclass
    class Inner(object):
        def __repr__(self):
            return '<%s.%s inner object of %r>' % (
                self.owner.__class__.__name__,
                self.__class__.__name__,
                self.owner
            )

>>> o1= Outer()
>>> o2= Outer()
>>> i1= o1.Inner()
>>> i1
<Outer.Inner inner object of <__main__.Outer object at 0x7fb2cd62de90>>
>>> isinstance(i1, Outer.Inner)
True
>>> isinstance(i1, o1.Inner)
True
>>> isinstance(i1, o2.Inner)
False

(Điều này sử dụng trình trang trí lớp, mới trong Python 2.6 và 3.0. Nếu không, bạn phải nói “Inner = lớp bên trong (Bên trong)” sau định nghĩa lớp.)


5
Các trường hợp sử dụng yêu cầu điều đó (tức là các lớp bên trong Java-esque, các trường hợp của chúng có mối quan hệ với các trường hợp của lớp bên ngoài) thường có thể được giải quyết bằng Python bằng cách xác định các phương thức bên trong lớp bên trong của lớp bên ngoài - họ sẽ thấy của bên ngoài selfmà không cần bất kỳ công việc bổ sung nào (chỉ cần sử dụng một số nhận dạng khác mà bạn thường đặt bên trong self; giống như innerself), và sẽ có thể truy cập phiên bản bên ngoài thông qua đó.
Evgeni Sergeev

Sử dụng một WeakKeyDictionarytrong ví dụ này không thực sự cho phép các khóa được thu thập rác, vì các giá trị tham chiếu mạnh mẽ đến các khóa tương ứng của chúng thông qua ownerthuộc tính của chúng .
Kritzefitz

36

Có điều gì đó bạn cần phải xoay quanh để có thể hiểu được điều này. Trong hầu hết các ngôn ngữ, định nghĩa lớp là chỉ thị cho trình biên dịch. Đó là, lớp được tạo trước khi chương trình được chạy. Trong python, tất cả các câu lệnh đều có thể thực thi được. Điều đó có nghĩa là tuyên bố này:

class foo(object):
    pass

là một câu lệnh được thực thi trong thời gian chạy giống như câu lệnh sau:

x = y + z

Điều này có nghĩa là bạn không chỉ có thể tạo các lớp trong các lớp khác, bạn có thể tạo các lớp ở bất cứ đâu bạn muốn. Hãy xem xét mã này:

def foo():
    class bar(object):
        ...
    z = bar()

Do đó, ý tưởng về một "lớp bên trong" không thực sự là một cấu trúc ngôn ngữ; nó là một cấu trúc lập trình viên. Guido có một bản tóm tắt rất tốt về cách điều này xảy ra ở đây . Nhưng về cơ bản, ý tưởng cơ bản là điều này đơn giản hóa ngữ pháp của ngôn ngữ.


16

Lồng các lớp trong các lớp:

  • Các lớp lồng nhau làm phình to định nghĩa lớp khiến cho việc xem điều gì đang diễn ra trở nên khó khăn hơn.

  • Các lớp lồng nhau có thể tạo ra sự ghép nối khiến việc kiểm tra khó khăn hơn.

  • Trong Python, bạn có thể đặt nhiều hơn một lớp trong một tệp / mô-đun, không giống như Java, vì vậy lớp này vẫn gần với lớp cấp cao nhất và thậm chí có thể đặt tên lớp trước bằng "_" để giúp biểu thị rằng những lớp khác không nên. sử dụng nó.

Nơi mà các lớp lồng nhau có thể tỏ ra hữu ích là trong các hàm

def some_func(a, b, c):
   class SomeClass(a):
      def some_method(self):
         return b
   SomeClass.__doc__ = c
   return SomeClass

Lớp nắm bắt các giá trị từ hàm cho phép bạn tạo động một lớp như lập trình siêu mẫu trong C ++


7

Tôi hiểu các đối số chống lại các lớp lồng nhau, nhưng có một trường hợp sử dụng chúng trong một số trường hợp. Hãy tưởng tượng tôi đang tạo một lớp danh sách được liên kết kép và tôi cần tạo một lớp nút để bảo trì các nút. Tôi có hai lựa chọn, tạo lớp Node bên trong lớp DoublyLinkedList hoặc tạo lớp Node bên ngoài lớp DoublyLinkedList. Tôi thích lựa chọn đầu tiên hơn trong trường hợp này, vì lớp Node chỉ có ý nghĩa bên trong lớp DoublyLinkedList. Mặc dù không có lợi ích ẩn / đóng gói, nhưng có một lợi ích nhóm khi có thể nói lớp Node là một phần của lớp DoublyLinkedList.


5
Điều này đúng giả sử rằng cùng một Nodelớp không hữu ích cho các loại lớp danh sách liên kết khác mà bạn cũng có thể tạo, trong trường hợp đó, nó có thể chỉ nằm bên ngoài.
Acumenus

Một cách khác để đặt nó: Nodenằm dưới không gian tên của DoublyLinkedList, và nó có ý nghĩa hợp lý là như vậy. Đây Pythonic: "Không gian tên là một trong những ý tưởng tuyệt vời - hãy làm nhiều hơn nữa!"
cbarrick

@cbarrick: Làm "nhiều hơn trong số đó" không nói gì về việc lồng chúng.
Ethan Furman

3

Có điều gì không thể hoàn thành nếu không có chúng? Nếu vậy, đó là thứ gì?

Có một thứ không thể dễ dàng thực hiện được nếu không có : kế thừa các lớp liên quan .

Đây là một ví dụ tối giản với các lớp liên quan AB:

class A(object):
    class B(object):
        def __init__(self, parent):
            self.parent = parent

    def make_B(self):
        return self.B(self)


class AA(A):  # Inheritance
    class B(A.B):  # Inheritance, same class name
        pass

Mã này dẫn đến một hành vi khá hợp lý và có thể dự đoán được:

>>> type(A().make_B())
<class '__main__.A.B'>
>>> type(A().make_B().parent)
<class '__main__.A'>
>>> type(AA().make_B())
<class '__main__.AA.B'>
>>> type(AA().make_B().parent)
<class '__main__.AA'>

Nếu Blà một lớp cấp cao nhất, bạn không thể viết self.B()trong phương thức make_Bmà chỉ cần viết B(), và do đó mất liên kết động với các lớp thích hợp.

Lưu ý rằng trong cách xây dựng này, bạn không bao giờ được tham chiếu đến lớp Atrong phần thân của lớp B. Đây là động lực để giới thiệu parentthuộc tính trong lớp B.

Tất nhiên, liên kết động này có thể được tạo lại mà không có lớp bên trong với chi phí là một công cụ đo đạc tẻ nhạt và dễ xảy ra lỗi của các lớp.


1

Trường hợp sử dụng chính mà tôi sử dụng này là ngăn chặn sự gia tăng của các mô-đun nhỏ để ngăn chặn ô nhiễm không gian tên khi các mô-đun riêng biệt không cần thiết. Nếu tôi đang mở rộng một lớp hiện có, nhưng lớp hiện có đó phải tham chiếu đến một lớp con khác phải luôn được kết hợp với nó. Ví dụ: tôi có thể có một utils.pymô-đun có nhiều lớp trợ giúp trong đó, không nhất thiết phải được kết hợp với nhau, nhưng tôi muốn củng cố việc ghép nối cho một số lớp trợ giúp đó. Ví dụ: khi tôi triển khai https://stackoverflow.com/a/8274307/2718295

: utils.py:

import json, decimal

class Helper1(object):
    pass

class Helper2(object):
    pass

# Here is the notorious JSONEncoder extension to serialize Decimals to JSON floats
class DecimalJSONEncoder(json.JSONEncoder):

    class _repr_decimal(float): # Because float.__repr__ cannot be monkey patched
        def __init__(self, obj):
            self._obj = obj
        def __repr__(self):
            return '{:f}'.format(self._obj)

    def default(self, obj): # override JSONEncoder.default
        if isinstance(obj, decimal.Decimal):
            return self._repr_decimal(obj)
        # else
        super(self.__class__, self).default(obj)
        # could also have inherited from object and used return json.JSONEncoder.default(self, obj) 

Sau đó chúng ta có thể:

>>> from utils import DecimalJSONEncoder
>>> import json, decimal
>>> json.dumps({'key1': decimal.Decimal('1.12345678901234'), 
... 'key2':'strKey2Value'}, cls=DecimalJSONEncoder)
{"key2": "key2_value", "key_1": 1.12345678901234}

Tất nhiên, chúng tôi có thể đã tránh kế thừa json.JSONEnocderhoàn toàn và chỉ ghi đè default ():

:

import decimal, json

class Helper1(object):
    pass

def json_encoder_decimal(obj):
    class _repr_decimal(float):
        ...

    if isinstance(obj, decimal.Decimal):
        return _repr_decimal(obj)

    return json.JSONEncoder(obj)


>>> json.dumps({'key1': decimal.Decimal('1.12345678901234')}, default=json_decimal_encoder)
'{"key1": 1.12345678901234}'

Nhưng đôi khi chỉ để quy ước, bạn muốn utilstạo thành các lớp để có thể mở rộng.

Đây là một trường hợp sử dụng khác: Tôi muốn một nhà máy sản xuất các biến thể trong OuterClass của mình mà không cần phải gọi copy:

class OuterClass(object):

    class DTemplate(dict):
        def __init__(self):
            self.update({'key1': [1,2,3],
                'key2': {'subkey': [4,5,6]})


    def __init__(self):
        self.outerclass_dict = {
            'outerkey1': self.DTemplate(),
            'outerkey2': self.DTemplate()}



obj = OuterClass()
obj.outerclass_dict['outerkey1']['key2']['subkey'].append(4)
assert obj.outerclass_dict['outerkey2']['key2']['subkey'] == [4,5,6]

Tôi thích mẫu này hơn bộ @staticmethodtrang trí mà bạn sẽ sử dụng cho một chức năng nhà máy.


1

1. Hai cách tương đương về chức năng

Hai cách hiển thị trước đây giống hệt nhau về chức năng . Tuy nhiên, có một số khác biệt nhỏ, và có những trường hợp bạn muốn chọn cái này hơn cái khác.

Cách 1: Định nghĩa lớp lồng nhau
(= "Lớp lồng nhau")

class MyOuter1:
    class Inner:
        def show(self, msg):
            print(msg)

Cách 2: Với cấp độ mô-đun Lớp bên trong được gắn với lớp Bên ngoài
(= "Lớp bên trong được tham chiếu")

class _InnerClass:
    def show(self, msg):
        print(msg)

class MyOuter2:
    Inner = _InnerClass

Dấu gạch dưới được sử dụng để tuân theo PEP8 "các giao diện nội bộ (gói, mô-đun, lớp, chức năng, thuộc tính hoặc các tên khác) phải - được bắt đầu bằng một dấu gạch dưới hàng đầu."

2. Điểm giống nhau

Đoạn mã dưới đây thể hiện các điểm tương đồng về chức năng của "Lớp lồng nhau" so với "Lớp bên trong được tham chiếu"; Chúng sẽ hoạt động theo cùng một cách trong việc kiểm tra mã kiểu của một cá thể lớp bên trong. Không cần phải nói, m.inner.anymethod()nó sẽ hoạt động tương tự với m1m2

m1 = MyOuter1()
m2 = MyOuter2()

innercls1 = getattr(m1, 'Inner', None)
innercls2 = getattr(m2, 'Inner', None)

isinstance(innercls1(), MyOuter1.Inner)
# True

isinstance(innercls2(), MyOuter2.Inner)
# True

type(innercls1()) == mypackage.outer1.MyOuter1.Inner
# True (when part of mypackage)

type(innercls2()) == mypackage.outer2.MyOuter2.Inner
# True (when part of mypackage)

3. Sự khác biệt

Sự khác biệt của "Lớp lồng nhau" và "Lớp bên trong được tham chiếu" được liệt kê bên dưới. Chúng không lớn, nhưng đôi khi bạn muốn chọn cái này hay cái kia dựa trên những thứ này.

3.1 Đóng gói mã

Với "Các lớp lồng nhau", có thể đóng gói mã tốt hơn so với "Lớp bên trong được tham chiếu". Một lớp trong không gian tên mô-đun là một biến toàn cục. Mục đích của các lớp lồng nhau là để giảm sự lộn xộn trong mô-đun và đưa lớp bên trong vào bên trong lớp ngoài.

Trong khi không có ai * đang sử dụng from packagename import *, số lượng biến cấp mô-đun thấp có thể rất tốt, chẳng hạn như khi sử dụng IDE với mã hoàn thành / intellisense.

* Đúng không?

3.2 Khả năng đọc của mã

Tài liệu Django hướng dẫn sử dụng Meta lớp bên trong cho siêu dữ liệu mô hình. Rõ ràng hơn một chút * để hướng dẫn người dùng khung viết một class Foo(models.Model)với bên trong class Meta;

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

thay vì "viết a class _Meta, sau đó viết a class Foo(models.Model)với Meta = _Meta";

class _Meta:
    ordering = ["horn_length"]
    verbose_name_plural = "oxen"

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Với cách tiếp cận "Lớp lồng nhau", mã có thể được đọc một danh sách dấu đầu dòng lồng nhau , nhưng với phương thức "Lớp bên trong được tham chiếu", người ta phải cuộn ngược lên để xem định nghĩa _Metađể xem các "mục con" (thuộc tính) của nó.

  • Phương thức "Lớp bên trong được tham chiếu" có thể dễ đọc hơn nếu mức độ lồng mã của bạn tăng lên hoặc các hàng dài vì một số lý do khác.

* Tất nhiên, vấn đề thị hiếu

3.3 Các thông báo lỗi hơi khác nhau

Đây không phải là một vấn đề lớn, nhưng chỉ để hoàn thiện: Khi truy cập thuộc tính không tồn tại cho lớp bên trong, chúng ta thấy các ngoại lệ khác nhau rõ ràng. Tiếp tục ví dụ được đưa ra trong Phần 2:

innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'

innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'

Điều này là do các typelớp bên trong là

type(innercls1())
#mypackage.outer1.MyOuter1.Inner

type(innercls2())
#mypackage.outer2._InnerClass

0

Tôi đã sử dụng các lớp bên trong của Python để tạo các lớp con có lỗi cố ý trong các hàm đơn nhất (tức là bên trong def test_something():) để tiến gần hơn đến phạm vi kiểm tra 100% (ví dụ: kiểm tra rất hiếm khi kích hoạt các câu lệnh ghi nhật ký bằng cách ghi đè một số phương thức).

Nhìn lại, nó tương tự như câu trả lời của Ed https://stackoverflow.com/a/722036/1101109

Các lớp bên trong như vậy sẽ vượt ra ngoài phạm vi và sẵn sàng cho việc thu gom rác sau khi tất cả các tham chiếu đến chúng đã bị xóa. Ví dụ: lấy inner.pytệp sau :

class A(object):
    pass

def scope():
    class Buggy(A):
        """Do tests or something"""
    assert isinstance(Buggy(), A)

Tôi nhận được các kết quả tò mò sau trong OSX Python 2.7.6:

>>> from inner import A, scope
>>> A.__subclasses__()
[]
>>> scope()
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A, scope
>>> from inner import A
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A
>>> import gc
>>> gc.collect()
0
>>> gc.collect()  # Yes I needed to call the gc twice, seems reproducible
3
>>> from inner import A
>>> A.__subclasses__()
[]

Gợi ý - Đừng tiếp tục và thử làm điều này với các mô hình Django, mô hình này dường như giữ các tham chiếu khác (được lưu trong bộ nhớ cache?) Đến các lớp lỗi của tôi.

Vì vậy, nói chung, tôi sẽ không khuyên bạn nên sử dụng các lớp bên trong cho loại mục đích này trừ khi bạn thực sự coi trọng phạm vi kiểm tra 100% đó và không thể sử dụng các phương pháp khác. Mặc dù tôi nghĩ rằng thật tuyệt khi biết rằng nếu bạn sử dụng __subclasses__(), đôi khi nó có thể bị ô nhiễm bởi các lớp bên trong. Dù bằng cách nào nếu bạn đã theo dõi đến mức này, tôi nghĩ rằng chúng ta đã khá sâu vào Python tại thời điểm này, các vết bẩn riêng tư và tất cả.


3
Đây không phải là tất cả về các lớp con và không phải các lớp bên trong sao ?? A
klaas

Trong thùng trên, Buggy kế thừa từ A. Vì vậy, phân loại cho thấy điều đó. Cũng xem hàm tích hợp sẵn Issubclass ()
klaas

Cảm ơn @klaas, tôi nghĩ có thể nói rõ hơn rằng tôi chỉ đang sử dụng .__subclasses__()để hiểu cách các lớp bên trong tương tác với trình thu gom rác khi mọi thứ vượt ra khỏi phạm vi trong Python. Điều đó trực quan dường như chiếm ưu thế trong bài đăng nên 1-3 đoạn đầu tiên đáng được mở rộng hơn một chút.
pzrq
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.