在Python中从类变量调用类/静态方法

12
我试图创建一个ImageLoader类,用于处理加载和处理图像资源,代码如下:
class ImageLoader:
    TileTable = __loadTileTable('image path', some other variable)

    @staticmethod
    def _loadTileTable(arg1, arg2):
        blah blah

然而,在编译时我会得到:NameError: name '_loadTileTable' is not defined

如果我将第二行替换为TileTable = ImageLoader.__loadTileTable('image path', some other variable),那么我会得到NameError: name 'ImageLoader' is not defined

由于我从C#转到Python,使用静态类和静态方法是我用来实现这个的方法。然而,我也很想知道在Python中如何做到这一点(即调用仅通过它们的功能进行分组的静态库函数)。

更新: 阅读了两个答案后,我认为我尝试做的可能不正确。 我该怎么样才能实现ImageLoader,以便我可以这样做:

假设tile table返回一个数组

module1.py

aTile = ImageLoader.TileTable[1]

module2.py

anotherTile = ImageLoader.TileTable[2]

理想情况下,我只需一次填充TileTable。

更新:

感谢所有答案,我在Python模块文档中找到了上一次回答填充TileTable的方案。

 

"一个模块可以包含可执行语句以及函数定义。这些语句旨在初始化模块。它们仅在第一次导入模块时执行。"

至于静态类,我将放弃使用类,只制作一个模块级变量。


2
当你在Python中使用staticmethod时,通常意味着你正在做一些错误的事情。如果你既不需要实例变量也不需要类变量(静态字段),只需使用模块级函数即可。 - user395760
1
@delnan:我不完全同意。以Python默认的dict.fromkeys()为例,这些东西确实有合法的用例。 - jathanism
1
@delnan:我不同意。封装是面向对象编程的主要原则,Joe 正试图使用它。将仅与 ImageLoader 相关的函数保留在 ImageLoader 类中是一种有效的编程方式,可以在编码时明确表示它们之间的关系(而不是因为它们在同一个模块中而隐式关联)。 - Jason R. Coombs
1
@jathanism @Jason R. Coombs:是的,有合法的使用情况。但它们很少见。当与包含类存在明确的关联时,例如作为具有有用名称的替代构造函数 - 但这在这里并非如此,该函数是私有的,很可能永远不会在其他地方调用。如果您来自一种只能使用静态方法编写独立于实例的代码的语言,则应将静态方法视为要遗忘的内容(因为通常您在Python中不需要它们)。 - user395760
@Joe:“我该如何实现ImageLoader,以便我可以做到这一点?”这是一个单独的问题。关闭这个关于静态方法的问题。在一个新的问题中提出你的真正问题。 - S.Lott
显示剩余3条评论
4个回答

5
在Python中,类块中的代码会先执行,然后结果命名空间会被传递给类初始化器。你编写的代码也可以写成这样:
TileTable = _loadTileTable(arg1, arg2)
@staticmethod
def _loadTileTable(arg1, arg2):
    pass # blah blah
ImageLoader = type('ImageLoader', (), {'TileTable': TileTable, '_loadTileTable': _loadTileTable})
del TileTable
del _loadTileTable

正如您所看到的,_loadTileTable 的调用出现在其定义之前。在您的示例中,在类定义内,对 _loadTileTable 的调用必须在 _loadTileTable 的定义之后。

一种可能的解决方法是简单地重新排列类定义。

class ImageLoader:
    def _loadTileTable(arg1, arg2):
        pass # blah, blah

    TileTable = _loadTileTable('image path', other_var)

请注意,我移除了 'staticmethod',因为在调用 _loadTileTable 的时候,它被作为函数而不是方法调用。如果你真的想让它在类初始化后可用,你可以在事实发生后将其定义为静态方法。

class ImageLoader:
    def _loadTileTable(arg1, arg2):
        pass # blah, blah

    TileTable = _loadTileTable('image path', other_var)

    _loadTileTable = staticmethod(_loadTileTable)

你的示例代码实际上无法工作,因为类内部的方法调用需要传递无法传递的参数。 - jathanism
jathanism:我更新了答案以匹配问题的更改(假设_loadTileTable的参数在globals()中)。 - Jason R. Coombs
我不确定我完全理解你的意思。你的第二个代码块确实有效,我第一次没有尝试删除staticmethod装饰器。然而这意味着我必须担心我的变量/方法的声明顺序。是否有一种像C#那样的方式,可以在类中声明一个变量并使用一个方法,而不必担心调用的顺序? - Joe
@Joe:不是的。Python 是动态的,并将函数视为一等对象——因此,只有当执行流程到达“def…”行时,函数才会被创建并与名称关联。但由于函数的主体不是直接执行的,所以只有在声明与要直接执行的语句混合在一起时才会产生影响(出于这个原因,这种情况很少发生)。声明两个相互调用的函数是可以的,只要在定义两个函数之后才调用它们中的任何一个即可。 - user395760
我已经更新了问题,询问我真正想要的是什么,现在我明白了,在Python中我不知道如何编写这个。 - Joe
@Joe:作为一名曾经的C++程序员,我认为我从你最初的问题中理解了你所寻求的。我的答案仍然适用于你更新后的问题。 - Jason R. Coombs

5

回答更新后的问题,你在Python中所要做的就是将TileTable变成一个名为tile_table的变量,放在名为imageloader的模块中。完全没有必要把这些内容放在一个类中。

然后你会得到:

module1.py

import imageloader
aTile = imageloader.tile_table[1]

module2.py

import imageloader
anotherTile = imageloader.tile_table[2]

imageload.py 代码大致如下:

def _loadTileTable(arg1, arg2):
    pass # blah blah
tile_table = _loadTileTable('image path', other_var)

将Python模块视为其他语言中的单例实例(事实上它就是)并且您将能够将其与从其他语言继承的任何面向对象的先入为主的观念相一致。


我喜欢你的回答的简洁性,但这是否意味着每次我调用tile_table时,它都会再次执行_loadTileTable函数,从而不必要地加载相同的内容?因此,在这种情况下,我可能需要一个将其加载到模块级变量中并使用该变量的函数。 - Joe
不,imageload.py 中的代码只会在第一次导入时执行(哎呀,刚注意到应该是 imageloader.py!)。后续的导入不会再次执行代码,它们只会重复使用现有的模块。 - Duncan

3
类级别变量的更新是一件非常糟糕的事情。我们默认期望对象实例是有状态的,而类是无状态的。
在这种情况下,我们试图“神奇地”初始化一个集合作为类变量,这是一个极其糟糕的想法。一个集合只是一个具有简单实例级属性的对象。
神奇的Tile Table不应该是ImageLoader中隐藏的静态部分。没有任何理由这样做。如果您想避免多次加载它,它应该是ImageLoader的参数。
将它们分开可以促进可测试性。这不是武断的。这就是单元测试的方式。
你需要的是这个。
class ImageLoader( object ):
    def __init__( self, theTileTable ):
        self.tile_table= theTileTable

class TileTable( object ):
    def __init__( self, path, some_other_arg ):
         self.tileTable= self._loadTileTable( path, some_other_arg )
    def _loadTileTable(arg1, arg2):
         blah blah

没有静态的东西。独立的单元。更容易进行测试。没有奇怪的依赖关系。没有魔法。

1
类级变量永远不会被更新,只能被检索。顾名思义,它是一个图像瓦片表(一次从文件中检索)。imageloader类仅用于管理瓦片资源的创建和存储,我不认为有必要创建另一个类来保存一个在逻辑上属于imageloader类的方法。最后,要在两个单独的模块中使用它,我要么必须创建两个实例,要么传递一个imageloader引用,这两种方法我在我的问题中都提到了,但我不想这样做。 - Joe
@Joe:“我不明白为什么要创建另一个类来容纳一个在逻辑上应该属于imageloader类的方法。”可测试性。我认为答案已经说过了。“要在两个单独的模块中使用它,我要么必须创建两个实例,要么传递一个imageloader引用”,遗憾的是,传递实例是正确的答案,也是最具可测试性的答案。拒绝编写可测试代码并不是最好的想法。 - S.Lott
1
嗯...这有点偏题了,但即使我想测试从静态位置获取这些静态图像,我不是可以只运行模块本身并查看它是否生成图像吗?或者你是在谈论无法使用模块级变量进行模拟/存根类的能力? - Joe
@Joe:“我无法使用模块级变量来模拟/存根类的能力。” 确切地说,这就是为什么具有更改(“一次从文件中检索”是一种更改)的类级别魔术变量会使测试比必要复杂的原因。 - S.Lott
1
啊,知道了。你是说在测试时,我不应该有任何类或模块级别的变量吗?我注意到你没有提到模块级别的变量,我假设它们在测试时和类变量一样糟糕?嗯... 我想暂时加入这些会给我这个初学者的程序增加太多复杂性,但这些提示对我学习很有帮助。 - Joe
显示剩余2条评论

2

您使用静态方法的设计原因是什么?如果是这样,因为您没有重载类初始化,所以需要在方法定义之后声明变量

但是,如果这样做,您将会得到一个新的错误:

NameError: name 'arg1' is not defined

这是因为你在类实例化之前执行了方法,因此你没有机会将参数传递给方法。因此,正确的方法是重载__init__()方法,以便只有在构造类时才进行TileTable的赋值:
class ImageLoader(object):
    def __init__(self, arg1, arg2):
        self.TileTable = self._loadTileTable(arg1, arg2)

    @staticmethod
    def _loadTileTable(arg1, arg2):
        print arg1, arg2

这使您能够在没有实例的情况下调用ImageLoader._loadTileTable(),但也允许您在创建实例时创建TileTable实例变量。 使用类方法
针对我关于可能需要类方法的评论,以下是一个涵盖了这一点的示例:
class ImageLoader:
    @classmethod
    def _loadTileTable(cls, arg1, arg2):
        return arg1, arg2

# We're creating the class variable outside of the class definition. If you're doing 
# this in a module, no one will ever notice.
ImageLoader.TileTable = ImageLoader._loadTileTable('foo', 'bar')

可能有更好的方法,我不知道。但我认为这可以涵盖您想要的内容:

>>> i = ImageLoader()
>>> i
<__main__.ImageLoader instance at 0x100488f80>
>>> ImageLoader.TileTable
('foo', 'bar')
>>> i.TileTable
('foo', 'bar')

这里有一个实例i,可以访问类变量,但是ImageLoader.TileTable仍然可以从类对象中获得,而不需要实例。


我认为应该指出我的目标是创建一个静态的TileTable,以便其他类可以访问它。我不希望有单独的ImageLoader实例漂浮在周围。loadTileTable旨在成为一个私有函数,在TileTable首次使用或静态类首次初始化时执行一些工作来加载瓷砖。因此,只需要在应用程序启动时执行一次。 - Joe
听起来你可能需要一个classmethod,它使用类对象作为第一个参数(而实例方法使用实例作为第一个参数)。静态方法不绑定到类,因此无法访问类变量。 - jathanism
我尝试将装饰器更改为classmethod,但它仍然告诉我该方法未定义。 - Joe
我更新了答案,使用了classmethod,请看一下。我想知道这是否有帮助。 :) - jathanism
那么调用ImageLoader.TileTable会被重定向到ImageLoader._loadTileTable对吧?但这意味着每次我调用ImageLoader.TileTable,都会重新加载瓷砖表,就像其他答案一样。我希望的是在应用程序的整个生命周期中只调用一次_loadTileTable。 - Joe
我非常确定,就像任何变量赋值一样,除非重新声明变量,否则对它的任何进一步引用都只会给出赋值操作的返回值,并且不会再次执行方法/函数。 - jathanism

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