一个黑魔法,竟能让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):passdef 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__方法中調用該方法實例,最后返回執行結果。
現在給出完整的實現代碼:
''' 遇到問題沒人解答?小編創建了一個Python學習交流QQ群:778463939 尋找有志同道合的小伙伴,互幫互助,群里還有不錯的視頻學習教程和PDF電子書! ''' import inspect import typesclass MultiMethod:def __init__(self, name):self._methods = {}self.__name__ = namedef register(self, meth):'''根據方法參數類型注冊一個新方法'''sig = inspect.signature(meth)# 用于保存方法參數的類型types = []for name, parm in sig.parameters.items():# 忽略selfif name == 'self':continueif 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)] = methtypes.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 selfclass 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))@classmethoddef __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指定)。
總結
以上是生活随笔為你收集整理的一个黑魔法,竟能让Python支持方法重载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 弃繁就简!一行代码搞定 Python 日
- 下一篇: Python基础教程:3个方面理解Pyt