3

卧槽,好强大的魔法,竟能让Python支持方法重载

 3 years ago
source link: https://www.cnblogs.com/nokiaguy/p/14454143.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

卧槽,好强大的魔法,竟能让Python支持方法重载

1. 你真的了解方法重载吗?

方法重载是面向对象中一个非常重要的概念,在类中包含了成员方法和构造方法。如果类中存在多个同名,且参数(个数和类型)不同的成员方法或构造方法,那么这些成员方法或构造方法就被重载了。下面先给出一个Java的案例。

class MyOverload {
    public MyOverload() {
        System.out.println("MyOverload");
    }
    public MyOverload(int x) {
        System.out.println("MyOverload_int:" + x);
    }
    public MyOverload(long x) {
        System.out.println("MyOverload_long:" + x);
    }
    public MyOverload(String s, int x, float y, boolean flag) {
        System.out.println("MyOverload_String_int_float_boolean:" + s + x  + y + flag);
    }
}

这是一个Java类,有4个构造方法,很明显,这4个构造方法的参数个数和类型都不同。其中第2个构造方法和第3个构造方法尽管都有一个参数,但类型分别是int和long。而在Java中,整数默认被识别为int类型,如果要输入long类型的整数,需要后面加L,如20表示int类型的整数,而20L则表示long类型的整数。

如果要调用这4个构造方法,可以使用下面的代码:

new MyOverload();
new MyOverload(20);
new MyOverload(20L);
new MyOverload("hello",1,20.4f,false);

编译器会根据传入构造方法的参数值确定调用哪一个构造方法,例如,在分析new MyOverload(20)时,20被解析为int类型,所以会调用 public MyOverload(int x) {...}构造方法。

以上是Java语言中构造方法重载的定义和处理过程。Java之所以支持方法重载,是因为可以通过3个维度来确认到底使用哪一个重载形式,这3个维度是:
(1)方法名
(2)数据类型
(3)参数个数

如果这3个维度都相同,那么就会认为存在相同的构造方法,在编译时就会抛出异常。

方法的参数还有一种特殊形式,就是默认参数,也就是在定义参数时指定一个默认值,如果在调用该方法时不指定参数值,就会使用默认的参数值。

class MyClass {
    public test(int x, String s = "hello") {
        ... ...
    }
}

如果执行下面的代码,仍然是调用test方法。

new MyClass().test(20);

不过可惜的是,Java并不支持默认参数值,所以上面的形式并不能在Java中使用,如果要实现默认参数这种效果,唯一的选择就是方法重载。从另一个角度看,默认参数其实与方法重载是异曲同工的,也就是过程不同,但结果相同。所以Java并没有同时提供两种形式。

2. Python为什么在语法上不支持方法重载

首先下一个结论,Python不支持方法重载,至少在语法层次上不支持。但可以通过变通的方式来实现类似方法重载的效果。也就是说,按正常的方式不支持,但你想让他支持,那就支持。要知详情,继续看下面的内容。

我们先来看一下Python为什么不支持方法重载,前面说过,方法重载需要3个维度:方法名、数据类型和参数个数。但Python只有2个维度,那就是参数名和参数个数。所以下面的代码是没办法实现重载的。

class MyClass:
    def method(self, x,y):
        pass
    def method(self, a, b):
        pass  

在这段代码中,尽管两个method方法的形参名不同,但这些参数名在调用上无法区分,也就是说,如果使用下面的代码,Python编译器根本不清楚到底应该调用哪一个method方法。

MyClass().method(20, "hello")

由于Python是动态语言,所以变量的类型随时可能改变,因此,x、y、a、b可能是任何类型,所以就不能确定,20到底是x或a了。

不过Python有参数注解,也就是说,可以在参数后面标注数据类型,那么是不是可以利用这个注解实现方法重载呢?看下面的代码:

class MyClass:
    def method(self, x: int):
        print('int:', x)
    def method(self, x: str):
        print('str:',x)

MyClass().method(20)        
MyClass().method("hello")  

在这段代码中,两个method方法的x参数分别使用了int注解和str注解标注为整数类型和字符串类型。并且在调用时分别传入了20和hello。不过输出的却是如下内容:

str: 20
str: hello

这很显然都是调用了第2个method方法。那么这是怎么回事呢?

其实Python的类,就相当于一个字典,key是类的成员标识,value就是成员本身。不过可惜的是,在默认情况下,Python只会用成员名作为key,这样以来,两个method方法的key是相同的,都是method。Python会从头扫描所有的方法,遇到一个方法,就会将这个方法添加到类维护的字典中。这就会导致后一个方法会覆盖前一个同名的方法,所以MyClass类最后就剩下一个method方法了,也就是最后定义的method方法。所以就会输出前面的结果。也就是说,参数注解并不能实现方法的重载。

另外,要注意一点,参数注解也只是一个标注而已,与注释差不多。并不会影响传入参数的值。也就是说,将一个参数标注为int,也可以传入其他类型的值,如字符串类型。这个标注一般用作元数据,也就是给程序进行二次加工用的。

3. 用黑魔法让Python支持方法重载

既然Python默认不支持方法重载,那么有没有什么机制让Python支持方法重载呢?答案是:yes。

Python中有一种机制,叫魔法(magic)方法,也就是方法名前后各有两个下划线(_)的方法。如__setitem__、__call__等。通过这些方法,可以干预类的整个生命周期。

先说一下实现原理。在前面提到,类默认会以方法名作为key,将方法本身作为value,保存在类维护的字典中。其实这里可以做一个变通,只要利用魔法方法,将key改成方法名与类型的融合体,那么就可以区分具体的方法了。

这里的核心魔法方法是__setitem__,该方法在Python解析器没扫描到一个方法时调用,用于将方法保存在字典中。该方法有两个参数:key和value。key默认就是方法名,value是方法对象。我们只要改变这个key,将其变成方法名和类型的组合,就能达到我们的要求。

我们采用的方案是创建一个MultiMethod类,用于保存同名方法的所有实例,而key不变,仍然是方法名,只是value不再是方法对象,而是MultiMethod对象。然后MultiMethod内部维护一个字典,key是同名方法的类型组成的元组,value是对应的方法对象。

另外一个核心魔法方法是__call__,该方法在调用对象方法时被调用,可以在该方法中扫描调用时传入的值参的类型,然后将参数类型转换成元组,再到MultiMethod类维护的字典中搜索具体的方法实例,并在__call__方法中调用该方法实例,最后返回执行结果。

现在给出完整的实现代码:

import inspect
import types

class MultiMethod:

    def __init__(self, name):
        self._methods = {}
        self.__name__ = name

    def register(self, meth):
        '''
        根据方法参数类型注册一个新方法
        '''
        sig = inspect.signature(meth)

        # 用于保存方法参数的类型
        types = []
        for name, parm in sig.parameters.items():
            # 忽略self
            if name == 'self':
                continue
            if parm.annotation is inspect.Parameter.empty:
                raise TypeError(
                    '参数 {} 必须使用类型注释'.format(name)
                )
            if not isinstance(parm.annotation, type):
                raise TypeError(
                    '参数 {} 的注解必须是数据类型'.format(name)
                )
            if parm.default is not inspect.Parameter.empty:
                self._methods[tuple(types)] = meth
            types.append(parm.annotation)

        self._methods[tuple(types)] = meth
    # 当调用MyOverload类中的某个方法时,会执行__call__方法,在该方法中通过参数类型注解检测具体的方法实例,然后调用并返回执行结果
    def __call__(self, *args):
        '''
        使用新的标识表用方法
        '''
        types = tuple(type(arg) for arg in args[1:])
        meth = self._methods.get(types, None)
        if meth:
            return meth(*args)
        else:
            raise TypeError('No matching method for types {}'.format(types))

    def __get__(self, instance, cls):
        if instance is not None:
            return types.MethodType(self, instance)
        else:
            return self

class MultiDict(dict):
    def __setitem__(self, key, value):
        if key in self:
            # 如果key存在, 一定是MultiMethod类型或可调用的方法
            current_value = self[key]
            if isinstance(current_value, MultiMethod):
                current_value.register(value)
            else:
                mvalue = MultiMethod(key)
                mvalue.register(current_value)
                mvalue.register(value)
                super().__setitem__(key, mvalue)
        else:
            super().__setitem__(key, value)


class MultipleMeta(type):

    def __new__(cls, clsname, bases, clsdict):
        return type.__new__(cls, clsname, bases, dict(clsdict))

    @classmethod
    def __prepare__(cls, clsname, bases):
        return MultiDict()
# 任何类只要使用MultileMeta,就可以支持方法重载
class MyOverload(metaclass=MultipleMeta):
    def __init__(self):
        print("MyOverload")

    def __init__(self, x: int):
        print("MyOverload_int:", x)

    def bar(self, x: int, y:int):
        print('Bar 1:', x,y)

    def bar(self, s:str, n:int):
        print('Bar 2:', s, n)
    def foo(self, s:int, n:int):
        print('foo:', s, n)

    def foo(self, s: str, n: int):
        print('foo:', s, n)
    def foo(self, s: str, n: int, xx:float):
        print('foo:', s, n)
    def foo(self, s: str, n: int, xx:float,hy:float):
        print('foo:', s, n)

my = MyOverload(20)   # 调用的是第2个构造方法
my.bar(2, 3)
my.bar('hello',20)
my.foo(2, 3)
my.foo('hello',20)

运行程序,会输出如下的运行结果:

MyOverload_int: 20
Bar 1: 2 3
Bar 2: hello 20
foo: 2 3
foo: hello 20

很显然,构造方法、Bar方法和foo方法都成功重载了。以后如果要让一个类可以重载方法,可以直接使用MultipleMeta类(通过metaclass指定)。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK