繁体   English   中英

嵌套类是否有用例?

[英]Is there a usecase for nested classes?

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

class A:
    foo = 1

    class B:
        def blah(self):
            pass

换句话说,他们有嵌套类。 这是有效的(虽然Python新手似乎遇到了问题,因为它的行为并不像他们想象的那样),但我想不出任何理由在任何语言中都这样做,当然也不是在Python中。 有这样的用例吗? 为什么人们这样做? 搜索它似乎在C ++中相当普遍,有充分的理由吗?

将一个类放在另一个类中的主要原因是避免使用仅在一个类中使用的内容来污染全局名称空间,因此不属于全局名称空间。 这甚至适用于Python,全局命名空间是特定模块的命名空间。 例如,如果您有SomeClass和OtherClass,并且它们都需要以专门的方式读取内容,那么最好使用SomeClass.Reader和OtherClass.Reader而不是SomeClassReader和OtherClassReader。

不过,我从未在C ++中遇到过这种情况。 从嵌套类控制对外部类字段的访问可能会有问题。 在头文件中定义的编译单元中只有一个公共类以及CPP文件中定义的一些实用程序类(Qt库就是一个很好的例子)也很常见。 这样它们对于“外人”是不可见的,这是好的,所以将它们包含在标题中没有多大意义。 它还有助于增加二进制兼容性,否则这是一个难以维护的问题。 嗯,无论如何,这是一种痛苦,但更不用说了。

嵌套类非常有用的语言的一个很好的例子是Java。 嵌套类自动有一个指向创建它们的外部类的实例的指针(除非您将内部类声明为静态)。 这样您就不需要将“外部”传递给它们的构造函数,只需按名称来解析外部类的字段。

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

我不是python的忠实粉丝,但对我来说这种类型的决策比语法更具语义性。 如果您正在实现一个列表,那么List的类Node本身并不是一个可以在任何地方使用的类,而是列表的实现细节。 同时,您可以在TreeGraph拥有Node内部类。 编译器/解释器是否允许您访问类是另一回事。 编程是关于编写计算机可以遵循的规范以及其他List.Node可以读取的List.NodeList.Node更明确,因为NodeList内部而不是将ListNode作为第一级类。

在某些语言中,嵌套类可以访问外部类中的变量。 (与函数或类函数嵌套类似。当然,函数类嵌套只是创建一个方法,其行为相当不出人意料。;))

在更多技术术语中,我们创建了一个闭包

Python允许你使用函数(包括lambdas)做很多事情,在C ++ 03或Java中你需要一个类(虽然Java有匿名内部类,所以嵌套类并不总是看起来像你的例子)。 听众,访客,那种事。 列表理解是一种松散的访问者:

蟒蛇:

(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编写数据结构,那么你必须将blighter放在某个地方。 要遵循标准的库容器接口,无论该类是否嵌套,您都将拥有嵌套的typedef,因此认为“嵌套类”是相当自然的。

[*]他们也会问自己,“我应该写一个for循环吗?”,但让我们假设一个案例,答案是否定的......

至少在C ++中,嵌套类的一个主要常见用例是容器中的迭代器。 例如,假设的实现可能如下所示:

class list
{
   public:

   class iterator 
   {
      // implementation code
   };

   class const_iterator
   {
      // implementation code
   };
};

C ++中嵌套类的另一个原因是私有实现细节,如地图,链表等的节点类。

“嵌套类”可以表示两种不同的东西,可以按意图分为三个不同的类别。 第一个是纯粹的风格,另外两个用于实际目的,并且高度依赖于使用它们的特征语言。

  1. 嵌套类定义,用于创建新命名空间和/或更好地组织代码。 例如,在Java中,这是通过使用静态嵌套类来完成的,并且官方文档建议将其作为创建更易读和可维护的代码以及将类逻辑分组在一起的方法。 然而,Python的Zen表明你较少嵌套代码块,从而阻碍了这种做法。

    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中创建一个元类,它允许您创建实体内部的嵌套类,甚至可以通过使用__subclasscheck____instancecheck__来生成正确支持isinstance绑定和未绑定类代理。 但是,它不会通过其他更简单的方法获得任何东西(例如__init__的附加参数)。 它只会限制你可以用它做什么,而且每次我必须使用它时,我发现Java中的内部类非常混乱。

在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)返回函数表示,而不是类表示,更容易这样做。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM