1. 真的能用隐式类型转换作为强弱类型的判断标准吗?
最近有些学员问我,Python到底是强类型语言,还是弱类型语言。我就直接脱口而出:Python是弱类型语言。没想到有一些学员给我了一些文章,有中文的,有英文的,都说Python是强类型语言。我就很好奇,特意仔细研究了这些文章,例如,下面就是一篇老外写的文章:
其他中文的相关文章,大家可以去网上搜,一堆,这里就不一一列举了。
我先不说这些结论对不对,我先总结一下这些文章的核心观点。这些文章将编程语言分为强类型、弱类型、动态类型和静态类型。这4个概念的解释如下:
强类型:如果一门语言不对变量的类型做隐式转换,这种编程语言就被称为强类型语言 ;
弱类型:与强类型相反,如果一门语言对变量的类型做隐式转换,那我们则称之为弱类型语言;
动态类型:如果一门语言可以在运行时改变变量的类型,那我们称之为动态类型语言;
静态类型:与动态类型相反,如果一门语言不可以在运行时改变变量的类型,则称之为静态类型语言;
其实这些概念就涉及到编程语言的两个特性:隐式类型转换和类型固化。
所谓类型固化,就是指一旦变量在初始化时被确定了某个数据类型(如整数类型),那么这个变量的数据类型将永远不会变化。
关于动态类型和静态类型,在本文的后面再来讨论,这里先探讨强类型和弱类型。
现在姑且认为这个结论没问题。强类型就是不允许做隐式类型转换。OK,我们看看用这个隐式类型转换来判断强类型和弱类型是否合理。
在这些文章中,给出了很多例子作为证据来证实这个结论,其中最典型的例子是在Python语言中,int + string是不合法的,没错,确实不合法。如执行1 + 'abc'会抛出异常。当然,还有人给出了另一个例子:string / int也是不合法的,如执行'666' / 20会抛出异常,没错,字符串与整数的确不能直接相除。那你怎么不用乘号举例呢?如'abc' * 10,这在Python中可是合法的哦,因为这个表达式会将'abc'复制10份。为何不用我大乘号来举例,难道瞧不起我大乘号吗? 这是运算符歧视?
PS:虽然'abc' * 10没有做类型转换,但这里说的是乘号(*),尽管目前Python不支持'abc' * '10'的操作,但已有也有可能会支持'abc' * '10',也就是将'10'转换为10,这就发生了类型转换。
另外,难道没听说过Python支持运算符重载吗?通过运算符重载,可以让两个类型完全不同的变量或值在一起运算,如相加,看下面的例子:
class MyClass1: def __init__(self,value): self.value = value class MyClass2: def __init__(self,value): self.value = value my1 = MyClass1(20) my2 = MyClass2(30) print( my1 + my2)
如果执行这段代码,100%会抛出异常,因为MyClass1和MyClass2肯定不能相加,但如果按下面的方式修改代码,就没问题了。
class MyClass1: def __init__(self,value): self.value = value def __add__(self,my): return self.value + my.value class MyClass2: def __init__(self,value): self.value = value def __add__(self,my): return self.value + my.value my1 = MyClass1(20) my2 = MyClass2(30) print( my1 + my2)
这段代码对MyClass1和MyClass2进行了加法运算符重载,这样两个不同类型的变量就可以直接相加了,从表面上看,好像是发生了类型转换,但其实是运算符重载。
当然,运算符重载也可能会使用显式类型转换,如下面的代码允许不同类型的值相加。
class MyClass1: def __init__(self,value): self.value = value def __add__(self,my): return str(self.value) + str(my.value) class MyClass2: def __init__(self,value): self.value = value def __add__(self,my): return str(self.value) + str(my.value) my1 = MyClass1(20) my2 = MyClass2("xyz") print( my1 + my2)
其实这段代码也就相当于int + string形式了,只是用MyClass1和MyClass2包装了一层。可能有很多同学会说,这能一样吗?明显不是int + string的形式,ok,的确是不太一样。
可惜目前Python还不支持内建类型(如int、str)的运算符重载,但不能保证以后不支持,如果以后Python要是支持内建类型运算符重载,那就意味着可以重载str类的__add__方法了,目前str类定义在builtins.py文件中,里面已经预定义了很多可能被重载的运算符。当然,目前Python是直接将这些运算符方法固化在解析器中了,例如,__add__方法是只读的,不能修改。如下面的Python代码相当于a + "ok"。
a = "abc" print( a.__add__("ok"))
但你不能用下面的代码覆盖掉str类的__add__方法。
def new_add(self, value): return str(self) + str(value) str.__add__ = new_add # 抛出异常
执行这段代码会抛出如下图的异常,也就是说,目前Python的内建类型,如str,是不能动态为其添加新的成员或覆盖以前的成员的。
但现在不能,不代表以后不能。如果以后Python支持覆盖内建类型的运算符,那么int + string就可以让其合法化。不过可能还会有同学问,就算内建类型支持运算符重载,那不还需要使用显式类型转换吗?是的,没错,需要类型转换。
现在我们先来谈谈类型转换,先用另外一个被公认的弱类型编程语言JavaScript为例。在JS中,1 + 'abc'是合法的、'444'/20也是合法的,所以就有很多人认为js是弱类型语言,没错,js的确是弱类型语言。但弱类型确实是根据1 + 'abc'和'444'/20得出来的?
有很多人认为,JavaScript不做类型检查,就直接将1和'abc'相加了!你是当真的?如果不做类型检查,那么js怎么会知道如何将1和'abc'相加,为啥不将1当做1.0呢?其实不管是什么类型的编程语言,数据类型检测都是必须的,不管是js、还是Python,或是Java,内部一定会做数据类型检测,只是检测的目的不同而已。在Python中,进行数据类型检测后,发现不合规的情况,有时会自动处理(如int+float),有时干脆就抛出异常(如int + string)。而在Java中就更严格了,在编译时,发现不合规的情况,就直接抛出编译错误了。在js中,发现不合规的情况,就会按最大可能进行处理,在内部进行类型转换。对,不是不管数据类型了,而是在内部做的数据类型转换。那么这和通过Python的运算符重载在外部做类型转换有什么区别呢?只是一个由编译器(解析器)内部处理的,一个是在外部由程序员编写代码处理的!而且就算Python不会支持内建类型的运算符重载,那么也有可能直接支持int + string的形式。因为目前Python不支持,所以正确的Python代码不可能有int + string的形式。所以如果以后支持int + string的形式,也可以完全做到代码向下兼容。就算Python未来不支持int + string形式,那么我自己做一个Python解析器(例如,我们团队现在自己做的Ori语言,支持类型隐式转换,不过实际上是生成了其他的编程语言,也就是语言之间的转换,这是不是代表Ori是弱类型语言呢?),完全兼容Python的代码,只不过支持int+string形式,那么能不能说,我的这个Python版本是弱类型Python呢?这很正常,因为像C++这种语言也有很多种实现版本,Python同样也可以拥有,只不过目前没多少人做而已,但不等于没有可能。
如果Python真这么做了,那么能不能说Python又从强类型语言变成了弱类型语言呢?如果大家认为一种语言的类型强弱是可以随着时间变化的,那么我无话可说!
总之,需要用一种确定不会变的特性来表示强弱类型才是最合适的。通常来讲,某种语言的变量一旦数据类型确定了,就不允许变化了,这种才可以称为强类型,强大到类型一言九鼎,类型一旦确定,就不允许变了,而Python显然不是,x = 20; x = 'abc';同样是合法的,x先后分别是int和str类型。
PS:这里再给大家一个表,通常编程语言中确定类型是否兼容,就是通过类似的表处理的。这个表主要用于内建类型,如果是自定义类型,需要通过接口(实现)和类(继承)类确定类型是否兼容。
这个表只给出了3个数据类型:int、float和str。根据业务不同,这个表可以有多种用途,例如,赋值,是否可以进行运算等。这里就只考虑进行加法运算。 其中True表示允许进行加法运算,False表示不允许进行加法运算,很显然,如果是int + int形式,第1个操作数可以从第1列查找,第2个操作数可以从第1行查找,找到了(1,1)的位置,该位置是True,所以int + int是合法的,int + float,float + float、str + str的情形类似,如果遇到int + str,就会找到(1,3)或(3,1),这两个位置都是False,就表明int + str是不合法的。其实Python和JavaScript都进行到了这一步。只不过Python就直接抛出了异常,而JS则尝试进行类型转换,但都需要进行类型检测。因为类型转换需要确定数据类型的优先级,优先级低的会转换为优先级高的类型,如str的优先级比int高,所以int会转换为str类型。float比int高,所以int会转换为float类型,这就涉及到另外一个类型优先级表了。
根据这个表可知,编程语言只是在遇到类型不合规的情况下处理的方式不同,这就是编译器(解析器)的业务逻辑了,这个业务逻辑随时可能变(通常不会影响程序的向下兼容),所以是不能用这一特性作为强弱语言标识的,否则强类型和弱类型语言就有可能会不断切换了,因为编程语言会不断进化的。
2. 为什么应该用类型固化作为强弱类型的标识
那么为什么可以用类型固化作为强弱类型的标识呢?因为类型固化通常是不可变的,那么为什么是不可变的呢?下面用Python来举例:
下面的Python代码是合法的。x从int变成了str,类型并没有固化,所有Python是弱类型语言。
x = 20 x = 'abc'
那么有没有可能Python以后对类型进行固化呢?从技术上来说,完全没问题,但从代码兼容性问题上,将会造成严重的后果。因为类型没固化属于宽松型,一旦类型固化,属于严格型。以前已经遗留了很多宽松的代码,一旦严格,那么就意味着x = 'abc'将会抛出异常,就会造成很多程序无法正常运行。所以如果Python这么做,就相当于一种新语言了,如PythonX,而不能再称为Python了。就像人类进化,无论从远古的尼安德特人,还是智人,或是现代各个国家的人,无论怎么进化,都需要在主线上发展,例如,都有一个脑袋,两条腿,两个胳膊。当然,可能细节不同,如黑眼睛,黄头发等。你不能进化出两个头,8条腿来,当然可以这么进化,但这个就不能再称为人了,就是另外一种生物了。
现在再看一个相反的例子,如果一种编程语言(如Java)是强类型的,能否以后变成弱类型语言呢?
看下面的Java代码:
int x = 20; x = "200"; // 出错
其实从技术上和兼容性上这么做是没问题的。但也会有很多其他问题,如编译器(或运行时)的处理方式完全不同,我们知道,类型固化的程序要比类型不固化的程序运行效率高,因为类型不固化,需要不断去考虑类型转换的问题。而且在空间分配上更麻烦,有可能会不断分配新的内存空间。例如,对于一个数组来说,js和python(就是列表)是可以动态扩容的,其实这个方式效率很低,需要用算法在合理的范围内不断分配新的内存空间,而Java不同,数组一旦分配内存空间,是不可变的,也就是空间固化(类似于类型固化),这样的运行效率非常高。
所以一旦编程语言从类型固化变成类型不固化,尽管可以保证代码的兼容性,但编译器或运行时的内部实现机理将完全改变,所以从本质上说,也是另外一种编程语言了。就像人类的进化,尽管从表面上符合人类的所有特征。但内部已经变成生化人了,已经不是血肉之躯了,这也不能称为人类了。
所以无论往哪个方向变化,都会形成另外一种全新的编程语言,所以用类型固化来作为强弱类型标识是完全没有问题的。
3. C++、Java、Kotlin是强类型语言,还是弱类型语言
我看到网上有很多文章将C++归为弱类型语言。其实,这我是头一次听说C++有人认为是弱类型语言,是因为C++支持string+int的写法吗?没错,C++是支持这种写法,但直接这么写,语法没问题,但不会得到我们期望的结果,如下面的代码:
std::cout << "Hello, World!" + 3 << std::endl;
这行代码并不会输出Hello,World!3,要想输出正常的结果,需要进行显式类型转换,代码如下:
std::cout << "Hello, World!" + std::to_string(3) << std::endl;
尽管C++编译器支持string+int的写法,但得不到我们期望的结果,所以C++的string和int相加需要进行转换。因此,仅仅通过string+int或类似的不同类型不能直接在一起运算来判断语言是否是强类型和弱类型的规则是站不住脚的。而且C++也支持运算符重载,也就意味着可以让"abc" + 4变成不合法的。
那么Java是强类型还是弱类型呢?Java是强类型语言,因为很多文章给出了下面的例子(或类似):
"666" / 4;
是的,这个表达式会出错,但你不要忘了,Java支持下面的表达式:
"666" + 4;
这行表达式输出了6664,为啥不用加号(+)举例呢?前面歧视Python的乘号,现在又歧视Java里的加号吗?其实这是因为前面描述的类型优先级问题,由于string的优先级高于int,因此4会转换为"4"。所以"666" / 4其实会也会发生隐式类型转换,变成"666"/"4",两个字符串自然不能相除了,而"666" + 4会变成"666" + "4",两个字符串当然可以相加了。这就是个语义的问题,和强弱类型有毛关系。
所以吗?Java是强类型语言没错,但判断依据错了。
Kotlin是强类型还是弱类型呢?答案是Kotlin是强类型语言。不过Kotlin支持运算符重载,看下面的代码。
class MyClass(var value: Int) { operator fun plus(other: Int): Int { return value + other; } } fun main() { var my: MyClass = MyClass(200); print(my + 20); // 输出220 }
我们都知道,Kotlin也是JVM上的一种编程语言(尽管可以生成js,但需要用Kotlin专有API),而Java是不支持运算符重载的,在同一个运行时(JVM)上,有的语言支持运算符重载,有的语言不支持运算符重载。从这一点就可以看出,运算符来处理两侧的操作数,只不过是个语法糖而已。想让他支持什么样的运算都可以,如,"abcd" / "cd",其实也可以让他合法化,例如,语义就表示去掉分子以分母为后缀的子字符串,如果没有该后缀,分子保持不变,所以,"abcd"/"cd"的结果就是"ab",而"abcd"/"xy"的结果还是"abcd",语法糖而已,与强弱类型没有半毛钱关系。
4. 静态语言和动态语言
现在来说说静态语言和动态语言。 有人说可以用是否实时(在运行时)改变变量类型判别是静态语言还是动态语言,没错,变量类型的实时改变确实是动态语言的特征之一,但并不是全部。动态语言的另一些特征是可以随时随地为类【或其他类似的语法元素】(主要是指自定义的类,有一些语言可能不支持对内建类型和系统类进行扩展)添加成员(包括方法、属性等)。
例如,下面的JavaScript代码动态为MyClass类添加了一个静态方法(method1)和一个成员方法(method2)。
class MyClass { } // 动态添加静态方法 MyClass.method1 = function () { console.log('static method'); } MyClass.method1() var my = new MyClass(); // 动态添加成员方法 my.method2 = function () { console.log('common method') } my.method2()
Python动态添加成员的方式与JavaScript类似,代码如下:
class MyClass: pass def method1(): print('static method') # 动态添加静态方法 MyClass.method1 = method1 MyClass.method1() my = MyClass() def method2(): print('common method') # 动态添加静态方法 my.method2 = method2 my.method2()
还有就是数组的动态扩容(根据一定的算法,并不是每一次调用push方法都会增加内存空间),如JavaScript的代码:
a = [] a.push("hello") a.push(20) a.push("world") console.log(a)
Python的数组(列表)扩容:
a = [] a.append('world') a.append(20) a.append("hello") print(a)
当然,动态语言还有很多特性,这里就不一一介绍了。
这些特性在静态语言(如Java、C++)中是无法做到的。在静态语言中,一个类一旦定义完,就不能再为类动态添加任何成员和移除任何成员,除非修改类的源代码。
所以说,静态和动态其实涵盖了多个方面,如类型固化,动态扩展、数组扩容等。而强类型和弱类型的特性其实只能算静态和动态的特性之一。也就是说,说一种语言是静态语言,其实已经包含了这种语言的变量类型一旦确定不可改变的事实,也就是静态语言一定是强类型的编程语言。
如果单独强调强类型,其实就相当于下面这句话:
这个人是一个男人,而且是一个男演员。
这句话看起来没毛病,也能看懂,但其实是有语病的。因为前面已经说了这个人是一个男人了,后面就没必要强调是男演员了,而只需要按下面说的即可:
这个人是一个男人,而且是一个演员。
现在来总结一下:
应该用固定不变的特性来标识一种语言的特性。而语言是否支持隐式类型转换,这只是编译器或运行时的内部业务逻辑,相当于语法糖而已,是随时可以改变的。而类型固化,动态扩展、数组扩容,这些涉及到编程语言的根本,一旦改变,就变成了另外一种语言了,所以通常用这些特性标识语言的特性。通常来讲,静态语言的效率会高于动态语言。因为,这些动态特性会让程序有更大负担,如类型不固定,就意味着可能会为新的类型分配新的内存空间,动态扩展和数组扩容也意味着不断进行边界检测和分配新的内存空间(或回收旧的内存空间)。这就是为什么C++、Java、C#等编程语言的性能要高于js、Python的主要原因。
其实过度强调静态、动态、强类型、弱类型,意义并不大。以为编程语言以后的发展方向是静态语言动态化,弱类型强类型化。都是互相渗透了,如果以后出现一种编程语言,同时拥有静态和动态的特性,其实并不稀奇。例如,尽管变量类型不允许改变,但允许动态为对象添加成员。就和光一样,既是光子(粒子),又是电磁波,也就是说光拥有波粒二象性! 编程语言也一样,也会同时拥有静动态二象性!