动态语言
在动态类型语言去掉类型二字,就成了动态语言,比方说不少地方都说python是动态语言,其实只是它的动态性比较多一些而已,很多所谓的静态语言也有动态性,只是没有那么明显,因此动态语言是一个很含糊的词,一般而言就是随主流的认识,比如python,ruby是动态语言,C,java是静态语言等
动态性不适合修饰语言,适合修饰一些语法特性,简单的说,静态就是编译期可以决定的一些事情,而动态就是运行期才能决定的事情,比如C++,普通函数调用是编译期决定得,虚函数的调用则是运行期决定,编译期只能输出一段查表的代码,具体查到哪个函数来调用,是根据运行时情况来决定,另外还有java的基类向派生类做类型转换、instanceof等,不过,这些操作不是很费时。python的动态性就更加灵活,比如动态类型,实时增减变量和属性等
很多动态语言的动态体现在,绝大多数机制都是在运行期实现,对编译期来说只是简单得把代码转换成字节码,字节码也只是描述“做什么”而不是非常具体的“怎么做”,举一个python的例子:
class A:
pass
A().f()
这当然会报错,因为A类没有f方法,但是我们可以再这样:
class B:
def f(self):
print self
A.__bases__ = (B,)
A().f()
指定B为A的基类,这样一来,最后调用f就成功了,而且打印出的是A()这个对象。这个例子的表现在C++和java中是不可想象的,因为后者都是必须现有基类,然后才从基类派生出子类,而python则显示了面向对象更本质的东西,一个子类不一定要依赖基类的存在,实际上它只是想以自己为参数去调用基类的接口而已(或访问数据,不过python的实现中,对象数据部分是子类对象自己的,但能访问基类的静态数据,实际就是类对象的属性),因此,子类可以先于基类存在,然后等有了基类后,再认爹也不迟(后续还可以随时换爹),子类对象调用方法时候,先在自己这里找,找不到就去基类找,再找不到就去基类的基类(如果有的话),最终还找不到,就只好报错了。实际上C++和java在也干这个事情,只不过是静态的,找不到会编译报错
因此可以认为,动态性就是把要编译期做的事情,放在运行期做,实现更强的灵活性,这样所付出的代价就是效率问题,上面这个找方法的就比用虚表慢很多了,因为python中的属性,是用名字和值对应的dict来存的,比如我们可以这样:
class A:
pass
a = A() #{}
a.a = 123 #{"a" : 123}
a.b = 456 #{"a" : 123, "b" : 456}
del a.a #{"b" : 456}
动态给对象增减属性,实现机制就是注释写的,a对象自己维护了一个dict,key就是变量名,value是变量的值,这样一来,我们就可以实时地根据变量名字符串来找到对应的值,比如getattr(a, "cc"),相当于a.cc。所以在python里面对于对象属性访问,就是拿个字符串去hash表里面查,查不到的话就报错,自然这里涉及字符串hash和比较了
引申一个问题,字符串比较最普通的算法很多人都会写,大概是这个样子(C代码):
for (int i = 0; a[i] || b[i]; ++ i)
{
if (a[i] != b[i])
{
return a[i] - b[i];
}
}
return 0;
现在问,这个算法的时间复杂度是多少
我问过不少人这个问题,80%以上的都说O(N),这个答案算不上错,但也不全对,事实上,在最坏情况下是O(N),平均则是O(1)!平均情况的证明如下: 假设有a和b两个字符串,每个字符的取值可能是c,则随机输入下,对于上述算法过程中某一对字符而言,只有1/c的概率是相等的,会进入下一次循环,有(c-1)/c的概率是直接返回,因此比较一次就返回的概率是(c-1)/c,比较两次返回的是(c-1)/c^2……比较k次返回的概率是(c-1)/c^k,这是个收敛级数,因此比较次数平均趋向一个常数,c/(c-1) 还可以从另一个角度来看,假设俩字符串随机且长度无穷,设比较次数的期望值为E,则若第一对字符相等,从第二对字符开始的比较次数期望也是E,有方程E=(c-1)/c+(E+1)/c,解得E=c/(c-1)
不过回到python的变量名查找这个问题,如果代码没有bug,那就应了最坏情况,因为几乎每次属性访问的时候,属性都存在的,python在这里做了两点优化: 一,字符串的hash值只计算一次,保存在字符串对象的头结构里面,这样省去了每次反复计算,因为python的字符串是不可变对象,所以可以这么做 二,属性相关的字符串有intern机制,简单说就是维护一个字符串表,对有些字符串,保证其全局唯一,这样一来就不用strcmp了,直接比较两个字符串对象的地址即可,地址一样则内容肯定一样,因为就是一块内存,当然地址不同内容不一定不同,所以实际是先比较地址,地址不同了再调用strcmp,运行时再通过动态修改,逐步加速执行速度,如果代码本身动态性不强(少用getattr这类),则几乎用不到strcmp 不过,这样做还是有一个hash表操作,速度上还是不如静态类型语言了
另外需要特殊说明的是局部变量,全局变量因为可以看做是模块的属性,实现方式和上面说的相同,而局部变量是做了进一步优化的,因为对于一个函数来说,不能从外部直接引用其局部变量,所以,内部都优化成tuple了,这是一个特例,编译的时候可以区分开来。并不是在用到的时候根据实际情况来找名字,比如:
i = 456
def f():
print i
i = 123
print i
第一个print会执行出错,因为这个i是局部变量,这时候还没赋值,并不是像很多初学者想的那样先打印全局的i再打印局部,这里为了效率牺牲了动态性(或者作者认为这种动态性没有必要)