我在Python 3.6中遇到了奇怪的行为。 我能够从基类方法调用仅在子类中定义的函数和访问变量。 我发现这在我的代码中很有用,但是我来自C ++,并且这段代码看起来很奇怪。 有人可以解释这种行为吗?
class a:
def __init__(self):
print(self.var)
self.checker()
class b(a):
def __init__(self):
self.var=5
super().__init__()
def checker(self):
print('inside B checker')
myB = b()
输出:
5
inside B checker
答案 0 :(得分:2)
Python中的所有方法都是动态查找的。您正在self
上调用一个方法,并且self
是一个b
实例,并且b
实例具有一个checker
方法,因此该方法被调用。
在模块顶层或顶层函数中考虑以下代码:
myB = b()
myB.checker()
显然,全局模块代码不是b
类定义的一部分,但是,这显然是合法的。如果将代码放在class a
定义中,并将myB
重命名为welf
,为什么会有什么不同? Python不在乎。您只是在问这个值-您是否将其命名为myB
或self
-“您是否有一个名为checker
的东西?”,答案是肯定的,因此可以调用它。
var
更简单; self.var
只需将var
添加到self.__dict__
,就可以了;它是一个b
实例的事实在这里甚至都不相关(间接除外,因为一个b
实例意味着它被称为b.__init___
,而这就是var
所在的位置创建)。
如果您想知道这种“索取价值”的方式,一个略为简化的版本是:
__dict__
。当您执行self.var=5
时,实际上就执行了self.__dict__['var'] = 5
。而当您print(self.var)
时,它会print(self.__dict__['var'])
。KeyError
时(如self.checker
一样,Python会尝试type(self).__dict__['checker']
,如果不起作用,它将在type(self).mro()
上循环并尝试所有这些命令。KeyError
时,就像使用self.spam
一样,Python会调用self.__getattr__('spam')
。AttributeError
。请注意,如果您尝试构造一个a
实例,它将失败并显示一个AttributeError
。这是因为现在self
是a
,而不是b
。它没有checker
方法,也没有经过添加__init__
属性的var
代码。
在C ++中无法执行此操作的原因是C ++方法是静态查找的。值在运行时不是什么类型,而是在编译时是什么类型。如果静态查找方法说它是虚拟的,则编译器将插入一些动态查找代码,否则,它不会插入。 1
通常被解释的一种方式是在Python(以及其他具有SmallTalk语义的语言,例如ObjC和Ruby)中,所有方法都是自动虚拟的。但这有点误导,因为在C ++中,即使使用虚拟方法,方法名称和签名仍然必须在基类上可以找到;在Python(和SmallTalk等)中,这不是必需的。
如果您认为这一定太慢了,那么Python每次调用一个方法时都必须做一些事情,例如通过名称搜索该方法的一些名称空间堆栈-可以,但是这样做并不慢如您所料。一方面,名称空间是字典,因此它是恒定时间搜索。然后将字符串嵌入并缓存其哈希值。如果需要的话,解释器甚至可以缓存查找结果。结果 仍然比通过vtable取消引用指针要慢,但幅度不大(此外,Python中的许多 other 东西可能慢20倍)比起C ++,就像for
循环;当您需要每个细节尽可能快地工作时,您不使用纯Python)。
1。 C ++还存在另一个问题:即使您在var
中定义了checker
属性和a
虚拟方法,也没有选择初始化程序的调用顺序。编译器会先自动调用a
的构造函数,然后自动调用b
的构造函数。在Python中,它调用b.__init__
,您可以选择何时调用super().__init__()
,无论是在方法的开始,结束,中间还是什至从不。
答案 1 :(得分:0)
首先,您要创建class b
的实例,该实例将调用类b __init__
的构造函数。
在构造函数内部,您将属性self.var
设置为5。super().__init__()
之后将调用父类A的构造函数。
在class A
的构造函数中,同时打印self.var
和调用self.checker()
。
请注意,在调用super().__init__()
时,默认情况下会将子类实例self
作为第一个参数。