派生类是否自动具有基类的所有属性?

26 投票
2 回答
46573 浏览
提问于 2025-04-16 19:49

网上似乎没有很好的文档来解释这个问题:如果我创建了一个派生类,它会自动拥有基类的所有属性吗?那BaseClass.__init()是干什么用的呢?你还需要对其他基类的方法做同样的事情吗?BaseClass.__init__()需要参数吗?如果你的基类的__init__()有参数,那么这些参数在派生类中也会用到吗?你需要在派生类的__init__()中显式地设置这些参数,还是应该在BaseClass.__init__()中设置呢?

2 个回答

11

如果我创建一个派生类,它会自动拥有基类的所有属性吗?

类属性是会的,但实例属性就不一定了(因为实例属性在类创建时并不存在)。不过,如果派生类没有定义__init__方法,那么就会调用基类的__init__,这样实例属性就会被设置。

BaseClass.init()需要参数吗?

这要看类和它的__init__方法的定义。如果你在派生类中明确调用Base.__init__,那么至少需要把self作为第一个参数传进去。如果你有

class Base(object):
    def __init__(self):
        # something

那么很明显__init__不接受其他参数。如果你有

class Base(object):
    def __init__(self, argument):
        # something

那么在调用基类的__init__时,你就必须传入argument。这并不复杂。

如果你的基类init()有参数,派生类也会用到这些参数吗?你需要在派生类的init()中显式设置这些参数,还是直接设置给BaseClass.init()?

同样,如果派生类没有__init__,就会使用基类的那个。

class Base(object):
    def __init__(self, foo):
        print 'Base'

class Derived(Base):
    pass

Derived()   # TypeError
Derived(42) # prints Base

在其他情况下,你需要以某种方式处理这个问题。你可以选择使用*args, **kwargs,直接将参数传递给基类,或者复制基类的参数定义,或者从其他地方提供参数,这取决于你想要实现的目标。

62

如果你在一个从 BaseClass 继承的类中实现了 __init__ 方法,那么这个方法会覆盖掉从 BaseClass 继承来的 __init__ 方法,这样 BaseClass.__init__ 就不会被调用了。如果你需要调用 BaseClass 的 __init__ 方法(这通常是需要的),那么你得自己手动去调用它,通常是在你新实现的 __init__ 方法里调用 BaseClass.__init__

class Foo(object):
    def __init__(self):
        self.a = 10

    def do_something(self):
        print self.a

class Bar(Foo):
    def __init__(self):
        self.b = 20

bar = Bar()
bar.do_something()

这样会导致以下错误:

AttributeError: 'Bar' object has no attribute 'a'

所以,do_something 方法是如预期那样被继承了,但这个方法需要属性 a 被设置,而因为 __init__ 方法被覆盖了,所以 a 从来没有被设置。我们可以通过在 Bar.__init__ 中显式调用 Foo.__init__ 来解决这个问题。

class Foo(object):
    def __init__(self):
        self.a = 10

    def do_something(self):
        print self.a

class Bar(Foo):
    def __init__(self):
        Foo.__init__(self)
        self.b = 20

bar = Bar()
bar.do_something()

这样就会打印出 10,正如预期的那样。这里的 Foo.__init__ 期望接收一个参数,这个参数是 Foo 的一个实例(通常我们称它为 self)。

通常,当你在一个类的实例上调用方法时,这个类的实例会自动作为第一个参数传入。类实例的方法被称为 绑定方法bar.do_something 就是一个绑定方法的例子(你会注意到它是没有任何参数被传入的)。而 Foo.__init__ 是一个 未绑定方法,因为它没有和特定的 Foo 实例绑定,所以第一个参数,即 Foo 的实例,需要显式传入。

在我们的例子中,我们将 self 传给 Foo.__init__,这个 self 是传入 Bar__init__ 方法的 Bar 实例。由于 Bar 继承自 Foo,所以 Bar 的实例也是 Foo 的实例,因此将 self 传给 Foo.__init__ 是允许的。

你继承的类可能需要或接受的不仅仅是类的实例作为参数。这些参数的处理方式和你在 __init__ 中调用的任何方法是一样的:

class Foo(object):
    def __init__(self, a=10):
        self.a = a

    def do_something(self):
        print self.a

class Bar(Foo):
    def __init__(self):
        Foo.__init__(self, 20)

bar = Bar()
bar.do_something()

这将打印出 20

如果你想实现一个接口,完全暴露基类的所有初始化参数,你需要显式地做到这一点。通常是通过 *args 和 **kwargs 这两个参数来实现(这些名字是约定俗成的),它们是所有未显式命名的参数的占位符。以下示例使用了我讨论过的所有内容:

class Foo(object):
    def __init__(self, a, b=10):
        self.num = a * b

    def do_something(self):
        print self.num

class Bar(Foo):
    def __init__(self, c=20, *args, **kwargs):
        Foo.__init__(self, *args, **kwargs)
        self.c = c

    def do_something(self):
        Foo.do_something(self)
        print self.c


bar = Bar(40, a=15)
bar.do_something()

在这个例子中,参数 c 被设置为 40,因为它是传给 Bar.__init__ 的第一个参数。第二个参数则被包含在 argskwargs 变量中(* 和 ** 是特定的语法,表示在传递给函数/方法时将列表/元组或字典展开为单独的参数),并传递给 Foo.__init__

这个例子还强调了任何被覆盖的方法如果需要被调用,都必须显式调用(就像这里的 do_something)。

最后一点,你会经常看到 super(ChildClass, self).method()(其中 ChildClass 是某个任意的子类)被用来代替显式调用 BaseClass 的方法。关于 super 的讨论是另一个话题,但可以简单说,在这些情况下,它通常用于实现和调用 BaseClass.method(self) 一样的功能。简而言之,super 将方法调用委托给方法解析顺序中的下一个类 - MRO(在单继承中就是父类)。有关更多信息,请查看 super 的文档

撰写回答