基础数据类型

写在前面

我们在之前,不论是本课程,还是《FirstWeb》,反复在强调 函数也是变量 此类的概念。
不但如此,一个 Class 是变量,一个 Module 是变量,我们在代码世界里看到的,都是变量。
在 Python 的世界中,把 变量 换成 对象,也是一样的理解,就把这两个名词当做一个泛指性质的吧,万物皆对象。两个名字如果一定说区别的话,变量 就像是给 对象 取的名字,但反正是同一个东西。

那么,问题出现了,变量 从语义上应该如何定义呢?感觉太混淆了:

  1. 我们说把某个参数传入某个函数,这很好理解;
  2. 传参的过程中,如果说 这个变量,其实是指传入的参数,而不会指 函数
  3. 但用 这个变量 的描述其实是错误的,因为似乎默认不把 函数 视为一种变量了。

万物皆对象(变量)的前提下,那从语文的角度,怎么区分 一般性变量函数 这类特殊变量呢?
我个人认为,不用区分这么清楚,知道各自的描述就可以了。语文,本身就不是非常精准的,何必强求呢?
不『强求』的前提,是保持思维的开阔性。认为 函数 并不是 变量,甚至觉得把 Module 都作为一种 变量很匪夷所思,反倒就画地为牢了。要知道,把一个 函数 作为参数传入到另外一个 函数,是一种常规做法呀!

本节内容,主要讲的是 Python 中的 基础数据类型,也是上文中提到的一般性变量 (也就是大家想当然认为是变量的变量)。 它们非常重要,是代码在运行过程中,数据流淌过程中的基本载体和容器。
技术世界中,有些会违背我们人类的自然认知,在基础数据类型中,也会存在这种现象。有些细节需要特别注意,在没有真正开始代码之前,把基础的基础多巩固一些,未来可以少犯一点错误。

dir

在开始之前,我们要先介绍 dir 这个函数,它是 Python 自省机制的一部分 (课程后面会更详细的介绍)。dir(一个对象),可以知道这个对象的子属性,子属性可能是一个普通的变量,或者是一个函数的调用。
dir 对于一个初学者来说,太重要了!我们不可能在课程里事无巨细地介绍每个数据类型、每个 Class(类) 的具体使用办法,那么,如何获得基本的帮助呢?除了搜索引擎,更好的办法就是代码本身。一方面可以使用 PyCharm 的自动补全,尝试 hit 到某个属性,然后再去看看源码 (参考之前介绍的『如何 Debug』),了解这个属性 (可能是个函数) 的意义或者用法;另外则是直接遍历当前某个对象的所有属性,找到自己可能需要的,再进一步了解。

比如 1 作为整数,我们想了解它可以有那些操作,就直接 dir(1) 获取,参考下面的代码:

>>> dir(1)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

注: __xx__ 这种类似的属性,叫 Magic Methods (魔术方法),在课程的后续也会有进一步的介绍。一般情况下,比如使用 dir,并不需要关注这些 Magic Methods。
新手时,Python 交互环境下,help 跟 dir 一样,也是蛮有用的函数,键盘按 q 可退出 help 状态。

type

type 是 Python 内置的一个函数,可以查看当前变量的类型。

>>> type(1)
<type 'int'>
>>> type(True)
<type 'bool'>
>>> type(1.0)
<type 'float'>
>>> type(.123)
<type 'float'>
>>> type([])
<type 'list'>

type 返回的类型本身也是一个变量,有些类型还可被调用 (实际上是实例化),用于数据格式化:

>>> int
<type 'int'>
>>> type(1)
<type 'int'>
>>> int(1.05)
1
>>> type(1)(1.05)
1

上面的 type(1)(1.05) 相当于 type(1) --> int,然后再 int(1.05)

None

None 自成一个数据类型,表示空值。因为语文的模糊性,空值 有时可能指 '' 这样的空字符串 (length 为 0 的字符串)。但 None 这个空值是比较特殊的,相当于 ,比如一个函数运行的最后,代码中没有任何返回值,但实际上是返回了值,这个值就是 None,可以说,这个概念是一个空字符串无法表达的。

bool (True/False)

bool 翻译成中文,叫 布尔型,表示 。一般一个条件判断之后,返回 bool 类型,另外,也可以通过这个 type 本身进行转化,比如 00.0空字符 这些最终都会转为 False,其它则为 True 。

>>> 1 == 1.0
True
>>> 1 != 2 !=3
True
>>> 1 in [1, 2, 3, 4, 5, 6]
True
>>> bool(1)
True
>>> bool(0)
False
>>> bool(''), bool('hello')
(False, True)

int & float

int 是整数类型,float 是浮点类型(也就是带小数点的)。float 类型,如果是零点几,比如 0.1,有时赋值的时候,直接省略写成 .1 也是可以的。
在 Python 2 中,如果 int 和 float 两种类型的数据运算 (加减乘除) 后,得到的会是 float 类型数据。但 int 和 int 类型运算后,得到的仍然是 int。Python 3 中虽然略有不同,但都要注意不同类型数据,共同参与运算后,最终的数据是什么类型的,不然,可能会导致结果产生巨大误差。
比如:

>>> 5/3
1

5 除以 3,得到的结果竟然是 1,这个完全无法理解。按照这个运算结果,很容易出现最终的错误。但这个又遵循了 int、int 的运算,得到 int 的结果。
而在 Python 3 中,则是下面更容易理解结果。如果要达到 int+int -> int 的逻辑,运算符可以改为 // (取):

>>> 5/3
1.6666666666666667
>>> 5//3
1

另外,float 是有精度的概念的,但一般情况下,我们不用去管它,只要接受 1.0000000000000001 == 1 的现实就好,并且还要避免使用这种特性 (因为不可控),其它精度的示例:

>>> 5.6 * 0.2
1.1199999999999999
>>> 5.6 * 0.2 == 1.12
False
>>> 56 * 0.2
11.200000000000001
>>> 56 * 0.2 == 11.2
False
>>> 56 * 0.2 * 10 == 112
False

unicode & str

在更早期的 Python 1 的年代,中文的编码,说起来都是一脑袋的头痛。但因为时间真的太久,具体什么问题也都忘记了,只是记得当年的头痛。
字符串 也是 Python 中非常基本的数据类型,在 Python 2 中,字符串的类型有两种,一个是 unicode,一个是 str。str 相当更加原始的内容,而 unicode 则是文本倾向很明显的,比如一张图片,它的二进制内容,可以是 str 的类型,但不可能是 unicode。
在 Python 中,一个 unicode 字符的长度是 1,而 str 则取决于原始的编码,比如下面的代码,默认的 str 文本使用的是 utf8 的编码,一个中文是 3 个字节长度,后面转为 gbk 的编码,则是 2 个字节长度。当然,一个普通的字母,比如 len('a'),其结果肯定是 1。

>>> u'中文'
u'\u4e2d\u6587'
>>> str('中文')
'\xe4\xb8\xad\xe6\x96\x87'
>>> len(u'中文')
2
>>> len('中文')
6
>>> len(u'中文'.encode('gbk'))
4

在 Python 3 中,字符串则有 str、bytes 两种类型。相当于 Python 2 的 unicode 等同于 Python 3 的 str,而 Python 2 的 str 等同于 Python 3 的 bytes。这样处理肯定是更好的,Python 2 里还要因为 str、unicode 的转化不够自动,编码上报错的概率会高很多。
但不要忘了,本质还是一样的。不要误以为 Python 3 的进步,让我们在两种性质不一样的字符串数据类型的使用上 ,就可以为所欲为了。

再有,就是字符串的一些常用命令:

  • strip: 头尾去空(默认也包括 \t\n\r 这些字符),也可传入参数,'1abcd221'.strip('1a2') 会获得 bcd 的结果
  • lstrip: 左边去空
  • rstrip: 右边去空
  • split: 根据传入的字符串,分解为 list,比如 '1,2'.split(',') 会获得 ['1', '2']
  • rsplit: 从右边开始 split,同 split 一样可接受第二参数,表示最多 split 多少次
  • upper: 大写化
  • lower: 小写化
  • title: 首字母大写
  • endswith: 是否以 传入的字符串 结尾
  • startswith: 是否以 传入的字符串 开头
  • 与 in 配合,判断字符串是否在: 比如 'a' in 'helloabc' 会获得 True
  • .etc

list

list 翻译为 列表,由很多元素组成,每个元素的类型是任意的。
以 range 函数为例,range 可以形成整数的数组,range(n) 等同于 range(0, n),可以 range(n1, n2),获得 n1 ~ n2 之间的数组。因为 range(n1, n2) 表示一个函数调用,从文本形式的角度来看,更确切的描述应该类似于 range[n1, n2) ,相当于包括 n1,但是不包括 n2 (至于为何 range 是有头没尾,以后你应该会明白的,代码中实际用起来确实方便很多)。
range 函数 (在 Python 3 中是一个 class 了) 从 Python 的源代码 (也就是 C 语言写的) 来看,不论 Python 2 还是 Python 3,注释里的意思,range 的返回值是不可变的,应该类似 tuple 的类型,但是 Python 2 中的实际效果,却是 list 类型,而 Python 3 中则是一个特定的迭代器对象。且不管它,先把它当做一个临时的 list 来看待就好。

>>> range(5)
[0, 1, 2, 3, 4]
>>> range(0, 5)
[0, 1, 2, 3, 4]

一个 list,通常会跟 for 配合,形成循环:

a_list = range(0, 5)
b_list = []
for i in a_list:
    b_list.append(i)

list 的构建,也能简化为单行:

>>> a_list = range(0, 5)
>>> b_list = [i for i in a_list]
>>> c_list = [i**2 for i in a_list]
>>> b_list
[0, 1, 2, 3, 4]
>>> c_list
[0, 1, 4, 9, 16]

一个 list 有自己的子命令,比如 reverse (倒排),sort (排序,默认从小到大),但这些命令的调用,只是改变了当前 list 内的元素排序,是不返回结果的 (也就相当于返回了 None)。

>>> result = c_list.reverse()
>>> print(result)
None
>>> c_list
[16, 9, 4, 1, 0]
>>> c_list.sort()
>>> c_list
[0, 1, 4, 9, 16]

另外,一个字符串,使用 list 的话,会把字符串逐个分解为 list,比如:

>>> list('abcdefg')
['a', 'b', 'c', 'd', 'e', 'f', 'g']

在 list 中如何取值呢?参考如下代码,一个 list 中,我们不仅可以取出某个位置上的元素,还可以取出一个子list:

>>> a_list = ['a', 'b', 'c', 'd', 'e']
>>> a_list[0]
'a'
>>> a_list[1]
'b'
>>> a_list[0:2]
['a', 'b']
>>> a_list[:2]
['a', 'b']
>>> a_list[-1]
'e'
>>> a_list[:-2]
['a', 'b', 'c']

一个 list 可以和另外一个 list 相加,一个 list 还可以和一个整数相乘,这些,你以后都可以直接在代码中试试效果。上面代码中,呈现了 list 如何取子集的方法,有些时候,你可能会在别人的代码中看到 b_list = a_list[:][:] 其实等于 [0:-1],相对于对当前的 list 进行了一次复制。

tuple

tuple 的中文翻译一般叫 元组,跟 list 最大的区别,tuple 是不可变的。一旦 tuple 类型的数据确定了,就不能增减了,当然也不能进行排序、倒序这些操作了。
声明 tuple 的几种类型:

>>> tuple([1,2,3,4,5])
(1, 2, 3, 4, 5)
>>> (1,2,3)
(1, 2, 3)
>>> 1,2
(1, 2)
>>> 1,
(1,)

特别要注意 a, 这种简化了的赋值形式,相当于 (a, )。有时候,代码中不小心多了一个 , ,就会导致当前的赋值的数据类型变成了 tuple。这种 typo 性质的错误,有时会导致程序无法运行,有时又因为这个语法是合规的 (PyCharm 这样的编辑器都不会提示为错误),还能让整个程序不出错地跑下去,当然结果可能已经错得一塌糊涂了。

set

set 翻译为 集合,跟 list 有些像,却是不同的类别。它不像 list、tuple 一样会保留元素的次序 (有先有后),并且会自动 去重
因为 set 不保留次序,所以在判断 in 的时候,性能是远远超过 list、tuple 的,虽然一般量级的数据,这个性能问题并不明显,但随着量级上去,list 性质的数据对象中判断某个元素是否 in,其性能将逐渐成为不可用的状态。

>>> a_list = [1, 2, 3, 1, 2]
>>> a_set = set(a_list)
>>> a_set
{1, 2, 3}
>>> a_list
[1, 2, 3, 1, 2]
>>> 1 in a_set
True

set集合,它有交集、并集这样的运算逻辑:

>>> a_set & b_set # 交集
{2, 3}
>>> a_set | b_set # 并集,也叫合集
{1, 2, 3}
>>> a_set - b_set # 在 a_set 中但不在 b_set 中
{1}
>>> b_set - a_set # 在 b_set 中但不在 a_set 中
set()

set 在其它入门课程中,通常不作为常用的数据类型,但从 Python 真正实用的角度来看,set 是一个非常重要的基础数据类型。

dict

dict 的翻译为 字典,也是一个常用的数据格式,作用是相当于建立了一个映射

>>> d1 = {'a': 1, 'b': 2, 'c': 3}
>>> d2 = dict(a=1, b=2, c=3)
>>> d1 == d2
True
>>> 
>>> d1.keys()
dict_keys(['a', 'b', 'c'])
>>> d1.values()
dict_values([1, 2, 3])
>>> for i,j in d1.items(): print(i,j)
...
a 1
b 2
c 3
>>> 
>>> d1['d'] = 5 # 新 key 赋值
>>> d1['a'] = 'hello' # 旧 key 改值
>>> d1.update(dict(a=56)) # 合并其它字典

OrderedDict

OrderedDict 是一个 dict 类型,一般的 dict 是不会特意记录次序 (Python 3 中似乎有所改善),只负责映射关系。但实际应用场景中,会出现既要映射关系,也要考虑次序的情况。

# OrderedDict 不是内置类型,需要从 collections 中先 import
>>> from collections import OrderedDict
>>> d1 = OrderedDict(c=1, b=2, a=3)
>>> d1 # 按照赋值前后关系,呈现出次序了
OrderedDict([('c', 1), ('b', 2), ('a', 3)])

同 set 一样,在其它多数入门课程中,OrderedDict 也不被作为常用数据类型,但它同样很重要,主要是两个方面的意义:

  1. 有序的 字典 类型数据,实际应用中确实需要;
  2. 通过 OrderedDict 我们知道,原始的 dict 这个 type 是可以被扩展,成为一个新的 type。

注: 最终开始写代码时,你可以尝试自己写一个基础的 Class,从 dict 继承过来,但是,它会限制元素的总数,超过一定总数的时候,会删除老的元素,以保证不溢出。

file

在操作文件的时候,会出现这个类型的变量:

f = open('文件路径', 'rb')
# 一些操作逻辑
# 最后关闭已打开的文件
f.close()

上面的代码也等同于:

with open('文件路径', 'rb') as f:
    # 使用 with as 的语法,相当于代码的最后会自动处理 f.close()
    # 一些操作逻辑

rb 表示作为原始码 (二进制)读取,如果要写入文件的话,则需要改为 wb,但是要注意:

  1. 不要随便操作 ,因为会覆盖原本的文件;
  2. 要写入的字符串其数据类型当为 str (Python 2)、bytes (Python3),不然可能会报错。

当文件处于读取的时候,一般常用命令为:

  1. read: 全部读取
  2. readlines: 按行全部读取,返回一个 list,一行文本作为 list 内的一个元素

更多的细节,用到的时候,再去搜索引擎中寻找对应的知识。对 file 进行操作时,务必注意其对内存可能造成的影响,比如说文件非常大,那么就不要一次性读取,而是一点点读取、解析,这样就不会对内存造成过重的负担了。

注意: 在 Python 3 中,没有 file 的类型,实际上是一个 _io.TextIOWrapper 的对象 ( r 模式下),如果是 rb 模式,则是 _io.BufferedReader 对象,实际上不用细分它们具体是什么对象类型,当做一般的 file 类型就好。