在Python中将类嵌套3层深——这是一种不好的做法吗?

3
我正在工作中编写一个Python脚本,用于与XLS / XLSX / CSV电子表格进行交互。有三个主要的类嵌套在一起(不是相互扩展的,这些类实际上是嵌套在另一个类内部)。
下面解释了三个主要的类:
  1. 主要的Workbook类,是XLS / XLSX / CSV类的工厂方法。这可以从外部访问
  2. Workbook类中的私有__Worksheet类,用于打开文件中的特定电子表格或工作表。只能通过Workbook.worksheet()方法访问
  3. __Worksheet类中的私有__Cell类,用于与单元格交互。这不应从外部访问,而应仅通过__Worksheet类访问
以下是到目前为止的类结构简化版:
class Workbook( object ):

    def __init__( self, file_name ):
        self.__file_name = file_name

    def worksheet( self, name ):
        return self.__Worksheet( self, name )

    class __Worksheet():
        def __init__( self, workbook, worksheet ):
            self.__workbook = workbook

        def cell( self, cell_id, data = None ):
            return self.__Cell( cell_id, data )

        class __Cell():
            def __init__( self, cell, data = None ):
                self.__cell = cell
                self.__data = data

            def setVal( self, data ):
                self.__data = data

            def __str__( self ):
                return self.__data

workbook = Workbook( 'test-file.csv' )
worksheet = workbook.worksheet( 'First Worksheet' )
cell_A1 = worksheet.cell('A1', 'Foo...')

print("Before - Cell A1: %s" % cell_A1) # => Before - Cell A1: Foo...
cell_A1.setVal('Bar...')
print("After - Cell A1: %s" % cell_A1)  # => Before - After - Cell A1: Bar...

所以我的问题是 - 在类中嵌套另一个类,再次嵌套第三个类是否被认为是不好的实践?
我对Python有些陌生,我的经验主要集中在PHP/JS/Perl上。在Python中,在类内部嵌套另一个类似乎并不太罕见,但由于某种原因,将一个类嵌套到三层深度中似乎是错误的。如果是这样,并且有更好的方法来解决它,那就太好了。
我知道的替代方法是不要嵌套类,而只需检查是否将Workbook的实例作为参数提供给Worksheet。然后在Workbook中创建一个方法,该方法仅返回Worksheet的实例,同时将self作为用于初始化它的参数之一。
示例:
class Workbook( object ):
    def __init__( self, file_name ):
        self.__file_name = file_name

    def worksheet( self, name ):
        return self.Worksheet( self, name )

class Worksheet( object ):
    def __init__( self, workbook, worksheet = 0 ):
        if not isinstance( workbook, Workbook ):
            raise Exception( 'Expected the workbook to be an instance of the Workbook class' )

        self.__workbook = workbook

    def cell( self, cell_id, data = None ):
        return self.Cell( cell_id, data )

class Cell( object ):
    def __init__( self, worksheet, cell, data = None ):
        if not isinstance( worksheet, Worksheet ):
            raise Exception( 'Expected the worksheet to be an instance of the Worksheet class' )

        self.__cell = cell
        self.__data = data

    def setVal( self, data ):
        self.__data = data

    def __str__( self ):
        return self.__data

# Example Usage One
workbook = Workbook( 'test-file.xls' )
worksheet = workbook.worksheet( 'First Worksheet' )
cell_A1 = worksheet.cell('A1', 'Foo...')

print("Before - Cell A1: %s" % cell_A1) # => Before - Cell A1: Foo...
cell_A1.setVal('Bar...')
print("After - Cell A1: %s" % cell_A1)  # => Before - After - Cell A1: Bar...

# Example Usage Two
workbook = Workbook( 'test-file.xlsx' )
worksheet = Worksheet( workbook, 'First Worksheet' )
cell_A1 = Cell( worksheet, 'A1', 'Foo...')

print("Before - Cell A1: %s" % cell_A1) # => Before - Cell A1: Foo...
cell_A1.setVal('Bar...')
print("After - Cell A1: %s" % cell_A1)  # => Before - After - Cell A1: Bar...

# Failed Example
worksheet = Worksheet( 'Not worksheet', 1 ) # => Exception, as expected

然而,这种替代方案意味着WorksheetCell类可以从外部访问并可以手动初始化...但我想这不是什么可怕的事情。请告诉我您认为最好的路线是什么! 这篇文章中的一条评论提供了另一篇SO帖子的链接,其中一个用户发布了嵌套类的3个优点,第一个是:

类的逻辑分组:如果一个类只对另一个类有用,则将其嵌入该类并将两个类放在一起是很合理的。嵌套这样的“helper classes”可以使它们的包更加流畅。

这正是我所想的。我只是觉得将它们嵌套到3层有些尴尬,因为我以前只做过2层。

如果有更好的方法,那就太好了。 - BartoszKP
2
为什么要这样做?与将它们全部设置为模块级别相比,您不会获得任何好处。 - user2357112
1
你的代码可读性好吗?在Python shell中键入“import this”以查看Python之禅。特别注意“扁平比嵌套更好”和“可读性很重要”。另一方面,在这里没有必要教条主义。如果你的代码已经很清晰易懂了,为什么不就让它保持原样呢? - John Coleman
@Justin请停止再次添加“谢谢”。同样的问题也适用于“更新”、“附言”和其他无关的标签。只有理解你的问题所必需的内容才是重要的(事实上,现在问题太长且难以理解。大字体粗体标记在这里无济于事)。 - BartoszKP
1
@Justin 顺便提一下,请停止使用双下划线来使你的变量“私有”,只需使用一个下划线即可达到此目的。 - Markus Meskanen
显示剩余5条评论
2个回答

6
把你的问题转化一下:嵌套编写类有什么优势吗?与函数不同,类不使用“词法作用域”(即与函数不同,无法从当前类的命名空间中解析出无法解析的名称)。这就是为什么您必须相对于 Workbook 的实例引用 __Worksheet 类的原因。
这反过来意味着,虽然可以使用嵌套,但并没有明显的优势。大多数有经验的Python程序员可能会在不使用嵌套的情况下编写您的示例。这简化了代码(因为类名都是包含模块的全局变量)。
请注意,这与在函数体内声明类完全不同。在这种情况下,类的代码可以引用函数命名空间中的变量,并且Python会尽力确保即使在函数调用终止并与调用相关联的本地命名空间已被销毁后,这些引用仍然可用于类。

你会建议在更新的帖子的最后一部分使用替代方法吗? - Justin
@Justin你在问题底部提供的答案太差了,我建议坚持使用这个。 - BartoszKP
Python之禅(可通过python -m this或在解释器中使用import this获得)。其中一行写道“扁平比嵌套好”。我个人不认为这是一块石碑,但它充满了对于经验不足者的良好指导方针。 - holdenweb

2
抛开其他问题,比如你是否需要所有这些类,我要说不需要:最好不要嵌套这些类。最好只是记录它们的预期使用方式。考虑一些让类本身不那么吸引人的备选名称,例如 WorksheetReference 和 CellReference。添加一些文档字符串,即使只是像这样的说明:
WorksheetReference 不应直接实例化。您应该通过 Workbook 的 worksheet() 方法访问它。
本答案的其余部分将探讨为什么这样做。
嵌套的成本和收益是什么?
在你的情况下,嵌套这些类只会提供额外的嵌套。这使得谈论嵌套的类更加困难:它不是一个 Worksheet,而是一个 Workbook.__Worksheet(实际上是一个 Workbook._Workbook__Worksheet,但这更加繁琐)。由于谈论起来更加困难,因此很难记录接口,以便某人知道如何使用从调用 myworkbook.worksheet 返回的内容。你的文档会说“返回具有 cells 方法的对象”还是“返回 Workbook.__Worksheet 对象”,或者采取完全不同的方法?

这种嵌套并不阻止某人尝试通过探究来实例化该类。因此,如果您真的担心防止“误用”,则仍需要检查传递给工作表的 __init__ 函数的值(稍后会详细介绍)。

为什么您关心强制执行使用模式?

Python 的原则之一是“成年人自行决定”。也就是说,我们最好展示和记录使用方法,并相信人们没有充分的理由来破坏它。如果创建没有工作簿的工作表或没有工作表的单元格没有好处,那么没有人会费心去做。

另一个原则是 鸭子类型。这基本上是指允许编程使用松散规定的接口是有益的。如果有人想要创建一个实现了足够多 Workbook 方法的类,那么它通常足够用于任何可以使用 Workbook 实例的地方。即使它在内部执行非常不同的操作。结合成年人的概念,这表明让某人尝试这样做可能是有益的。嵌套和基于类型的验证都与此相抵触。 试图防止误用 正如我刚才所提到的,我通常不推荐这样做。然而,如果你确实要这样做,那么你应该做好。你备选的 __init__ 函数中提出的检查会带来额外的麻烦,因为它鼓励了一种不良实践。由于它抛出了 Exception 类型的异常,唯一捕获它的方法是捕获所有异常。相反,既然你正在验证参数的类型,你应该考虑抛出一个更具体的异常,比如 TypeError,或者(在一个更大的代码库中)甚至是其自定义子类。 进一步调整方法 我同意一个评论,询问为什么WorksheetCells存在。就目前而言,拥有这个帮助类的好处非常少;它可以成为实现细节,或者可能完全省略。为了更简单地记录这样一个实现细节,您可以考虑通过__getitem__和/或__setitem__来实现其中一个或两个,从而使像这样的代码看起来更简洁:
worksheet = workbook['First Worksheet']
worksheet['A1'] = 'Foo...'

我假设在您的真实代码中,它们将具有比您已经展示的方法更多的功能。您可能仍然需要考虑使用__getitem__和/或__setitem__访问器,但是有足够的其他原因来公开和记录类。
作为附加功能的一部分,从访问现有工作表和单元格的方法中分离出可以创建它们的方法可能会很有帮助。
什么时候嵌套才是正确的?
这个特定的例子似乎不符合这种情况,但有时按照类似的接口对事物进行分组是有意义的。例如,您可能有多个Workbook子类需要实例化Worksheet的不同子类,这些子类又需要实例化不同的Cell子类。利用类继承机制将其存储在类中可能非常有用。
请注意,即使是这种情况也不需要类本身被嵌套,包含像这样的行就足够了:
class Workbook:
    # : : :
    worksheet_type = Worksheet
    # : : :
    def worksheet(self, name):
        return self.worksheet_type(self, name)

class WorkbookVariant(Workbook):
    # : : :
    worksheet_type = WorkbookVariant

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