卧槽,好强大的魔法,竟能让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指定)。

卧槽,好强大的魔法,竟能让Python支持方法重载的更多相关文章
- 强大的flash头像上传插件(支持旋转、拖拽、剪裁、生成缩略图等)
今天介绍的这款flash上传头像功能非常强大,支持php,asp,jsp,asp.net 调用 头像剪裁,预览组件插件. 本组件需要安装Flash Player后才可使用,请从http://dl.pc ...
- MSSQLSERVER数据库- LEFT JOIN后面跟着WHERE竟变成内联 解决方法
在LEFT JOIN后面跟着WHERE竟变成内联.百度后看到这个解决方法. 记录如下: select sre.*, co.description from subscribedratingelemen ...
- spring+mybatis的优缺点
mybatis的优缺点: 优点: 1. 易于上手和掌握. 2. sql写在xml里,便于统一管理和优化. 3. 解除sql与程序代码的耦合. 4. 提供映射标签,支持对象与数据库的orm字段关系映射 ...
- Velocity 语法(转)
一.基本语法 1."#"用来标识Velocity的脚本语句,包括#set.#if .#else.#end.#foreach.#end.#iinclude.#parse.#macro ...
- 【转】Velocity 语法
一.基本语法 1."#"用来标识Velocity的脚本语句,包括#set.#if .#else.#end.#foreach.#end.#iinclude.#parse.#macro ...
- Python中的Class的讨论
尽管Python在Function Programming中有着其他语言难以企及的的优势,但是我们也不要忘了Python也是一门OO语言哦.因此我们关注Python在FP上的优势的同时,还得了解一下P ...
- robotframework笔记22
创建测试库 支持的编程语言 机器人框架本身是用写的 Python 和自然的测试 库扩展它可以使用相同的实现 语言. 运行时框架上 Jython ,图书馆也可以 实现使用 Java . 纯Python代 ...
- mybatis知识点
1.Mybatis比IBatis比较大的几个改进是什么 a.有接口绑定,包括注解绑定sql和xml绑定Sql , b.动态sql由原来的节点配置变成OGNL表达式, c. 在一对一,一对多的时候引进了 ...
- 配置mybatis流程
使用mybatis的优点: 1. 易于上手和掌握. 2. sql写在xml里,便于统一管理和优化. 3. 解除sql与程序代码的耦合. 4. 提供映射标签,支持对象与数据库的orm字段关系映射 5. ...
随机推荐
- 枚举--让盗版美国总统wcc给你整明白哈哈
1.为什么要有枚举 Java中的枚举其实是一种语法糖,在 JDK 1.5之后出现,用来表示固定且有限个的对象.比如一个季节类有春.夏.秋.冬四个对象:一个星期有星期一到星期日七个对象.这些明显都是固定 ...
- pandas中的遍历方式速度对比
对一个20667行的xlsx文件进行遍历测试 import pandas as pd # 定义一个计算执行时间的函数作装饰器,传入参数为装饰的函数或方法 def print_execute_time( ...
- 《C++ Primer Plus》啃书计 第1~4章
<C++ Primer Plus>啃书计 第1~4章 第一章 预备知识 1.1-1.3略过 1.4 程序创建的技巧 1. cfront,它将C++源代码翻译成C源代码,然后再使用标准C编译 ...
- 2019牛客暑期多校训练营(第八场)B-Beauty Values(期望线性性)
>传送门< 题意:思路:期望的线性性(可加性),比赛的时候写的代码超级无敌长,不过值得欣慰的是一发AC了,官方的题解写的还不错~ 我们可以把每种数字对答案的贡献分开来计算,即枚举每个数字, ...
- python爬虫模板 - 最好大学网
import requests from bs4 import BeautifulSoup import bs4 def get_html_text(url): try: #kv = {'user-a ...
- CodeForces833 B. The Bakery 线段树维护dp
题目链接:https://vjudge.net/problem/CodeForces-833B 题意:给长度为n的数组a,和一个整数k要求把数组分成连续的k段,每段的权值是该段中不同数的个数,输出最大 ...
- Chapter Zero 0.1.3 其他单元设备以及运作流程
其他单元设备 五大单元中的控制单元.算数逻辑段元都被整合到CPU的封装中, 但其实系统单元中,不止有CPU(控制单元.算数逻辑单元), 计算机单元还有哪些? 系统单元:系统单元包括CPU.主存储器(内 ...
- K8S(06)web管理方式-dashboard
K8S的web管理方式-dashboard 目录 K8S的web管理方式-dashboard 1 部署dashboard 1.1 获取dashboard镜像 1.1.1 获取1.8.3版本的dsash ...
- Django的settings配置文件
一.邮件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.qq.com' EMAI ...
- vs2019 写入访问权限冲突
先说句题外话 vs反应有时候有点慢,改过的地方等几秒才会显示正确 另外有时候正确的地方会报错,重启吧 回到正题 "引发了异常: 写入访问权限冲突._Left 是 0xCDCDCDCD.如有适 ...