Python类语法-这是一个好主意吗?

4

我倾向于这样定义我的Python类:

class MyClass(object):
    """my docstring"""

    msg = None
    a_variable = None
    some_dict = {}

    def __init__(self, msg):
        self.msg = msg

在类似Java的语言中,将对象变量(msg、a_variable等)放在顶部声明好还是坏还是无所谓?我知道这是不必要的,但仍然很有诱惑力。


2
虽然属性存储在不同的位置,但当值是不可变的并且通过self而不是类对象访问它们时,这并没有什么区别。对于可变对象(如some_dict),这确实会有很大的影响。 - Remy Blank
对于不可变的“变量”(msg和a_variable分配为None),我强烈建议您阅读有关Python中名称的内容:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables 如果您将其视为标签 名称而不是变量,那么“初始化变量”就没有任何意义。 None只是一个站在那里的对象,除非您正在使用它,否则不应该给它打标签。 - cregox
4个回答

8
在类定义中定义变量可以使该变量在该类的每个实例之间可访问。从Java的角度来看,这有点像将变量设为静态变量。然而,下面展示了主要区别。
class MyClass(object):
    msg = "ABC"

print MyClass.msg     #prints ABC
a = MyClass()
print a.msg           #prints ABC
a.msg = "abc"
print a.msg           #prints abc
print MyClass.msg     #prints ABC
print a.__class__.msg #prints ABC

从上面的代码可以看出,它与定义在类作用域的变量不太相同。虽然可以通过self.msg访问变量,但当它被赋值时,它并没有被赋值给在类作用域中定义的变量。

通过您所使用的方法进行操作的一个缺点是,它会向类添加隐藏状态,从而可能导致错误。比如说,有人在构造函数中遗漏了self.msg = "ABC"(或更现实地说,代码被重构,只有一个定义被改变)。

a = MyClass()
print a.msg   #prints ABC

#somewhere else in the program
MyClass.msg = "XYZ"

#now the same bit of code leads to a different result, despite the expectation that it
#leads to the same result.
a = MyClass()
print a.msg   #prints XYZ

最好避免在类级别上定义msg,这样就可以避免出现问题:
class MyClass(object):
    pass

print MyClass.msg #AttributeError: type object 'MyClass' has no attribute 'msg'

6
在类定义内直接声明变量会使它们成为类变量而不是实例变量。类变量有点类似于Java中的静态变量,应该像 MyClass.a_variable 这样使用。但是它们也可以像 self.a_variable 一样使用,这是一个问题,因为天真的程序员可能会将它们视为实例变量。例如,您的 "some_dict" 变量将被 MyClass 的每个实例共享,因此如果您向其中添加键 "k",那么任何实例都可以看到。
如果您始终记得重新分配类变量,那么与实例变量几乎没有区别。只有在 MyClass 中的初始定义将保留。但无论如何,这都不是好的实践,因为当不重新分配这些变量时,可能会遇到麻烦!
最好像这样编写类:
class MyClass(object):
    """
    Some class
    """

    def __init__(self, msg):
        self.__msg = msg
        self.__a_variable = None
        self.__some_dict = {}

使用双下划线表示“私有”变量(伪私有!)是可选的。如果变量应该是公共的,只需保留它们的名称而不带有__前缀。


"__" 前缀不能使变量真正私有化,只是进行了一些名称修饰(如链接文档中所解释的),这通常会让子类化和测试更加繁琐。现在很少有好的 Python 代码使用它,而更喜欢使用传统的单个下划线表示内部属性。 - Mike Graham
@Mike Graham:我提到这是可选的。开发人员可以自行决定。 - AndiDog

4
小心。这两个msg属性实际上存储在两个不同的字典中。其中一个会掩盖另一个,但被覆盖的msg属性仍然占用字典中的空间。因此它未被使用,但仍然占用一些内存。
class MyClass(object):    
    msg = 'FeeFiFoFum'   
    def __init__(self, msg):
        self.msg = msg

m=MyClass('Hi Lucy')

请注意,我们将'Hi Lucy'作为值。
print(m.__dict__)
# {'msg': 'Hi Lucy'}

注意,MyClass的字典(通过 m.__class__ 访问)仍然具有 FeeFiFoFum
print(m.__class__.__dict__)
# {'__dict__': <attribute '__dict__' of 'MyClass' objects>, '__module__': '__main__', '__init__': <function __init__ at 0xb76ea1ec>, 'msg': 'FeeFiFoFum', 'some_dict': {}, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': 'my docstring', 'a_variable': None}

另一个(可能更简单)的方法是这样看待它:
print(m.msg)
# Hi Lucy
print(MyClass.msg)
# FeeFiFoFum

在这种情况下,“占用一些内存”并不是一个值得注意的问题。如果您创建更多的MyClass实例,就不会有更多的“FeeFiFoFum”。它们只会有一个。从内存方面来看,这并不是什么大问题。 - FogleBird
@FogleBird:我同意浪费的内存量很小,但是为什么要浪费内存呢?无论如何,我主要提到内存问题是为了证明类属性和实例属性完全不同。在类级别初始化属性并不会初始化实例属性,因此OP的代码并没有做到我认为OP想做的事情。 - unutbu

1

当你声明一个类时,Python会解析它的代码并将所有内容放入该类的命名空间中;然后,该类将用作所有派生对象的一种模板——但任何对象都将拥有其自己的引用副本。
请注意,您总是有一个引用;因此,如果您能够更改所引用的对象,则更改将反映在其被使用的所有位置。但是,成员数据的插槽对于每个实例都是唯一的,因此将其分配给新对象不会反映到任何其他位置。

注意:Michael Foord在他的博客文章中详细介绍了类实例化的工作原理;如果您对此主题感兴趣,我建议您阅读短篇文章。

无论如何,就所有实际用途而言,你的两种方法之间有两个主要区别:

1. 名称已经在类级别可用,您可以在不实例化新对象的情况下使用它;这在命名空间中声明常量时听起来很整洁,但在许多情况下,模块名称可能已经是一个好名称。 2. 名称添加在类级别 - 这意味着您可能无法在单元测试期间轻松地模拟它,并且如果您有任何昂贵的操作,则会在导入的那一刻立即执行。
通常,我在审查代码时会有点怀疑地声明成员为类级别;它们有很多很好的用例,但也很可能它们存在于先前使用其他编程语言的经验的习惯中。

1
“该类将被用作所有从它派生的对象的一种模板,但任何对象都将有其自己的数据副本”是不正确的。 - Craig McQueen
-1: 像 OP 的 msga_variablesome_dict 这样设置类成员肯定会使它们在该类的所有对象之间共享,除非对象的构造函数复制并替换了这些成员。你见过什么不同的例子吗? - Jarret Hardie
你们两个都是对的。我只关注成员槽没有被共享的事实,但它持有一个引用的事实也应该是解释的一部分。现在应该更清楚了。 - rob
那么,问题的答案是什么?是好的、坏的还是无所谓的? - OscarRyz
这取决于你想做什么。如果你只是因为从其他语言习惯了这样做,那可能不好。如果你之所以这样做是因为你想要它变成那样,那就非常好。 - rob

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