Python 中的方法解析和调用是如何工作的?
在Python中,方法是怎么被调用的呢?也就是说,Python虚拟机是怎么理解这些调用的。
确实,Python的方法解析速度可能比Java要慢。这种现象被称为“晚绑定”。
这两种语言在反射机制上有什么不同呢?有没有好的资源可以帮助解释这些方面的内容?
3 个回答
确实,Python的方法解析速度可能比Java慢。那么,什么是晚绑定呢?
晚绑定是指一种策略,用于描述某种语言的解释器或编译器如何决定将一个标识符(比如变量名)映射到一段代码上。举个例子,在C#中写obj.Foo()
时,当你编译这段代码,编译器会尝试找到这个对象,并插入一个指向将在运行时调用的Foo
方法位置的引用。所有这些方法解析都是在编译时进行的;我们称这种方式为“早绑定”。
而Python则是“晚绑定”。方法解析发生在运行时:解释器会尝试找到具有正确签名的Foo
方法,如果找不到,就会出现运行时错误。
这两种语言的反射机制有什么不同?
动态语言通常比静态语言有更好的反射功能,而Python在这方面非常强大。不过,Java也有相当全面的方法来访问类和方法的内部信息。尽管如此,你还是无法避免Java的冗长;在Java中完成同样的事情,你需要写更多的代码,而在Python中则相对简单。可以查看java.lang.reflect
API。
在Python中,名字(比如方法、函数和变量)是通过查看命名空间来找到的。命名空间在CPython中是用dict
(字典)来实现的。
当在实例的命名空间(dict
)中找不到某个名字时,Python会去查找类,然后再查找基类,按照方法解析顺序(MRO)来进行。
所有的查找都是在程序运行时进行的。
你可以使用dis
模块来看看这些查找是如何在字节码中发生的。
简单的例子:
import dis
a = 1
class X(object):
def method1(self):
return 15
def test_namespace(b=None):
x = X()
x.method1()
print a
print b
dis.dis(test_namespace)
这段代码会打印:
9 0 LOAD_GLOBAL 0 (X)
3 CALL_FUNCTION 0
6 STORE_FAST 1 (x)
10 9 LOAD_FAST 1 (x)
12 LOAD_ATTR 1 (method1)
15 CALL_FUNCTION 0
18 POP_TOP
11 19 LOAD_GLOBAL 2 (a)
22 PRINT_ITEM
23 PRINT_NEWLINE
12 24 LOAD_FAST 0 (b)
27 PRINT_ITEM
28 PRINT_NEWLINE
29 LOAD_CONST 0 (None)
32 RETURN_VALUE
所有的LOAD
操作都是在查找命名空间。
在Python中,调用方法其实分为两个步骤。第一步是查找属性,第二步是调用查找到的结果。这意味着下面这两段代码的意思是一样的:
foo.bar()
method = foo.bar
method()
在Python中查找属性的过程其实挺复杂的。假设我们要查找一个名为attr的属性,属于对象obj,也就是在Python代码中写成obj.attr。
首先会在obj的实例字典里查找“attr”,如果没找到,就会按照一定的顺序在obj的类的实例字典和它的父类的字典里继续查找“attr”。
通常情况下,如果在实例字典里找到了值,就会返回这个值。但是如果在类里查找到了一个同时具有__get__和__set__方法的值(也就是说,如果在这个值的类和父类的字典里都找到了这两个键的值),那么这个类属性就被称为“数据描述符”。这意味着会调用这个值的__get__方法,并传入查找发生的对象,最后返回这个方法的结果。如果类属性没有找到,或者不是数据描述符,那么就会返回实例字典里的值。
如果在实例字典里没有找到值,那么就会返回类查找的结果。除非这个结果是“非数据描述符”,也就是它有__get__方法。这时会调用__get__方法,并返回结果。
还有一种特殊情况,如果obj恰好是一个类(也就是类型type的实例),那么也会检查实例值是否是描述符,并相应地调用。
如果在实例和它的类层级中都没有找到值,并且obj的类有一个__getattr__方法,那么就会调用这个方法。
下面的代码展示了这个算法在Python中的实现,实际上就是getattr()函数的功能。(不包括一些潜在的bug)
NotFound = object() # A singleton to signify not found values
def lookup_attribute(obj, attr):
class_attr_value = lookup_attr_on_class(obj, attr)
if is_data_descriptor(class_attr_value):
return invoke_descriptor(class_attr_value, obj, obj.__class__)
if attr in obj.__dict__:
instance_attr_value = obj.__dict__[attr]
if isinstance(obj, type) and is_descriptor(instance_attr_value):
return invoke_descriptor(instance_attr_value, None, obj)
return instance_attr_value
if class_attr_value is NotFound:
getattr_method = lookup_attr_on_class(obj, '__getattr__')
if getattr_method is NotFound:
raise AttributeError()
return getattr_method(obj, attr)
if is_descriptor(class_attr_value):
return invoke_descriptor(class_attr_value, obj, obj.__class__)
return class_attr_value
def lookup_attr_on_class(obj, attr):
for parent_class in obj.__class__.__mro__:
if attr in parent_class.__dict__:
return parent_class.__dict__[attr]
return NotFound
def is_descriptor(obj):
if lookup_attr_on_class(obj, '__get__') is NotFound:
return False
return True
def is_data_descriptor(obj):
if not is_descriptor(obj) or lookup_attr_on_class(obj, '__set__') is NotFound :
return False
return True
def invoke_descriptor(descriptor, obj, cls):
descriptormethod = lookup_attr_on_class(descriptor, '__get__')
return descriptormethod(descriptor, obj, cls)
你可能会问,这些描述符的东西和方法调用有什么关系呢?其实,函数也是对象,它们也实现了描述符协议。如果在类中查找到了一个函数对象,就会调用它的__get__方法,返回一个“绑定方法”对象。绑定方法就是一个小包装器,它把查找函数时用的对象存储起来,当调用时,会把这个对象加到参数列表的前面(通常情况下,对于作为方法的函数,self参数就是这个对象)。
下面是一些示例代码:
class Function(object):
def __get__(self, obj, cls):
return BoundMethod(obj, cls, self.func)
# Init and call added so that it would work as a function
# decorator if you'd like to experiment with it yourself
def __init__(self, the_actual_implementation):
self.func = the_actual_implementation
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class BoundMethod(object):
def __init__(self, obj, cls, func):
self.obj, self.cls, self.func = obj, cls, func
def __call__(self, *args, **kwargs):
if self.obj is not None:
return self.func(self.obj, *args, **kwargs)
elif isinstance(args[0], self.cls):
return self.func(*args, **kwargs)
raise TypeError("Unbound method expects an instance of %s as first arg" % self.cls)
对于方法解析顺序(在Python中其实就是属性解析顺序),Python使用的是来自Dylan的C3算法。这个算法太复杂了,不适合在这里解释,所以如果你感兴趣,可以看看这篇文章。除非你在做一些非常复杂的继承层次(其实不应该这样做),否则只需要知道查找顺序是从左到右,深度优先,并且会在查找类之前先搜索所有子类。