Python 中的方法解析和调用是如何工作的?

6 投票
3 回答
1538 浏览
提问于 2025-04-15 11:32

在Python中,方法是怎么被调用的呢?也就是说,Python虚拟机是怎么理解这些调用的。

确实,Python的方法解析速度可能比Java要慢。这种现象被称为“晚绑定”。

这两种语言在反射机制上有什么不同呢?有没有好的资源可以帮助解释这些方面的内容?

3 个回答

1

确实,Python的方法解析速度可能比Java慢。那么,什么是晚绑定呢?

晚绑定是指一种策略,用于描述某种语言的解释器或编译器如何决定将一个标识符(比如变量名)映射到一段代码上。举个例子,在C#中写obj.Foo()时,当你编译这段代码,编译器会尝试找到这个对象,并插入一个指向将在运行时调用的Foo方法位置的引用。所有这些方法解析都是在编译时进行的;我们称这种方式为“早绑定”。

而Python则是“晚绑定”。方法解析发生在运行时:解释器会尝试找到具有正确签名的Foo方法,如果找不到,就会出现运行时错误。

这两种语言的反射机制有什么不同?

动态语言通常比静态语言有更好的反射功能,而Python在这方面非常强大。不过,Java也有相当全面的方法来访问类和方法的内部信息。尽管如此,你还是无法避免Java的冗长;在Java中完成同样的事情,你需要写更多的代码,而在Python中则相对简单。可以查看java.lang.reflect API。

4

在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操作都是在查找命名空间。

8

在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算法。这个算法太复杂了,不适合在这里解释,所以如果你感兴趣,可以看看这篇文章。除非你在做一些非常复杂的继承层次(其实不应该这样做),否则只需要知道查找顺序是从左到右,深度优先,并且会在查找类之前先搜索所有子类。

撰写回答