【发布时间】:2010-10-17 17:01:43
【问题描述】:
Python 的内部/嵌套类让我感到困惑。有没有他们不能完成的事情?如果有,那是什么东西?
【问题讨论】:
标签: python class oop language-features
Python 的内部/嵌套类让我感到困惑。有没有他们不能完成的事情?如果有,那是什么东西?
【问题讨论】:
标签: python class oop language-features
引用自http://www.geekinterview.com/question_details/64739:
内部类的优点:
- 类的逻辑分组:如果一个类只对另一个类有用,那么将它嵌入该类并将两者放在一起是合乎逻辑的。嵌套这样的“帮助类”使它们的包更加精简。
- 增强封装:考虑两个顶级类 A 和 B,其中 B 需要访问 A 的成员,否则这些成员将被声明为私有。通过将类 B 隐藏在类 A 中,可以将 A 的成员声明为私有,并且 B 可以访问它们。此外,B 本身可以对外界隐藏。
- 更具可读性、可维护性的代码:在顶级类中嵌套小类可以使代码更接近使用它的位置。
主要优势是组织性。任何可以通过内部类完成的事情都可以在没有它们的情况下完成。
【讨论】:
CacheMiss 异常的DataLoader 类。将主类下的异常嵌套为 DataLoader.CacheMiss 意味着您可以只导入 DataLoader 但仍然使用异常。
类内嵌套类:
嵌套类使类定义膨胀,使其更难看到发生了什么。
嵌套类会产生耦合,使测试更加困难。
在 Python 中,您可以在一个文件/模块中放置多个类,这与 Java 不同,因此该类仍然接近顶级类,甚至可以在类名前加上“_”以帮助表示其他人不应该使用它。
嵌套类可以证明有用的地方是函数内部
def some_func(a, b, c):
class SomeClass(a):
def some_method(self):
return b
SomeClass.__doc__ = c
return SomeClass
该类从函数中捕获值,允许您在 C++ 中动态创建类,如模板元编程
【讨论】:
有没有他们不能完成的事情?
没有。它们绝对等同于通常在顶层定义类,然后将对其的引用复制到外部类中。
我不认为嵌套类被“允许”有任何特殊原因,除了明确“禁止”它们也没有什么特别的意义。
如果您正在寻找存在于外部/所有者对象的生命周期内的类,并且始终具有对外部类实例的引用 — Java 中的内部类 – 那么 Python 的嵌套类不是事物。但是你可以破解一些类似的东西:
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
(这使用了类装饰器,这是 Python 2.6 和 3.0 中的新功能。否则,您必须在类定义之后说“Inner= innerclass(Inner)”。)
【讨论】:
self 而无需任何额外的工作(只需使用不同的标识符,您通常会将内部的 self 放在其中;例如 @ 987654324@),并且将能够通过它访问外部实例。
WeakKeyDictionary 实际上并不允许对键进行垃圾收集,因为这些值通过它们的 owner 属性强烈引用它们各自的键。
您需要仔细研究一下才能理解这一点。在大多数语言中,类定义是编译器的指令。也就是说,类是在程序运行之前创建的。在python中,所有语句都是可执行的。这意味着这个声明:
class foo(object):
pass
是一个在运行时执行的语句,就像这样:
x = y + z
这意味着您不仅可以在其他类中创建类,还可以在任何地方创建类。考虑这段代码:
def foo():
class bar(object):
...
z = bar()
因此,“内部类”的概念并不是真正的语言结构。这是一个程序员的构造。 Guido 对here 的产生有很好的总结。但本质上,基本思想是这简化了语言的语法。
【讨论】:
我理解反对嵌套类的论点,但在某些情况下使用它们是有道理的。想象一下,我正在创建一个双向链表类,我需要创建一个节点类来维护节点。我有两个选择,在 DoublyLinkedList 类中创建 Node 类,或者在 DoublyLinkedList 类之外创建 Node 类。在这种情况下我更喜欢第一个选择,因为 Node 类只在 DoublyLinkedList 类中才有意义。虽然没有隐藏/封装的好处,但可以说 Node 类是 DoublyLinkedList 类的一部分,这是一个分组的好处。
【讨论】:
Node 类对您可能还创建的其他类型的链表类没有用处,在这种情况下它可能应该只是在外面。
Node 在DoublyLinkedList 的命名空间下,这样是合乎逻辑的。这是 Pythonic:“命名空间是一个很棒的主意——让我们做更多这样的事!”
我使用 Python 的内部类在 unittest 函数中(即在 def test_something(): 中)故意创建有错误的子类,以便接近 100% 的测试覆盖率(例如,通过覆盖某些方法来测试很少触发的日志记录语句)。
回想起来,它类似于 Ed 的回答 https://stackoverflow.com/a/722036/1101109
这样的内部类应该在所有对它们的引用都被删除后超出范围并准备好进行垃圾回收。例如,采用以下inner.py 文件:
class A(object):
pass
def scope():
class Buggy(A):
"""Do tests or something"""
assert isinstance(Buggy(), A)
我在 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__()
[]
提示 - 不要继续尝试对 Django 模型执行此操作,这似乎保留了对我的错误类的其他(缓存?)引用。
所以一般来说,我不建议将内部类用于这种目的,除非您确实重视 100% 的测试覆盖率并且不能使用其他方法。虽然我认为很高兴知道如果您使用__subclasses__(),它可能有时会被内部类污染。无论哪种方式,如果您遵循这一点,我认为我们目前已经对 Python 非常深入,私有 dunderscores 等等。
【讨论】:
.__subclasses__() 来了解当事情超出Python 的范围时内部类如何与垃圾收集器交互。这在视觉上似乎在帖子中占主导地位,因此前 1-3 段值得进一步扩展。
我使用它的主要用例是防止小模块的扩散和,以在不需要单独的模块时防止命名空间污染。如果我正在扩展现有类,但该现有类必须引用另一个应该始终耦合到它的子类。例如,我可能有一个 utils.py 模块,其中包含许多帮助类,它们不一定耦合在一起,但我想加强这些帮助类中的一些的耦合。比如当我实现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)
那么我们可以:
>>> 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}
当然,我们本可以完全避免继承 json.JSONEnocder 而只是覆盖 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}'
但有时只是为了约定,您希望 utils 由类组成以实现可扩展性。
这是另一个用例:我想在我的 OuterClass 中创建一个可变工厂,而不必调用 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]
我更喜欢这种模式,而不是 @staticmethod 装饰器,否则你会使用它作为工厂函数。
【讨论】:
有没有他们无法完成的事情?如果是这样的话, 那是什么东西?
有些事情如果没有就不容易做到:相关类的继承。
这是一个极简示例,其中包含相关类 A 和 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
这段代码导致了一个相当合理和可预测的行为:
>>> 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'>
如果B 是一个顶级类,你不能在make_B 方法中写self.B() 而只是写B(),从而失去动态绑定到足够的课程。
请注意,在此构造中,您永远不应在类 B 的主体中引用类 A。这就是在B 类中引入parent 属性的动机。
当然,这种动态绑定可以在没有内部类的情况下重新创建,但代价是对类进行繁琐且容易出错的检测。
【讨论】:
前面显示的两种方式在功能上是相同的。但是,存在一些细微的差异,在某些情况下,您想选择一个而不是另一个。
方式一:嵌套类定义
(="Nested class")
class MyOuter1:
class Inner:
def show(self, msg):
print(msg)
方式 2:将模块级别的内部类附加到外部类
(="Referenced inner class")
class _InnerClass:
def show(self, msg):
print(msg)
class MyOuter2:
Inner = _InnerClass
下划线用于PEP8“内部接口(包、模块、类、函数、属性或其他名称)应该——以单个前导下划线作为前缀。”
下面的代码 sn-p 演示了“嵌套类”与“引用内部类”的功能相似之处;在检查内部类实例的类型时,它们的行为方式相同。不用说,m.inner.anymethod() 的行为与 m1 和 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)
下面列出了“嵌套类”和“引用内部类”的区别。它们并不大,但有时您想根据这些选择其中一个。
使用“嵌套类”可以比使用“引用内部类”更好地封装代码。模块命名空间中的类是一个 global 变量。嵌套类的目的是减少模块中的混乱,将内部类放在外部类中。
虽然没有人* 使用 from packagename import *,但少量的模块级变量可能会很好,例如在使用带有代码完成/智能感知的 IDE 时。
*对吗?
Django 文档指示将inner class Meta 用于模型元数据。指示框架用户写一个class Foo(models.Model) 和内部class Meta 会更清楚一点*;
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
而不是“写一个class _Meta,然后用Meta = _Meta写一个class Foo(models.Model)”;
class _Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
class Ox(models.Model):
Meta = _Meta
horn_length = models.IntegerField()
使用“嵌套类”方法,代码可以读取嵌套的项目符号列表,但使用“引用内部类”方法,必须向上滚动查看_Meta 的定义以查看其“子项”(属性)。
如果您的代码嵌套级别增加或由于某些其他原因行很长,“引用的内部类”方法可能更具可读性。
* 当然,个人喜好问题
这没什么大不了的,只是为了完整性:当访问内部类的不存在属性时,我们会看到略有不同的异常。继续第 2 节中给出的示例:
innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'
innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'
这是因为内部类的types是
type(innercls1())
#mypackage.outer1.MyOuter1.Inner
type(innercls2())
#mypackage.outer2._InnerClass
【讨论】: