1. 程式人生 > >臥槽,好強大的魔法,竟能讓Python支援方法過載

臥槽,好強大的魔法,竟能讓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指定)。