嵌套类有什么用途?

5

最近我在Stackoverflow上看到有几个人做这类事情:

class A:
    foo = 1

    class B:
        def blah(self):
            pass

换句话说,它们有嵌套类。这是可行的(虽然 Python 初学者似乎会遇到问题,因为它的行为不像他们想象中的那样),但我想不出在任何语言中这样做的理由,尤其是在 Python 中。有这样的用例吗?人们为什么要这样做?搜索发现,在 C++ 中这种做法似乎相当普遍,是否有充分的理由呢?
8个回答

6
将一个类放入另一个类的主要原因是避免在全局命名空间中污染仅在一个类中使用且不属于全局命名空间的内容。即使在Python中,全局命名空间也是特定模块的命名空间。例如,如果您有SomeClass和OtherClass,并且它们都需要以专业方式读取某些内容,则最好拥有SomeClass.Reader和OtherClass.Reader,而不是SomeClassReader和OtherClassReader。
我从未在C++中遇到过这种情况。从嵌套类控制访问外部类字段可能会出现问题。在编译单元中只有一个公共类定义在头文件中,一些实用程序类定义在CPP文件中也很常见(Qt库是一个很好的例子)。这样它们对“外部人”不可见,这很好,所以在头文件中包含它们没有多大意义。这还有助于增加二进制兼容性,否则将难以维护。嗯,无论如何都很痛苦,但少得多。
一个很好的例子是Java,嵌套类在那里非常有用。嵌套类自动具有指向创建它们的外部类实例的指针(除非您将内部类声明为静态)。这样,您无需将“outer”传递给它们的构造函数,并且可以通过名称访问外部类的字段。

在Python中,你最好将东西放在模块中。通常全局命名空间中的东西很少,因为全局命名空间并不是真正的全局。所以我认为这并不适用于Python。 - Lennart Regebro
是的,在大多数情况下,你会更倾向于在Python中使用单独的模块。但这仍然是一个有效的答案,所以你还是可以得到+1分。 :) - Lennart Regebro
我可以说“同一个模块吗”?:-) 如果你想从命名空间中分离东西,通常会使用单独的模块。在这种情况下,你可能会将每个SomeClass放在somemodule中,而不是mymodule中,然后将其导入到mymodule中。或者也许将mymodule作为命名空间。但是,将其作为嵌套类是一种有效的方法。虽然我从未见过有人这样做,除了那些来自其他语言并且新于Python的人。 - Lennart Regebro
@Sergey Tachenov:没错。这是Python中比嵌套类更常见的解决方案,虽然我猜两者都是“正确”的。 - Lennart Regebro
@Martin,不,我从未使用过std::list或std::map,也没有遇到过使用它们的人。我使用Qt代替。现在我看看它,它也使用了一些嵌套类,只是我从来没有注意过这一点。但我仍然不会说它在C++中太常见。这不像Java,几乎每个程序都使用它们。 - Sergei Tachenov
显示剩余5条评论

4

它允许您控制嵌套类的访问-例如,它经常用于实现详细信息类。在C ++中,它还具有在何时解析各种内容以及无需先声明即可访问的优势。


3
我认为私有成员并不是一种障碍。容器类的用户不应该能够访问像std::map<K,V>::rb_node之类的内容。 - Charles Salvia
1
C++和Python在许多方面具有不同的基本设计目标--Python没有访问控制是有道理的,但对于C++来说并非如此。 - Stuart Golodetz
1
也许在像C++这样的低级语言中有安全原因,我没有足够的使用经验来知道。但我怀疑,使用该类的程序员只会找到更多漏洞的更糟糕的黑客攻击方法。:) 但是,在Java中私有成员已经咬了我一口,在Delphi中也是如此,我用了好几年。在Python中,我从未需要过它。 - Lennart Regebro
2
@Lennart:你完全没有考虑到一个原因:拥有一个私有方法,例如,允许你在编译时极其积极地进行内联。禁止您调用私有成员或字段的一个非常好的理由是,根据编译过程的积极程度,它们可能会不存在 - Jean Hominal
@Jean Hominal:这是一个有趣的原因。 @Steve Jessop:关键在于,在你提到的情况下,这个工具没有任何好处,而缺点是真实存在的。是的,工具作者通常会做出错误的决定。如果他们没有这样做,将事物设置为私有的就不会让我遇到麻烦。 - Lennart Regebro
显示剩余13条评论

3
我不是Python的铁粉,但对我来说,这种类型的决定更多的是语义层面而不是语法层面。如果你正在实现一个列表,List内部的Node类并不是一个独立的类,而是列表的实现细节。同时,你可以在TreeGraph内部有一个Node内部类。无论编译器/解释器是否允许你访问该类都是不同的事情。编程是关于编写计算机和其他程序员能读懂的规范,List.Node比将ListNode作为第一级类更加明确,因为它表明NodeList内部的类。

2

Python让你能够使用函数(包括lambda表达式)做很多事情,而在C++03或Java中,你需要用类来实现(尽管Java有匿名内部类,所以嵌套类并不总是像你的示例那样)。监听器、访问者等都需要用到类。列表推导式大致上可以看作是一种访问者:

Python:

(foo(x) if x.f == target else bar(x) for x in bazes)

C++:

struct FooBar {
    Sommat operator()(const Baz &x) const {
        return (x.f == val) ? foo(x) : bar(x);
    }
    FooBar(int val) : val(val) {}
    int val;
};

vector<Sommat> v(bazes.size());
std::transform(bazes.begin(), bazes.end(), v.begin(), FooBar(target));

C++和Java程序员会问自己一个问题:“我正在编写的这个小类应该出现在需要使用它的大类的同一作用域中,还是应该将其限制在仅使用它的类的作用域内?” 在这种情况下,由于您不想发布此内容或允许任何其他人依赖它,通常的答案是嵌套类。在Java中,私有类可以起到作用,在C ++中,您可以将类限制为TU,在这种情况下,您可能不太关心名称出现在哪个命名空间作用域中,因此实际上并不需要嵌套类。这只是一种风格,而且Java提供了一些语法糖。
正如其他人所说,另一个例子是C ++中的迭代器。Python可以支持迭代而无需迭代器类,但如果您在C ++或Java中编写数据结构,则必须将其放置在某个位置。为了遵循标准库容器接口,无论类是否嵌套,您都将具有嵌套typedef,因此自然而然地会考虑“嵌套类”。
他们也会问自己,“我应该只写一个for循环吗?”,但假设这种情况的答案是否定的...

2
在某些编程语言中,嵌套类将可以访问外部类作用域内的变量。同样的,函数或者函数内的类也是如此。当然,函数内嵌套类只会创建一个方法,其行为非常可预测。更加技术性地说,我们创建了一个闭包(closure)。

没错,而且在Python中,由于其明确性和显式的self,即使你不使用它,也可以从任何地方访问所有内容,如果你是明确的话,甚至你自己实例的成员也不能隐式地访问。 - Lennart Regebro

1
在C++中,至少一个主要的常见嵌套类用例是容器中的迭代器。例如,一个假设的实现可能看起来像这样:
class list
{
   public:

   class iterator 
   {
      // implementation code
   };

   class const_iterator
   {
      // implementation code
   };
};

在C++中使用嵌套类的另一个原因是为了私有实现细节,例如用于映射、链表等的节点类。

1
在Python中,更有用的模式是在函数或方法内部声明类。在另一个类的主体中声明类,正如其他答案中所指出的那样,几乎没有用处 - 是的,它确实避免了模块命名空间的污染,但由于存在模块命名空间,它上面有几个名称并不会影响太多。即使额外的类不打算直接由模块的用户实例化,将它们放在模块根目录下也可以使它们的文档更容易被其他人访问。
然而,在函数内部的类是完全不同的实体:每次运行包含类主体的代码时,它都会被“声明”和创建。这为创建各种用途的动态类提供了可能 - 以非常简单的方式。例如,通过这种方式创建的每个类都位于不同的闭包中,并且可以访问包含函数的变量的不同实例。
def call_count(func):
    class Counter(object):
       def __init__(self):
           self.counter = 0
       def __repr__(self):
           return str(func)
       def __call__(self, *args, **kw):
           self.counter += 1
           return func(*args, **kw)
    return Counter()

并在控制台上使用它:

>>> @call_count
... def noop(): pass
...
>>> noop()
>>> noop()
>>> noop.counter
2
>>> noop
<function noop at 0x7fc251b0b578>

因此,一个简单的 call_counter 装饰器可以使用一个静态的 "Counter" 类,在函数外定义,并将 func 作为其构造函数的参数 - 但如果你想调整其他行为,比如在这个例子中,使 repr(func) 返回函数表示而不是类表示,这种方式更容易实现。

1
“Nested classes” 这个术语可以有两种不同的含义,根据意图可以分为三个不同的类别。第一种纯粹是为了风格上的考虑,而另外两种则是为了实际应用目的而存在的,并且高度依赖于所使用的编程语言的特性。
  1. 嵌套类定义是为了创建新的命名空间和/或更好地组织代码。例如,在Java中,可以通过使用静态嵌套类来实现这一点,并且官方文档建议使用此方法来创建更易读和可维护的代码,并将类逻辑地分组在一起。然而,《Python之禅》建议您尽量减少嵌套代码块,因此不鼓励这种做法。

    import this

    在Python中,您更经常看到类被分组在模块中。

  2. 将一个类放在另一个类中作为其接口的一部分(或实例的接口)。首先,实现可以使用此接口来帮助子类化,例如想象一个嵌套类HTML.Node,您可以在HTML的子类中覆盖它以改变用于创建新节点实例的类。其次,虽然这对于类/实例用户可能有用,但除非您处于下面描述的第三种情况,否则这并不那么有用。

    至少在Python中,您不需要嵌套定义来实现上述任何一种情况,这可能非常罕见。相反,您可能会看到Node在类之外定义,然后在类定义中使用node_factory = Node(或专门用于创建节点的方法)。

  3. 嵌套对象的命名空间,或为不同的对象组创建不同的上下文。在Java中,非静态嵌套类(称为内部类)绑定到外部类的实例。这非常有用,因为它允许您拥有内部类的实例,这些实例位于不同的外部命名空间中。

    对于Python,请考虑decimal模块。您可以创建不同的上下文,并为每个上下文定义不同的精度。每个Decimal对象可以在创建时分配一个上下文。这通过不同的机制实现了与内部类相同的效果。如果Python支持内部类,并且ContextDecimal被嵌套,则会出现context.Decimal('3')而不是Decimal('3', context=context)

    您可以轻松地在Python中创建元类,使您可以创建生活在实例内部的嵌套类,甚至可以使其生成支持isinstance正确的适当绑定和未绑定类代理,通过使用__subclasscheck____instancecheck__。但是,它不会为您带来任何优势(例如__init__的附加参数)。它只会限制您可以使用它做什么,而且每次我必须使用Java中的内部类时,我都会感到非常困惑。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接