python风格——动态类型

python风格

想要写出python风格的代码,就得理解python的特点,合理的应用python所带来的东西。
python是一门动态类型的语言,这是由python的设计思想所决定的。在python中,我们编写对象接口而不是类型。我们关心的是一个对象能做什么,而不是关心它是什么。它是什么并不重要,重要的是它能做什么?我们希望代码能自动的适应非常多的类型,任何具有兼容性的接口对象能够正常工作。实际上这就是多态(多态:指为不同数据类型的实体提供统一的接口),这也是使用python的核心思想之一。

动态语言

既然我们只关心只它能做什么,那么它是什么就没有那么的重要了。因此将python设计为一门动态语言就非常合理。
动态语言程序运行时,允许改变程序结构(例如引进新函数、删除旧函数)或变量类型。动态语言中的变量本身是没有类型的,但是变量所绑定的值是有类型的,但是这个类型检查是发生在运行期的。
在python中,是没有类型声明的,直接给变量绑定值即可。例如:
s = "123"
一个有编写过静态类型语言经验的程序员,可能会想着如何在python判断数据类型是什么?但是这样的思路在使用python的时候是不合适的,在代码中检测数据类型,实际上会破坏python的灵活性。虽然python中的type()函数提供了类型判断的功能,但是不建议使用。下面看一个例子,它就体现了python的灵活性。

s = "123"
num = 123

print(type(s))
print(type(num))

s = 123
num = "123"
print(type(s))
print(type(num))

程序执行结果如下:

<class 'str'>
<class 'int'>
<class 'int'>
<class 'str'>

变量,对象和表达式

作为一名有经验的C程序员,在编写python程序的时候,要习惯python中变量不一样的地方。

  1. 变量在第一次赋值的时候会被创建出来;
  2. 变量在使用之前必须被赋值(必须存在这个变量);
  3. 变量引用的是对象;
  4. 变量没有数据类型,有数据类型的是对象。
  5. 变量在表达式中出现的时候,它会被其所引用的对象的值所取代。

总结来说,python里的变量实际上就是一个void *指针(通用类型指针),这个指针指向的是对象。只不过我们在使用的时候不需要解引用。而且这个指针指向的对象还可以改变。(这和C++的引用是完全不同的)
对象知道自己的类型,每个对象都包含一个头部信息,其中的类型标志符标记了这个对象的类型,其中的引用计数器决定何时回收这个对象。

垃圾回收

在python中,每当一个变量名被赋予一个新对象,如果原来的对象没有被其他变量或者对象引用,那么之前的那个对象所占用的内存空间就会被回收。这种自动回收对象空间的技术叫作垃圾回收。得益于垃圾回收机制,python程序员无需手动管理内存,这使得python程序员的工作得到了很大的简化。
在python的实现中,每个对象的头部都有一个引用计数器,它记录了该对象的引用数目,一旦计数器被设为0,那么这个对象的内存空间就会被回收。
在python这里有个问题是集合这样的数据类型本身是一个容器,他可以容纳任何数据类型。所有就会有下面这样的代码出现。

>>> a = [1,2,3]
>>> a.append(a)

这个时候,循环引用就出现了,这样的对象的引用计数器不会为0,必须进行特别处理。
不过这点不用我们担心,python默认是可以回收循环引用的对象。

共享引用

共享引用在python里是指多个变量引用了同一个对象。在看例子之前,我们首先介绍一个函数id()。
id()返回的是对象的“身份证号”,唯一且不变。一个对象的id值在CPython解释器里就代表它在内存中的地址。
下面,我们来看一个例子:

>>> a = 1
>>> b = 1
>>> id(a)
9788992
>>> id(b)
9788992

这个例子中,变量a和变量b所引用的对象的id是一致的,这就是共享引用。注意,a和b本身并没有什么关联。当然了,在python里变量之间是不可能有引用关系的。下面改变b变量所引用的对象,效果如下:

>>> b = '123'
>>> id(a)
9788992
>>> id(b)
140352439347888

这就是python的特殊之处,变量本身不是某个内存地址的标签,变量本身应当是类似void *类型的指针。给变量赋新值,就是改变变量所指向的对象,而不是改变原来对象。事实上,刚才的b指向的对象3是整形,而整形是不可变类型,你根本没有办法改变它。
需要注意的是对可变对象的共享引用,这可能会造成你预期之外的结果。例如:

>>> list1 = [1,2,3]
>>> list2 = list1
>>> id(list1)
140352439347392
>>> id(list2)
140352439347392
>>> list2[0] = 0
>>> list1
[0, 2, 3]
>>> list2
[0, 2, 3]
>>> id(list2)
140352439347392
>>> id(list1)
140352439347392

可以看到,list1和list2共享引用,但是由于列表是可变对象,可以改变其中的值。但是这并没有改变它们是共享引用。所以,list1和list2的结果都发生了改变。这种效果对于习惯了C/C++编程的人而言,一开始是不太习惯的,经历过几次这样的错误就好了。如果你真的想让list1和list2指向的对象不同,那么你可以使用复制对象来解决这个问题。例如:

>>> list1 = [1,2,3]
>>> list2 = list1.copy()
>>> id(list2)
140352438351616
>>> id(list1)
140352439347392

这样做了对象复制之后,list1和list2就会指向不同的对象。复制一个对象还可以将原来的对象传入相应的构造函数中,例如:

>>> list1 = [1,2,3]
>>> list2 = list(list1)
>>> id(list1)
140352438324288
>>> id(list2)
140352438560704

字典和列表也可以使用copy或者构造函数方式来复制原来的对象。复制一个对象方法有很多,这不是重点,重点是python的可变对象的共享引用是较为特殊,尤其是对习惯了C/C++的人而言。

共享引用和相等

首先,python解释器有时候并不会马上回收一个不再使用的对象,python会缓存并复用小的整数和小的字符串,缓存机制并不会影响代码。但是大多数的对象都是在不被引用的时候立即回收的。
基于python的引用模型,python中有两种方法去检测是否相等。例如:

>>> list1 = [1,2,3]
>>> list2 = list1
>>> list1 == list2    #比较值是否相等
True
>>> list1 is list2    #检测是否是同一个对象
True

在这里,由于list1和list2引用的是同一个对象,所以值是相同的。下面的另一个例子则说明了不同之处。

>>> list1 = [1,2,3]
>>> list2 = [1,2,3]
>>> list1 == list2    #比较值是否相等
True
>>> list1 is list2    #检测是否是同一个对象
False

这个例子中,list1和list2引用的对象是不同的(因为python只会缓存并复用小的整数和小的字符串,列表并不会被缓存),但是两个对象的值相同的。最后,我们再来一个说明缓存效果会带来的不同之处。

>>> a = 1
>>> b = 1
>>> a == b
True
>>> a is b
True
  • ==:测试两个被引用的对象是否具有相同的值
  • is:测试两个变量是否引用的是同一个对象

因此,is可以作为测试共享应用的一种方式。
python中sys模块中的getrefcount()方法可以返回对象的引用次数,以数字对象1为例,其返回的引用次数高达199次。

>>> import sys
>>> sys.getrefcount(1)
199

这种方式也是python为了其执行速度而采用的众多优化方式中的一种。

python的这个引用,赋值模型是唯一的,它具有良好的一致性。作为比较对象的C++语言,它的语法一致性奇差。

上一篇:使用lambda表达式对两个list并集、去重并集、交集、差集处理


下一篇:惠普易商,让中小企业服务器自主应用成为可能