Câu trả lời:
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.
DataLoader
lớp có thể ném một CacheMiss
ngoại lệ. Lồng ngoại lệ dưới lớp chính DataLoader.CacheMiss
có nghĩa là bạn có thể chỉ nhập DataLoader
nhưng vẫn sử dụng ngoại lệ.
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.)
self
mà 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 đó.
WeakKeyDictionary
trong 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 owner
thuộc tính của chúng .
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ữ.
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 ++
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.
Node
lớ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.
Node
nằm dưới không gian tên của DoublyLinkedList
, và nó có ý nghĩa hợp lý là như vậy. Đây là 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!"
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 A
và B
:
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 B
là một lớp cấp cao nhất, bạn không thể viết self.B()
trong phương thức make_B
mà 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 A
trong phần thân của lớp B
. Đây là động lực để giới thiệu parent
thuộ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.
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ỏ và để 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.py
mô-đ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.JSONEnocder
hoà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 utils
tạ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ộ @staticmethod
trang trí mà bạn sẽ sử dụng cho một chức năng nhà máy.
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."
Đ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 m1
vàm2
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)
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.
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?
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
Đâ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 type
lớp bên trong là
type(innercls1())
#mypackage.outer1.MyOuter1.Inner
type(innercls2())
#mypackage.outer2._InnerClass
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.py
tệ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ả.
.__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.