将类变量作为类方法参数的默认值

92 投票
4 回答
56826 浏览
提问于 2025-04-17 17:47

我想在一个类里面创建一个方法,这个方法的参数有默认值,这些默认值来自于这个类。一般来说,我会对一些数据进行过滤。在我的类里,有一个方法通常需要我传入一个数据向量。有时候我没有这个向量,就会用模拟的数据。每当我不传入特定的向量时,我希望默认使用模拟数据。我以为这应该很简单,只需要在我的方法定义里写 a=self.vector 就可以了。但出于某种原因,我遇到了一个错误 NameError: name 'self' is not defined。下面是我简化后的代码:

class baseClass(object):  # This class takes an initial data or simulation
    def __init__(self):
        self.x = 1
        self.y = 2

class extendedClass(baseClass): # This class does some filtering
    def __init__(self):
        baseClass.__init__(self)
        self.z = 5
    def doSomething(self, a=self.z):
        self.z = 3
        self.b = a

if __name__ == '__main__':
    a = extendedClass()
    print a.__dict__
    a.doSomething()
    print a.__dict__

我期望的输出应该是:

{'y': 2, 'x': 1, 'z': 5}
{'y': 2, 'x': 1, 'z': 3, 'b': 5}

我试过把默认值设置成 def doSomething(self, a=z):,但显然这样也不行。根据我的理解,self.z 在这个范围内是可见的,应该没有问题可以作为默认值。我不知道为什么会出现这个错误,也不知道该怎么做。这可能是个简单的问题,但我已经尝试了很长时间去弄明白或者找到解决方案。我只找到了一些类似的问题,但都是针对其他语言的。

4 个回答

11

这里是一个简单示例模块的代码反汇编。代码对象就像一个只读的容器,里面装着字节码、它用到的常量和名称,还有一些关于局部变量数量、所需栈大小等的元数据。注意,所有的代码对象都是作为常量编译的,这些是在编译时创建的。但是,像 class Afunction test 这样的对象是在执行时(比如模块被导入时)实例化的。

为了创建这个类,BUILD_CLASS 会使用名称 'A'、基类 tuple (object,),以及一个包含类命名空间属性的 dict。这就像手动调用 type(name, bases, dict) 来实例化一个类型。为了创建这个 dict,会从代码对象 A 创建一个函数并调用它。最后,类对象会通过 STORE_NAME 存储到模块命名空间中。

在代码对象 A 中,self.z 被加载到栈上,作为 MAKE_FUNCTION 的参数。字节码操作 LOAD_NAME 会在当前的局部变量(也就是正在定义的类命名空间)、模块的全局变量和内置函数中查找 self。如果 self 在全局或内置范围内没有定义,这显然会失败;而在局部范围内它也显然没有定义。

不过,如果查找成功了,函数会以 (self.z,) 作为它的 __defaults__ 属性被创建,然后存储到局部名称 test 中。

>>> code = compile('''
... class A(object):
...   def test(self, a=self.z): pass
... ''', '<input>', 'exec')

>>> dis.dis(code)
  2           0 LOAD_CONST               0 ('A')
              3 LOAD_NAME                0 (object)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object A ...>)
             12 MAKE_FUNCTION            0
             15 CALL_FUNCTION            0
             18 BUILD_CLASS         
             19 STORE_NAME               1 (A)
             22 LOAD_CONST               2 (None)
             25 RETURN_VALUE

>>> dis.dis(code.co_consts[1]) # code object A
  2           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  3           6 LOAD_NAME                2 (self)
              9 LOAD_ATTR                3 (z)
             12 LOAD_CONST               0 (<code object test ...>)
             15 MAKE_FUNCTION            1
             18 STORE_NAME               4 (test)
             21 LOAD_LOCALS         
             22 RETURN_VALUE       

@uselpa: 你的 pastebin 示例(为 2.x 重写):

>>> code = compile('''
... default = 1
... class Cl(object):
...     def __init__(self, a=default):
...         print a
... Cl()
... default = 2
... Cl()
... ''', '<input>', 'exec')
>>> dis.dis(code)
  2           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (default)

  3           6 LOAD_CONST               1 ('Cl')
              9 LOAD_NAME                1 (object)
             12 BUILD_TUPLE              1
             15 LOAD_CONST               2 (<code object Cl ...>)
             18 MAKE_FUNCTION            0
             21 CALL_FUNCTION            0
             24 BUILD_CLASS         
             25 STORE_NAME               2 (Cl)

  6          28 LOAD_NAME                2 (Cl)
             31 CALL_FUNCTION            0
             34 POP_TOP             

  7          35 LOAD_CONST               3 (2)
             38 STORE_NAME               0 (default)

  8          41 LOAD_NAME                2 (Cl)
             44 CALL_FUNCTION            0
             47 POP_TOP             
             48 LOAD_CONST               4 (None)
             51 RETURN_VALUE        

如你所见,类对象 Cl(和函数对象 __init__)只会被实例化一次,并存储到局部名称 'Cl' 中。模块在运行时是顺序执行的,因此后续重新绑定名称 default 不会对 __init__ 中的默认值产生影响。

你可以使用之前编译的代码和一个新的默认值动态实例化一个新函数:

>>> default = 1
>>> class Cl(object):
...     def __init__(self, a=default):
...         print a
... 

>>> from types import FunctionType
>>> default = 2
>>> Cl.__init__ = FunctionType(
...   Cl.__init__.__code__, globals(), '__init__', (default,), None)
>>> c = Cl()
2

这会重用已经编译的代码对象 __init__.__code__ 来创建一个带有新 __defaults__ 元组的函数:

>>> Cl.__init__.__defaults__
(2,)
13

默认参数只会在定义函数的时候计算一次。为了避免这个问题,可以这样做:

def doSomething(self, a=None):
    if a is None:
        a = self.z
    self.z = 3
    self.b = a

另外可以参考 这个链接

102

你的理解有点错误。self其实是这个函数定义里的一个参数,所以在那个时刻它是无法被访问的。它只能在这个函数内部被使用。

解决这个问题的方法很简单,就是把这个参数的默认值设为None,然后在方法内部检查这个值:

def doSomething(self, a=None):
    if a is None:
        a = self.z
    self.z = 3
    self.b = a

撰写回答