18

这个问题建立在前面提出的这个问题和这个问题的基础上,但解决了一个更微妙的问题,具体来说,什么算作“内部类”?

这是我的情况。我正在构建一个用于操作 MS Office 文件的 Python 库,其中许多类并不打算在外部构建,但它们的许多方法是 API 的重要部分。例如:

class _Slide(object):
    def add_shape(self):
        ...

_Slide不是在外部构建的,作为库的最终用户,您可以通过调用Presentation.add_slide(). 但是,一旦有了幻灯片,您肯定希望能够调用add_shape(). 所以方法是 API 的一部分,但构造函数不是。这种情况在库中出现了数十次,因为只有 Presentation 类具有外部/API 构造函数。

PEP8 在这一点上是模棱两可的,它提到了“内部类”而没有详细说明什么是内部类。在这种情况下,我想可以说这个类是“部分内部的”。

问题是我只有两个符号来区分至少三种不同的情况:

  1. 类的构造函数和“public/API”方法都可以使用。
  2. 不使用构造函数,但类的“公共/API”方法是。
  3. 该类确实是内部的,没有公共 API,我保留在未来版本中更改或删除它的权利。

我注意到,从沟通的角度来看,外部 API(图书馆最终用户受众)和内部 API(开发人员受众)的清晰表达之间存在冲突。对于最终用户来说,呼叫_Slide()是禁止/自担风险的。然而,它是内部 API 的一个快乐成员,并且_random_helper_method()只能从内部调用_Slide()

你对这个问题有什么看法可能对我有帮助吗?惯例是否规定我在类名上使用“单前导下划线”弹药来争取最终用户 API 的清晰性,或者我是否可以保留它以便与自己和其他开发人员就类 API 何时真正存在进行沟通?私有并且不被它所在模块之外的对象使用,因为它是一个可能会改变的实现细节?


更新:经过几年的进一步思考,我已经习惯了在命名不打算作为模块(文件)之外的类访问的类时使用前导下划线。这种访问通常是实例化该类的对象或访问类方法等。

这为模块的用户(当然通常是您自己 :) 提供了基本指示符:“如果带有前导下划线的类名出现在 import 语句中,则说明您做错了。” (单元测试模块是这个规则的一个例外,这样的导入可能经常出现在“内部”类的单元测试中。)

请注意,这是对的访问。在模块外访问该类(类型)的对象(可能由工厂或其他任何东西提供)非常好,并且可能是预期的。我认为这种无法将类与从它们创建的对象区分开来是导致我最初感到困惑的原因。

这个约定还有一个好处,就是在from module import *声明时不包括这些类。尽管我从不在自己的代码中使用这些并建议避免使用它们,但这是一种适当的行为,因为这些类标识符不打算成为模块接口的一部分。

这是我经过多年试验后的个人“最佳实践”,当然不是“正确”的方式。你的旅费可能会改变。

4

4 回答 4

4

我无法仅从您问题中的描述中看出,但从您在评论中提供的其他信息来看,我认为您的Slide课程实际上是公开的。

尽管实例只会通过调用 a 的add_slide()方法来间接创建,但这是正确的,Presentation因为调用者随后将可以自由(并且更有可能需要)调用实例的方法来操作它。在我看来,一个真正的私有类只能由“拥有”它的类的方法访问。

让事情成为任何其他方式既会破坏封装,又会增加设计组件之间的耦合,这两者都是不可取的,应尽可能避免以实现灵活性和可重用性。

于 2013-08-31T21:28:22.780 回答
4

我认为 martineau 的答案是一个很好的答案,当然是最简单的,无疑也是最 Pythonic 的。

不过,这绝不是唯一的选择。

一种经常使用的技术是将公共方法定义为接口类型的一部分。例如zope.interface.Interface在扭曲框架中被广泛使用。现代 python 可能会使用abc.ABCMeta相同的效果。本质上,公共方法Presentation.add_slide被记录为返回一些AbstractSlide具有更多公共方法的实例;但由于无法AbstractSlide直接构造 的实例,因此没有公开的方法来创建实例。

这种技术特别方便,因为可以创建抽象类型的模拟实例,它可以断言只有公共方法被调用;对于单元测试非常有用(特别是对于您的库的用户,这样他们就可以确保他们只使用公共接口)。

另外的选择; 因为创建幻灯片类实例的唯一公开方法是通过Presentation.add_slide;你可以让它成为构造函数。这可能需要一些元类恶作剧,这样的事情应该做。

from functools import partial

class InstanceConstructor(type):
    def __get__(cls, instance, owner):
        if instance is not None:
            return partial(cls, instance)
        return cls

并且只需定义类add_slide中的行为__init__

>>> class Presentation(object):
...     def __init__(self):
...         self.slides = []
...
...     class add_slide(object):
...         __metaclass__ = InstanceConstructor
...
...         def __init__(slide, presentation, color):
...             slide.color = color
...             presentation.slides.append(slide)
...
>>> p = Presentation()
>>> p.add_slide('red')
<instance_ctor.add_slide object at ...>
>>> p.slides
[<instance_ctor.add_slide object at ...>]
>>> p.slides[0].color
'red'
于 2013-08-31T21:55:26.643 回答
1

什么是“内部”类是一个约定问题,这就是 PEP 没有定义该术语的原因,但是如果您将这些暴露给您的用户(提供返回此类的方法,或者依赖于让此类通过周围)它根本不是真正的“内部”。

你有三个选择,真的。首先可能是记录并明确您对如何构建此类的意图,换句话说,允许您的用户构建和使用该类的实例,并记录如何执行此操作。由于您希望您的更高级别的类跟踪这些 Slide 对象,请使其成为构造函数接口要求和设置的东西,以便您的模块的用户可以像您一样制作这些对象。

另一种方法是将您想要在创建和管理这些实例的类中公开的此类的那些方面包装起来,换句话说,使其成为真正的内部类。如果您只需要公开一种方法(例如,add_shape()),这可能是非常合理的。但是,如果您希望公开该类的大部分接口,那将开始显得笨拙。

最后,您可以清楚地记录用户不应自己创建此类的实例,而应有权访问接口的某些部分。这令人困惑,但可能是一种选择。

我自己喜欢第一个选择:将类公开给模块的用户,并使用它的构造函数和析构函数实现来确保它始终以正确的方式连接到需要了解它的其他类。然后,用户可以做一些事情,比如子类化这个东西并包装他们想要调用的方法,并且你确保你的对象保持连接。

于 2013-08-31T22:31:55.010 回答
0

__all__ = ['Presentation']在模块中隐藏了help()除 Presentation 类之外的所有类。但是开发人员仍然可以从外部调用构造函数和方法。

于 2013-08-30T19:51:40.020 回答