0%

what is vim?#

Vim是从 vi 发展出来的一个 文本编辑器。简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方。 vim 则可以说是程序开发者的一项很好用的工具。

vim 的模式#

vim 共分为三种模式,分别是: - 命令模式(Command mode) - 输入模式(Insert mode) - 底线命令模式(Last line mode)

命令模式#

用户刚刚启动 vim,便进入了命令模式。

此状态下敲击键盘动作会被Vim识别为命令,而非输入字符。下面列举常用的命令模式

查找 与 替换#

查找#

  • Command模式下按 “/” 即可进入查找模式,输入要查找的字符串并 按下Enter。 Vim会跳转到第一个匹配。 按下 “n” 查找下一个,按下 “N” 查找上一个
  • 在查找模式中加入 示大小写不敏感查找,表示大小写敏感查找, 例如
    1
    /foo\c

替换#

1
:{作用范围}s/{目标}/{替换}/{替换标志}

例如 在全局范围(%)查找 foo 并替换为 bar,所有出现都会被替换(g)

作用范围#
作用范围 示例
当前行 :s/foo/bar/g
全文 :%s/foo/bar/g
选区,在 Visual 模式下选择区域后输入 :,Vim 即可自动补全为 :'<,'>。 :'<,'>s/foo/bar/g
2-11 行 :2,11s/foo/bar/g
当前行 . 与接下来两行 +2 :.,+2s/foo/bar/g
替换标志符#
替换标志符 示例
只替换从光标位置开始,目标的第一次出现 :%s/foo/bar
i 表示大小写不敏感查找 :%s/foo/bar/i
I 表示大小写敏感 :%s/foo/bar/I
c 表示需要确认 :%s/foo/bar/c
g 表示全局 global 替换 :%s/foo/bar/gc

插入#

1
2
3
4
5
6
7
8
9
10
11
12
13
-----进入输入模式(Insert mode)-----
i 当前位置前插入
I 当前行首插入

a 当前位置后插入
A 当前行尾插入

o 当前行之后插入一行
O 当前行之前插入一行

-----进入取代模式(Replace mode)-----
r 取代光标所在的那一个字符一次
R 一直取代光标所在的文字,直到按下 ESC 为止

移动#

以行为单位移动#

命令 作用
^ 移动到行首第一个词的首字母。
| 移动到行首第一个字符
$ 移动到行尾
j 移动到下一行。
k 移动到上一行。
:10 移动光标到文件第 10 行。可以 :set number 来让 vim 显示行号。
gg 移动到文件首行
G 移动到文件尾行

以屏幕为单位移动#

命令 作用
Ctrl + e 向下滚动一行
Ctrl + y 向上滚动一行
Ctrl + d(down) 向下滚动半屏
Ctrl + u(up) 向上滚动半屏
Ctrl + f(forward) 向下滚动一屏
Ctrl + b(back) 向上滚动一屏
Shift + g 切换到最后

以单词为单位移动#

命令 作用
w 移动到下一个单词的词首
b 移动到上一个单词的词首
e 移动到下一个单词的结尾

删除#

命令 作用
x, X 在一行字当中,x 为向后删除一个字符 (相当于 [del] 按键), X 为向前删除一个字符(相当于 [backspace] 亦即是退格键) (常用)
nx n 为数字,连续向后删除 n 个字符。举例来说,我要连续删除 10 个字符, 『10x』。
dd 删除游标所在的那一整行(常用)
ndd n 为数字。删除光标所在的向下 n 行,例如 20dd 则是删除 20 行 (常用)
d1G 删除光标所在到第一行的所有数据
dG 删除光标所在到最后一行的所有数据
d$ 删除游标所在处,到该行的最后一个字符
d0 那个是数字的 0 ,删除游标所在处,到该行的最前面一个字符

复制#

命令 作用
yy 复制游标所在的那一行(常用)
nyy n 为数字。复制光标所在的向下 n 行,例如 20yy 则是复制 20 行(常用)
y1G 复制游标所在行到第一行的所有数据
yG 复制游标所在行到最后一行的所有数据
y0 复制光标所在的那个字符到该行行首的所有数据
y$ 复制光标所在的那个字符到该行行尾的所有数据
p, P p 为将已复制的数据在光标下一行贴上,P 则为贴在游标上一行! 举例来说,我目前光标在第 20 行,且已经复制了 10 行数据。则按下 p 后, 那 10 行数据会贴在原本的 20 行之后,亦即由 21 行开始贴。但如果是按下 P 呢? 那么原本的第 20 行会被推到变成 30 行。 (常用)

撤销与重做#

命令 作用
u 撤销 (常用)
[Ctrl]+r 重做上一个动作 (常用)

查看历史输入#

1
q + (Ctrl + :)

输入模式#

在命令模式下按下 i就进入了输入模式。在输入模式中,可以使用以下按键:

  • 字符按键以及Shift组合,输入字符
  • ENTER,回车键,换行
  • BACK SPACE,退格键,删除光标前一个字符
  • DEL,删除键,删除光标后一个字符
  • 方向键,在文本中移动光标
  • HOME/END,移动光标到行首/行尾
  • Page Up/Page Down,上/下翻页
  • Insert,切换光标为输入/替换模式,光标将变成竖线/下划线
  • ESC,退出输入模式,切换到命令模式

底线命令模式#

在命令模式下按下 :(英文冒号) 就进入了底线命令模式。

底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。

在底线命令模式中,基本的命令有(已经省略了冒号):

1
2
3
4
5
6
7
8
9
10
:q          退出程序
:q! 若曾修改过档案,又不想储存,使用 ! 为强制离开不储存
:w 保存文件
:w! 若文件属性为『只读』时,强制写入该档案。到底能不能写入, 还是跟权限有关

----环境的变更----
:set nu 显示行号,设定之后,会在每一行的前缀显示该行的行号
:set nonu 与 set nu 相反,为取消行号

ESC 退出底线命令模式
惊叹号 ("!") 在 vim 当中,常常具有『强制』的意思

更改vim配置#

打开配置文件

1
sudo vi /etc/vim/vimrc
如设置显示行号, 跳转到文本末尾, 输入
1
set number  "显示行号

reference#

1.Linux vi/vim

内部结构#

C++ 标准模板库中的 map 是一种关联式容器,内部基于 红黑树 实现。红黑树是一种 平衡 二叉树,插入、删除、查找 等关键操作的时间复杂度均为 O(logn)

Python 虚拟机的运行依赖 dict 对象,包括 名字空间 以及 对象属性空间 等底层都由 dict 实现的。因此Python 中的 dict 对象基于查找效率更高的 hash table 实现的。

PyDictObject#

首先来看看dict的结构,PyDictObject 在头文件 Include/dictobject.h 中定义:

dict 对象 理论上应该是一种变长对象,但从 PyObject_HEAD 可以看出,它是基于 不可变长对象实现。 除了PyObject_HEAD外, PyDictObject 还包括以下几个字段:

  • Py_ssize_t ma_used ,对象当前所保存的 键值对个数
  • uint64_t ma_version_tag ,对象当前 版本号 ,每次 修改时更新
  • PyDictKeysObject *ma_keys ,指向按键对象映射的 哈希表 结构;
  • PyObject ** ma_values , split table模式下指向由所有 值对象 组成的数组

_dictkeysobject#

然后我们再看看 PyDictKeysObject 的源码,在Objects/dict-common.h 头文件中的 **_dictkeysobject**:

  • Py_ssize_t dk_refcnt ,引用计数,跟 映射视图 的实现有关,有点类似对象引用计数;
  • Py_ssize_t dk_size ,哈希表大小,必须是 \(2^n\),这样可将模运算优化成 按位与 运算;
  • dict_lookup_func dk_lookup , 哈希查找函数 指针,可根据 dict 当前状态选用最优函数版本;
  • Py_ssize_t dk_usable ,键值对数组 可用个数
  • Py_ssize_t dk_nentries ,键值对数组 已用个数
  • char dk_indices[] ,哈希表 起始地址 ,哈希表后紧接着 键值对数组 dk_entries。 char is required to avoid strict aliasing.

PyDictKeyEntry#

键值对结构体 PyDictKeyEntry 就非常直白了,除了保存键对象和值对象的指针外,缓存着键对象的哈希值:

1
2
3
4
5
6
typedef struct {
/* Cached hash code of me_key. */
Py_hash_t me_hash; /* 键对象的 哈希值 ,避免重复调用 __hash__ 计算哈希值*/
PyObject *me_key; /* 键对象指针*/
PyObject *me_value; /* 值对象指针 This field is only meaningful for combined tables */
} PyDictKeyEntry;
#### indices and entries dk_indices 是PyDictKeyEntry的数组,它的大小是 USABLE_FRACTION(dk_size)。
1
#define USABLE_FRACTION(n) (((n) << 1)/3)  /*line 413 in bjects/dictobjetc.c*/

DK_ENTRIES(dk)可用于获取指向entries的指针。
1
PyDictKeyEntry *ep0 = DK_ENTRIES(mp->ma_keys);    /*PyDictObject *mp*/
Objects/dictobjetc.c中,我们可以看到关于 DK_ENTRIES 的宏定义
1
2
3
4
/*line 312 in bjects/dictobjetc.c*/

#define DK_ENTRIES(dk) \
((PyDictKeyEntry*)(&((int8_t*)((dk)->dk_indices))[DK_SIZE(dk) * DK_IXSIZE(dk)]))
重写DK_ENTRIES宏:
1
2
3
4
5
// assume int8_t can fit into the indices array
size_t indices_offset = DK_SIZE(dk) * DK_IXSIZE(dk);
int8_t *pointer_to_indices = (int8_t *)(dk->dk_indices);
int8_t *pointer_to_entries = pointer_to_indices + indices_offset;
PyDictKeyEntry *entries = (PyDictKeyEntry *) pointer_to_entries;
因此,PyDictKeysObject结构就很清晰了:

容量策略#

split table和combined table#

在介绍PyDictObjet的ma_values时,我们曾提到 split table模式下才使用。那么,什么是plit table和combined table呢? ##### 两者区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
The DictObject can be in one of two forms.

Either:
A combined table:
ma_values == NULL, dk_refcnt == 1.
Values are stored in the me_value field of the PyDictKeysObject.
Or:
A split table:
ma_values != NULL, dk_refcnt >= 1
Values are stored in the ma_values array.
Only string (unicode) keys are allowed.
All dicts sharing same key must have same insertion order.
*/
##### 什么情况下,不同的dict对象共享相同的键但值不同 只包含 unicode键而不包含伪键(没有删除的对象),并保持 相同的插入顺序。 如果 同一类有多个实例,split table实现 可以节省大量内存。有关更多详细信息,请参阅 PEP 412

预分配机制#

dict 对象也有一种类似 list 对象的 预分配机制 。那么, dict 对象容量管理策略是怎样的呢?

由 Objects/dictobject.c 源文件中的 PyDict_MINSIZE 宏定义,我们知道 dict 内部哈希表最小长度为 8

1
#define PyDict_MINSIZE 8
哈希表越密集,哈希冲突则越频繁,性能也就越差。因此,哈希表必须是一种 稀疏 的表结构,越稀疏则性能越好。由于 内存开销 的制约,哈希表不可能无限度稀疏,需要在时间和空间上进行权衡。实践经验表明,1/2至2/3满的哈希表,较好地平衡了 内存开销搜索效率

为保证哈希表的稀疏程度,进而控制哈希冲突频率, Python 通过 USABLE_FRACTION 宏将哈希表内元素控制在2/3以内。USABLE_FRACTION 宏根据哈希表规模 n ,计算哈希表 可存储元素个数,也就是 键值对数组 的长度。以长度为 8 的哈希表为例,最多可以保持 5 个键值对,超出则需要扩容。USABLE_FRACTION 在indices and entries小节已经展示过了。

哈希表规模一定是 \(2^n\) (熟悉C++ unordered_map的都知道,哈希扩容时只是 近似翻倍的质数) ,也就是说 Python 采用 翻倍扩容 策略。

为什么python3.7中空dict占用了240字节?#
1
2
3
4
5
Python 3.7.6 (default, Jan  8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
>>> import sys
>>> empty_dict = {}
>>> sys.getsizeof(empty_dict)
240

从以上分析,可以看出 Python 为空 dict 对象分配了一个 长度为 8 的哈希表,因而也要占用相当多的内存,主要有以下几个部分组成:

  • 可收集对象链表节点,共 24 字节,在此不再展开, 垃圾回收机制 讲解中有详细介绍;
  • PyDictObject 结构体,6 个字段,共 48 字节;
  • PyDictKeysObject 结构体,除两个数组外有 5 个字段,共 40 字节;
  • 哈希索引数组,长度为 8 ,每个槽位 1 字节,共 8 字节;
  • 键值对数组,长度为 5(用USABLE_FRACTION算出来的,限制在2/3之内) ,每个 PyDictKeyEntry 结构体 24 字节,共 120 字节。

内存优化#

python2.7空dict占用272字节,而python3.7只占用240!!!

1
2
3
4
5
Python 2.7 (r27:82525, Jul  4 2010, 07:43:08) [MSC v.1500 64 bit (AMD64)] on win32
>>> import sys
>>> empty_dict = {}
>>> sys.getsizeof(empty_dict)
272

python3.6之前#
如果有许多大的稀疏哈希表,则会浪费大量内存。
python3.6之后#
为了以更紧凑的方式表示哈希表,可以在哈希表中 拆分 indices 和real key-value

indices 指向一个 索引数组,索引项指向原始内容存储的位置。可以将索引视为更简单的版本哈希表,将entries视为数组,数组将每个 哈希值、key和value(PyDictKeyEntry)存储为一个元素。

每当 搜索或插入 一个元素时,根据hash value 和 indices的大小,就可以在indices数组中得到一个索引,并根据新得到的索引从entries中得到结果。

为优化内存使用, Python 将 dict 哈希表分为 哈希索引(indices)键值对(entries) 两个数组来实现。例如,在64字大小的操作系统中,每个指针需要8个字节,原来需要 \(8 * 3 * 8 = 192\) ,而优化后只需 $8 * 3 * 3 + 1 * 8 = 80 $ ,节省了大约58%的内存使用。

为什么遍历字典python3.6之前是无序的,python3.6之后有序?#

因为entries 按插入顺序存储元素,所以可以 按插入项的相同顺序遍历哈希表。在旧版本的实现中,按hash key的顺序存储元素,遍历哈希表时会出现无序。这就是为什么dict在python3.6之前是无序的,而在python3.6之后是有序的

优化计算#

由于哈希表必须保持 稀疏 ,最多只有 2/3 满,这意味着要浪费至少 \(\frac{1}{3}\) 的内存空间。更雪上加霜的是,一个键值对条目 PyDictKeyEntry 大小达 24 字节,试想一个规模为 65536 的哈希表,将浪费高达 512KB 的内存空间:

\[65536 * \frac{1}{3} * 24 = 524288\]

为了尽量节省内存, Python 将 键值对数组压缩到原来的 \(\frac{2}{3}\),只负责存储,索引由另一个数组负责。由于索引数组只需要保存 键值对数组 的下标,而整数占用的内存空间只是若干字节,因此可以 节约大量内存

索引数组每个元素的大小#
索引数组 可根据哈希表规模,选择 尽量小的整数类型。对于规模 不超过 256 的哈希表,选择 8 位整数即可;对于长度 不超过 65536 的哈希表, 16 位整数足矣;以此类推。
1
2
3
4
5
6
7
8
9
10
11
12
13
#define DK_SIZE(dk) ((dk)->dk_size)
#if SIZEOF_VOID_P > 4
#define DK_IXSIZE(dk) \
(DK_SIZE(dk) <= 0xff ? \
1 : DK_SIZE(dk) <= 0xffff ? \
2 : DK_SIZE(dk) <= 0xffffffff ? \
4 : sizeof(int64_t))
#else
#define DK_IXSIZE(dk) \
(DK_SIZE(dk) <= 0xff ? \
1 : DK_SIZE(dk) <= 0xffff ? \
2 : sizeof(int32_t))
#endif
哈希表规模 条目表规模 旧方案 新方案 节约内存
8 8 * 2 / 3 = 5 24 * 8 = 192 1 * 8 + 24 * 5 = 128 64
256 256 * 2 / 3 = 170 24 * 256 = 6144 1 * 256 + 24 * 170 = 4336 1808
65536 65536 * 2 / 3 = 43690 24 * 65536 = 1572864 2 * 65536 + 24 * 43690 = 1179632 393232

free list#

1
2
3
4
#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
static PyDictObject *free_list[PyDict_MAXFREELIST];

CPython还使用 free list 重用删除的哈希表避免内存碎片,提高性能

每个进程全局变量都有个 free list

if we create a new dict object, the memory request is delegate to CPython's memory management system
1
d = dict()
1
del a

如果free list未满, dict类型的析构函数将当前dict存储到free list。

下次创建新的dict对象时,将检查 free list, 如果有 可用的对象则从 free list分配;如果没有,则从 CPython的内存管理系统分配。
1
d = dict()

哈希#

可哈希 ( hashable )对象#

根据哈希表性质, key对象 必须满足以下两个条件,否则哈希表便不能正常工作:

  • 哈希值在对象整个生命周期内 不能改变; ( list 、dict 等 可变对象均不能作为哈希key)
  • 可比较,且比较相等的对象哈希值必须相同; 满足这两个条件的对象便是 可哈希 ( hashable )对象,只有 可哈希对象才可作为哈希表的键。因此,诸如 dict 、set等底层由哈希表实现的容器对象,其键对象必须是可哈希对象。

而用户自定义的对象默认便是可哈希对象,对象哈希值由对象地址计算而来,且任意两个不同对象均不相等:

1
2
3
4
5
6
7
8
9
10
>>> class A:
... pass
>>>
>>> a = A()
>>> b = A()
>>>
>>> hash(a), hash(b)
(2852108, 2852116)
>>> a == b
False
#### 哈希函数 哈希值 计算作为对象行为中的一种,秘密也隐藏在类型对象中—— tp_hash 函数指针。而内置函数 hash 则依赖类型对象中的 tp_hash 函数,完成哈希值计算并返回。

以 str 对象为例,其哈希函数位于 Objects/unicodeobject.c 源文件,unicode_hash 是也:

1
2
3
4
5
6
7
8
9
10
11
12
PyTypeObject PyUnicode_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"str", /* tp_name */
sizeof(PyUnicodeObject), /* tp_size */
// ...

(hashfunc) unicode_hash, /* tp_hash*/

// ...
unicode_new, /* tp_new */
PyObject_Del, /* tp_free */
};
对于用户自定义的对象,可以实现 hash 魔术方法重写默认哈希值计算方法。举个例子,假设标签类 Tag 的实例对象由 value 字段唯一标识
1
2
3
4
5
6
7
8
9
10
11
12
class Tag:
def __init__(self, value, title):
self.value = value
self.title = title

def __hash__(self):
"""value 字段唯一标识,根据 value 字段实现 哈希函数"""
return hash(self.value)

def __eq__(self, other):
"""根据 value 字段实现 相等性 判断"""
return self.value == other.value
哈希值 使用频率 较高,而且在对象生命周期内均不变。因此,可以在对象内部 对哈希值进行缓存,避免重复计算。以 str 对象为例,内部结构中的 hash 字段便是用于保存哈希值

哈希冲突#

解决哈希冲突的常用方法有两种:

  • 分离链接法 ( separate chaining ) ;
  • 开放地址法 ( open addressing );
分离链接法#
开放地址法#
  • 线性探测 ,顾名思义, \(d_i\)是一个线性函数,例如 \(d_i = 2 * i\)
  • 平方探测 ,顾名思义, \(d_i\)是一个平方函数,例如 \(d_i = i^2\) 线性探测 和 平方探测 很简单,平方探测似乎更胜一筹。如果哈希表存在局部热点,探测很难快速跳过热点区域,而 平方探测 则好很多。然而,这两种方法都不够好——因为固定的探测序列加大了冲突的概率。

Python 探测方法在 lookdict 函数中实现,位于 Objects/dictobject.c 源文件内。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static Py_ssize_t _Py_HOT_FUNCTION
lookdict(PyDictObject *mp, PyObject *key,
Py_hash_t hash, PyObject **value_addr)
{
size_t i, mask, perturb;
PyDictKeysObject *dk;
PyDictKeyEntry *ep0;

top:
dk = mp->ma_keys;
ep0 = DK_ENTRIES(dk);
mask = DK_MASK(dk);
perturb = hash;
i = (size_t)hash & mask;

for (;;) {
Py_ssize_t ix = dk_get_index(dk, i);
// 省略键比较部分代码

// 计算下个槽位
// 由于参考了对象哈希值,探测序列因哈希值而异
perturb >>= PERTURB_SHIFT;
i = (i*5 + perturb + 1) & mask;
}
Py_UNREACHABLE();
}

哈希攻击#

Python 在 3.3 以前, 哈希算法 只根据对象本身计算哈希值。因此,只要 Python 解释器相同,对象哈希值也肯定相同。我们执行 Python 2 解释器启动一个交互式终端,并计算字符串 fasion 的哈希值:

1
2
3
4
5
>>> import os
>>> os.getpid()
2878
>>> hash('fasion')
-1023772170
我们再次执行 Python 2 解释器启动另一个交互式终端,发现字符串 fasion 的哈希值保存不变:
1
2
3
4
5
>>> import os
>>> os.getpid()
2915
>>> hash('fasion')
-1023772170
如果一些别有用心的人 构造出大量哈希值相同的 key ,并提交给服务器,会发生什么事情呢?例如,向一台 Python 2 Web 服务器 post 一个 json 数据,数据包含大量的 key ,所有 key 的哈希值相同。这意味着 哈希表将频繁发生哈希冲突,性能由 \(O(1)\) 急剧下降为 \(O(N)\),被活生生打垮!这就是 哈希攻击

问题很严重,好在应对方法却很简单:为对象加把 盐(salt)。具体做法如下: 1. Python 解释器进程启动后,产生一个 随机数作为 盐 ; 2. 哈希函数同时参考 对象本身 以及 随机数 计算哈希值; 这样一来,攻击者无法获悉解释器内部的随机数,也就无法构造出哈希值相同的对象了!Python 自 3.3 以后,哈希函数均采用 加盐模式,杜绝了 哈希攻击 的可能性。Python 哈希算法在 Python/pyhash.c 源文件中实现,有兴趣的童鞋可以学习一下,这里就不再展开了。

执行 Python 3.7 解释器,启动一个交互式终端,并计算字符串 fasion 的哈希值:

1
2
>>> hash('fasion')
6950491525924312838
再次执行 Python 3.7 解释器,启动另一个交互式终端,发现字符串 fasion 的哈希值已经变了:
1
2
>>> hash('fasion')
-7162025883309105262
## reference 1. dict 2. Python 源码深度剖析/13 dict 对象,高效的关联式容器 2. Python 源码深度剖析/14 dict 哈希表高级知识精讲

why not 哈希索引?#

  1. 哈希值是无序的值,既然无序,就 不能进行范围查找,而数据库需要很多大于小于的查找。

  2. 如果要进行 排序操作,也不能使用哈希值的哈希索引进行排序(例如不能用uuid做索引,只能做唯一标识,因为也是哈希无序的)。

  3. 有些值的 哈希索引是一样的,如果要进行查找,要 逐一比对,相当于全盘扫描。

why not 平衡二叉树?#

平衡二叉树是采用 二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据 减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下特定:

  1. 平衡二叉树是一种 二叉查找树
  2. 每个结点的左子树的高度减去右子树的 高度的绝对值不超过1
  3. 空树和左右子树都是平衡二叉树
  4. 相比红黑树,平衡二叉树比较 适用于没有删除的情况

为什么不能用平衡二叉树: 1. 由于每个子节点最大数量为2,导致 平衡二叉树高度较高,查找次数相对较多。 2. 范围查找时,需要 向上回旋查找(例如查找>=56的数,先找到56,然后向上回旋,找出65和87)

why not B树?#

B 树又叫 平衡多路查找树。一棵m阶的B 树 (注:切勿简单的认为一棵m阶的B树是m叉树,虽然存在四叉树,八叉树,KD树,及vp/R树/R*树/R+树/X树/M树/线段树/希尔伯特R树/优先R树等空间划分树,但与B树完全不等同)的特性如下: 1. 树中每个结点 最多含有m个孩子(m>=2); 2. 除根结点和叶子结点外,其它每个结点 至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数); 3. 若 根结点不是叶子结点,则 至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点); 4. 所有 叶子结点都出现在同一层,叶子结点 不包含任何关键字信息(可以看做是 外部接点或查询失败的接点,叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。)。 5. 每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中: a) Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。 b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。 c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。

为什么不能用B树: 查找深度明显比平衡二叉树要低,但是范围查找仍然需要 向上回旋查找

why choose B+树?#

B+树是B树的一个升级版,相对于B树来说B+树更 充分的利用了节点的空间,让查询速度更加稳定,其速度 完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别

B树和B+树#

  1. B+跟B树不同,B+树的 非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
  2. B+树 叶子节点保存了父节点的所有关键字记录的指针所有数据地址必须要到叶子节点才能获取到。所以 每次数据查询的次数都一样
  3. B+树叶子节点的关键字 从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
  4. 非叶子节点的子节点数=关键字数(来源百度百科)(根据各种资料 这里有两种算法的实现方式,另一种为非叶节点的关键字数=子节点数-1(来源维基百科),虽然他们数据排列结构不一样,但其原理还是一样的。Mysql 的B+树是用第一种方式实现);

因此,B+树的 查找次数少(树的高度低),范围查找也快(叶子节点通过指针连接)

reference#

  1. 彻底搞懂系列B-树、B+树、B-树、B*树

分析报错#

最近写博客时,遇到用hexo s指令失败报错的问题

1
2
3
4
5
FATAL Port 4000 has been used. Try other port instead.
FATAL {
err: Error: listen EADDRINUSE: address already in use :::4000
...
}

分析该报错,发现端口4000已经被占用

查找进程#

查找端口号 4000的网络占用

1
$netstat -ano|grep 4000
- -a 显示所有连接和侦听端口。 - -n 以数字形式显示地址和端口号。 - -o 显示拥有的与每个连接关联的进程 ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
协议      本地地址                外部地址                状态           PID(这一行输入netstat -ano 在头部会显示)
TCP 0.0.0.0:4000 0.0.0.0:0 LISTENING 21144
TCP 0.0.0.0:14000 0.0.0.0:0 LISTENING 21144
TCP 127.0.0.1:55016 127.0.0.1:55017 ESTABLISHED 24000
TCP 127.0.0.1:55017 127.0.0.1:55016 ESTABLISHED 24000
TCP [::]:4000 [::]:0 LISTENING 21144
TCP [::]:14000 [::]:0 LISTENING 21144
TCP [::1]:4000 [::1]:55146 TIME_WAIT 0
TCP [::1]:4000 [::1]:55147 TIME_WAIT 0
TCP [::1]:4000 [::1]:55152 TIME_WAIT 0
TCP [::1]:4000 [::1]:55166 TIME_WAIT 0
TCP [::1]:4000 [::1]:55167 TIME_WAIT 0
TCP [::1]:4000 [::1]:55174 TIME_WAIT 0
TCP [::1]:4000 [::1]:55177 TIME_WAIT 0
TCP [::1]:4000 [::1]:55178 TIME_WAIT 0
TCP [::1]:4000 [::1]:55183 TIME_WAIT 0
TCP [::1]:4000 [::1]:55186 TIME_WAIT 0
可以看到端口号 4000被 PID 21144占用

查找应用#

查找进程号包含 21144的所有进程

1
$ tasklist|findstr "21144"

1
2
映像名称                       PID  会话名                     会话  内存使用(这一行输入tasklist在头部会显示)
Code.exe 21144 Console 1 200,664 K

好家伙,就是这个Code.exe占用了

kill掉应用#

通过查看占用端口号的进程,可以直接杀掉进程(这一步谨慎操作,别杀了不能杀的进程,那就Good Game了)

1
2
3
4
5
taskkill /pid 21144 -f -t

or

taskkill /pid 21144 /f /t
- /pid processid 指定要终止的进程的 PID。 - /f 指定强制终止进程。 - /t 终止指定的进程和由它启用的子进程

reference#

  1. 在windows下查看端口被占用情况

读取文件之 tail 命令#

'tail' 命令 显示文件最后几行内容,读取log神奇。默认读取文件最后 10 行内容

1
tail <file name>  

1
$ tail log.txt  

'tail -n'#

语法

1
2
tail -n<number> <file name>  
# tail -n<行数> <文件名称>
如读取最后5行:
1
$ tail -n5 log.txt  

'tail -c' 选项#

tail -c 命令选项显示 文件以指定字符计数的最后内容

语法

1
2
tail -c<number> <file name>  
#tail -c<字符数> <文件名称>
如:
1
2
3
4
5
6
$ tail -c10 log.game*

==> log.game1 <==
028.76777
==> log.game2 <==
028.77236
显示 log.game为前缀 文件的 最后 10 个字符内容信息

'tail -f'#

1
tail -f log.test*

当前语句可以将 log.test1, log.test2等以 log.test 打头文件的 初始最后十行追加显示,这个是 看log神器

'tail -a'#

有时使用 grep命令会提示 "binary file matches .log"** 从提示可以看出,该文件是个二进制文本。

此时使用 -a参数接口。

1
grep -a test XXX.log

1
-a, --text equivalent to --binary-files=text,即让二进制文件等价于文本。

读取文件之 grep 命令#

-c 显示当前有多少条#

这个语句非常 necessary, 因为log太多时,查找可能会 爆内存,先看看log有多少条(配合tail -n100等语句使用)。

1
2
3
4
$grep "leo" ./log/log.ga* -c

./log/log.ga1:0
./log/log.ga2:175

需满足所有条件#

1
grep word1 file.txt | grep word2 |grep word3

满足任意条件#

1
grep -E "word1|word2|word3"   file.txt

搜索含有Track字段的log#

1
2
grep Trace log.txt
grep "Trace" log.txt

多文件查找#

1
grep Trace file_1 file_2 file_3 ...

输出除 Trace 之外的所有行 -v 选项:#

1
grep -v Trace file_name

标记匹配颜色 --color=auto 选项:#

1
grep "Trace" file_name --color=auto

使用正则表达式 -E 选项:#

1
2
3
grep -E "[1-9]+"

egrep "[1-9]+"

只输出文件中匹配到的部分 -o 选项:#

1
2
3
4
5
echo this is a test line. | grep -o -E "[a-z]+\."
line.

echo this is a test line. | egrep -o "[a-z]+\."
line.

统计文件或者文本中包含匹配字符串的行数 -c 选项:#

1
grep -c "Trace" file_name

输出包含匹配字符串的行数 -n 选项:#

1
2
3
grep "Trace" -n file_name

cat file_name | grep "Trace" -n

多个文件#

1
grep "Trace" -n file_1 file_2

打印样式匹配所位于的字符或字节偏移:#

1
2
3
echo gun is not unix | grep -b -o "not"
7:not
#一行中字符串的字符便宜是从该行的第一个字符开始计算,起始值为0。选项 -b -o 一般总是配合使用。

搜索多个文件并查找匹配文本在哪些文件中 -l:#

1
grep -l "Trace" file1 file2 file3...

lsof 做什么的#

lsof(list open files)是一个 查看当前系统文件的工具。在linux环境下,任何事物 皆以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问 网络连接和硬件。如传输控制协议 (TCP) 和用户数据报协议 (UDP) 套接字等,系统在后台都为该应用程序分配了一个 文件描述符,该文件描述符提供了大量关于这个应用程序本身的信息。

lsof打开的文件可以是:

  • 普通文件
  • 目录
  • 网络文件系统的文件
  • 字符或设备文件
  • (函数)共享库
  • 管道,命名管道
  • 符号链接
  • 网络文件(例如:NFS file、网络socket,unix域名socket)
  • or其它类型的文件

由于 有时候 lsof 需要 访问核心内存和各种文件,所以需要 root用户执行。

lsof 怎么用#

命令格式:#

1
lsof [参数][文件]

命令参数:#

  • -a 列出打开文件 存在的进程
  • -c<进程名> 列出指定进程所打开的文件
  • -g 列出GID号进程详情
  • -d<文件号> 列出占用该文件号的进程
  • +d<目录> 列出目录下被打开的文件
  • +D<目录> 递归列出目录下被打开的文件
  • -n<目录> 列出使用NFS的文件
  • -i<条件> 列出 符合条件的进程。(4、6、协议、:端口、 @ip
  • -p<进程号> 列出 指定进程号所打开的文件
  • -u 列出UID号进程详情
  • -h 显示 帮助信息
  • -v 显示版本信息

lsof实例分析#

example 1: 无任何参数#

1
2
3
4
5
6
7
8
9
10
11
$lsof|more

进程的名称 进程标识符 进程所有者
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME
systemd 1 root cwd unknown /proc/1/cwd (readlink: Permission denied)
kthreadd 2 root cwd unknown /proc/2/cwd (readlink: Permission denied)
kthreadd 2 root rtd unknown /proc/2/root (readlink: Permission denied)
bash 24070 liuwen03 cwd DIR 254,33 4096 614 /home/liuwen03
bash 24070 liuwen03 rtd DIR 253,0 4096 128 /
bash 24070 liuwen03 txt REG 253,0 1029688 16819734 /bin/bash.19.so
bash 24070 liuwen03 mem REG 253,0 43592 8451394 /lib/x86_64-linux-gnu/libnss_nis-2.19.so
  • COMMAND:进程的名称
  • PID:进程标识符
  • PPID:父进程标识符(需要指定-R参数)
  • USER:进程所有者
  • PGID:进程所属组
  • FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    (1)cwd:表示current work dirctory,即:应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改
    (2)txt :该类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的 /sbin/init 程序
    (3)lnn:library references (AIX);
    (4)er:FD information error (see NAME column);
    (5)jld:jail directory (FreeBSD);
    (6)ltx:shared library text (code and data);
    (7)mxx :hex memory-mapped type number xx.
    (8)m86:DOS Merge mapped file;
    (9)mem:memory-mapped file;
    (10)mmap:memory-mapped device;
    (11)pd:parent directory;
    (12)rtd:root directory;
    (13)tr:kernel trace file (OpenBSD);
    (14)v86 VP/ix mapped file;
    (15)0:表示标准输入
    (16)1:表示标准输出
    (17)2:表示标准错误
    一般在标准输出、标准错误、标准输入后还跟着文件状态模式:r、w、u等
    (1)u:表示该文件被打开并处于读取/写入模式
    (2)r:表示该文件被打开并处于只读模式
    (3)w:表示该文件被打开并处于
    (4)空格:表示该文件的状态模式为unknow,且没有锁定
    (5)-:表示该文件的状态模式为unknow,且被锁定
    同时在文件状态模式后面,还跟着相关的锁
    (1)N:for a Solaris NFS lock of unknown type;
    (2)r:for read lock on part of the file;
    (3)R:for a read lock on the entire file;
    (4)w:for a write lock on part of the file;(文件的部分写锁)
    (5)W:for a write lock on the entire file;(整个文件的写锁)
    (6)u:for a read and write lock of any length;
    (7)U:for a lock of unknown type;
    (8)x:for an SCO OpenServer Xenix lock on part of the file;
    (9)X:for an SCO OpenServer Xenix lock on the entire file;
    (10)space:if there is no lock.
  • TYPE:文件类型,如DIR、REG等,常见的文件类型:
    1
    2
    3
    4
    5
    6
    7
    (1)DIR:表示目录
    (2)CHR:表示字符类型
    (3)BLK:块设备类型
    (4)UNIX: UNIX 域套接字
    (5)FIFO:先进先出 (FIFO) 队列
    (6)IPv4:网际协议 (IP) 套接字
    (7)REG:常规文件
  • DEVICE:指定磁盘的名称
  • SIZE:文件的大小
  • NODE:索引节点(文件在磁盘上的标识)
  • NAME:打开文件的确切名称

example 2: 某个文件相关的进程#

1
2
3
4
5
6
7
$lsof /bin/bash

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 15079 liuwen03 txt REG 253,0 1029688 16819734 /bin/bash
bash 15382 liuwen03 txt REG 253,0 1029688 16819734 /bin/bash
bash 15934 liuwen03 txt REG 253,0 1029688 16819734 /bin/bash
bash 24070 liuwen03 txt REG 253,0 1029688 16819734 /bin/bash

example 3:某个用户打开的文件信息#

-u 选项,u是user的缩写

1
2
3
4
5
6
7
8
$lsof -u liuwen03|more

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
lsof 2059 liuwen03 cwd DIR 254,33 4096 614 /home/liuwen03
lsof 2059 liuwen03 rtd DIR 253,0 4096 128 /
lsof 2059 liuwen03 txt REG 253,0 163192 605633 /usr/bin/lsof
lsof 2059 liuwen03 mem REG 253,0 47712 8451392 /lib/x86_64-linux-gnu/libnss_files-2.19.so
...略

example 4:某个程序进程所打开的文件信息#

-c 将会列出所有 以python这个进程开头的程序的文件,其实你可以写成 lsof | grep python, 但是 第一种方法明显比第二种方法要少打几个字符;

1
2
3
4
5
6
7
8
9
10
$lsof -c python|more

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 1257 root cwd unknown /proc/1257/cwd (readlink: Permission denied)
python 1257 root txt unknown /proc/1257/exe (readlink: Permission denied)
python 1257 root NOFD /proc/1257/fd (opendir: Permission denied)
python 2834 liuwen03 cwd DIR 254,33 4096 4294389 /home/liuwen03/Dev/medal_and_tv/G37Server/dc/dc
python 2834 liuwen03 rtd DIR 253,0 4096 128 /
python 2834 liuwen03 txt REG 253,0 3777864 180384 /usr/bin/python2.7
...略

example 5: 某个用户以及某个进程所打开的文件信息#

其实就是上面 3 和 4 的组合拳

1
$lsof  -u liuwen03 -c python

example 6:显示 某个进程打开的文件#

列出 2834 进程打开的文件

1
2
3
4
5
$lsof -p 2834
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 2834 liuwen03 cwd DIR 254,33 4096 4294389 /home/liuwen03/Dev/medal_and_tv/G37Server/dc/dc
python 2834 liuwen03 rtd DIR 253,0 4096 128 /
...略

example 7:列出所有的网络连接#

1
2
3
4
5
6
$lsof -i|more

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 2834 liuwen03 7u IPv4 215114357 0t0 TCP *:27183 (LISTEN)
python 2843 liuwen03 4u IPv4 215901528 0t0 TCP localhost.i.nease.net:34189->localhost.i.nease.net:4000 (ESTABLISHED)
...略

example 8:列出所有udp/tcp 网络连接#

1
2
3
$lsof -i udp|more
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 4797 liuwen03 10u IPv4 215901407 0t0 UDP *:4000
1
$lsof -i tcp|more

example 9:列出谁在使用某个端口#

1
2
3
4
5
6
7
8
$lsof -i :4000

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 2843 liuwen03 4u IPv4 215901528 0t0 TCP localhost.i.nease.net:34189->localhost.i.nease.net:4000 (ESTABLISHED)
python 2844 liuwen03 4u IPv4 215901525 0t0 TCP localhost.i.nease.net:34187->localhost.i.nease.net:4000 (ESTABLISHED)
python 2845 liuwen03 4u IPv4 215896898 0t0 TCP localhost.i.nease.net:34188->localhost.i.nease.net:4000 (ESTABLISHED)
python 4797 liuwen03 8u IPv4 215900363 0t0 TCP *:4000 (LISTEN)
python 4797 liuwen03 10u IPv4 215901407 0t0 UDP *:4000

example 10:列出某个用户的所有活跃的网络端口#

1
2
3
4
5
6
$lsof -a -u liuwen03 -i|more

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 2834 liuwen03 7u IPv4 215114357 0t0 TCP *:27183 (LISTEN)
python 2843 liuwen03 4u IPv4 215901528 0t0 TCP localhost.i.nease.net:34189->localhost.i.nease.net:4000 (ESTABLISHED)
...略

example 11:列出被进程号为1234的进程所打开的所有IPV4 network files#

1
2
3
4
5
6
$lsof -i 4 -a -p 4678

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 4678 liuwen03 7u IPv4 215900052 0t0 TCP localhost.i.nease.net:54076->localhost.i.nease.net:x11 (ESTABLISHED)
python 4678 liuwen03 8u IPv4 215900054 0t0 TCP localhost.i.nease.net:39381->localhost.i.nease.net:30000 (ESTABLISHED)
...略

reference#

1.每天一个linux命令(51):lsof命令

eval函数#

eval作用是什么#

计算指定表达式的值。也就是说它要执行的Python代码只能是单个运算表达式(注意eval不支持任意形式的赋值操作),而不能是复杂的代码逻辑,这一点和lambda表达式比较相似。

函数定义: eval(expression, globals=None, locals=None) 参数说明: expression:必选参数,可以是字符串,也可以是一个任意的code对象实例(可以通过compile函数创建)。如果它是一个字符串,它会被当作一个(使用globals和locals参数作为全局和本地命名空间的)Python表达式进行分析和解释。 globals:可选参数,表示全局命名空间(存放全局变量),如果被提供,则必须是一个字典对象。 locals:可选参数,表示当前局部命名空间(存放局部变量),如果被提供,可以是任何映射对象。如果该参数被忽略,那么它将会取与globals相同的值。 如果globals与locals都被忽略,那么它们将取eval()函数被调用环境下的全局命名空间和局部命名空间。 返回值: 如果expression是一个code对象,且创建该code对象时,compile函数的mode参数是'exec',那么eval()函数的返回值是None; 否则,如果expression是一个输出语句,如print(),则eval()返回结果为None; 否则,expression表达式的结果就是eval()函数的返回值; 实例: x = 10

def func(): y = 20 a = eval('x + y') print('a: ', a) b = eval('x + y', {'x': 1, 'y': 2}) print('b: ', b) c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4}) print('c: ', c) d = eval('print(x, y)') print('d: ', d)

func() 输出结果:

a: 30 b: 3 c: 4 10 20 d: None 对输出结果的解释:

对于变量a,eval函数的globals和locals参数都被忽略了,因此变量x和变量y都取得的是eval函数被调用环境下的作用域中的变量值,即:x = 10, y = 20,a = x + y = 30 对于变量b,eval函数只提供了globals参数而忽略了locals参数,因此locals会取globals参数的值,即:x = 1, y = 2,b = x + y = 3 对于变量c,eval函数的globals参数和locals都被提供了,那么eval函数会先从全部作用域globals中找到变量x, 从局部作用域locals中找到变量y,即:x = 1, y = 3, c = x + y = 4 对于变量d,因为print()函数不是一个计算表达式,没有计算结果,因此返回值为None

exec#

在python中,变量查找遵循LGB原则,即优先在 局部作用域(local scope)中对变量进行查找,失败则在 全局作用域(global scope)中进行查找,最后尝试再 内建作用域(build-in scope)内查找

1
2
3
exec code in __main__.dict
相当于
exec code in globals(), locals()

如果__main__.dict中没有相应的key时,exec执行后会设置的

1
2
3
4
5
6
7
8
>>> print __main__.__dict__
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__main__': <module '__main__' (built-in)>, '__doc__': None, '__package__': None}
>>> a = 1
>>> exec "print a" in __main__.__dict__
1
>>> print __main__.__dict__
# exec执行后设置了a属性
{'a': 1, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__main__': <module '__main__' (built-in)>, '__name__': '__main__', '__doc__': None}
1
2
>>> exec 'print a' in {'a':1},{'a':789}
789

import模块#

import xxx#

定义test.py

1
import _locale

用dis模块编译一下

1
2
3
4
5
6
7
C:\Users\liuw\Desktop> python.exe -m dis .\test.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (_locale)
6 STORE_NAME 0 (_locale)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE

想象一下,当前 有两个或多个线程正在导入相同的random模块,CPython如何处理这种情况?

  1. 操作码IMPORT_NAME将检查导入的名称是否在sys.module中,如果是,则返回 sys.module中的内容
  2. 尝试获取锁 **_imp**
  3. 使用模块名称获取_module_locks中的锁对象,如有必要,在position 1中创建
  4. 尝试在第3步(position 2)中获取锁定对象
  5. 释放锁 **_imp**(position 3)
  6. 检查要导入的名称是否在sys.module中,如果是,则释放_module_locks中的锁对象并返回sys.module中的内容(position 4)
  7. 对于 sys.meta_path中的 finder,如果finder可以加载模块名称,请释放_module_locks中的锁对象并返回已加载的内容
  8. raise an error 在position 1,只有拥有_imp的线程才能修改_module_locks,当前线程将检查要导入的模块名称是否在_module_locks中,如果不是,则在_module_locks中插入新的锁定对象
在position 3,释放_imp锁,如果有其他线程导入其他模块,则它可以获得_imp锁并继续执行该过程,如果有其他线程导入同一模块,即使它成功获取了_imp,也将失败在获取_module_locks中的锁时,因为先前的线程持有该锁

在position 4,当前线程再次检查 sys.modules中的缓存

在position 5,它将在每个 finder调用之前获取 **_imp**锁。在函数调用之后查找并释放它

import xxx as x#

定义test.py
1
import demo as d
用dis模块编译一下
1
2
3
4
5
6
7
C:\Users\liuw\Desktop> python.exe -m dis .\test.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (demo)
6 STORE_NAME 1 (d)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE

可以看出,这里仅仅是 将模块的名字变为 d 了.因此,这个 import 语句变体其实等价于:

1
2
3
import demo
d = demo
del demo

from import#

定义test.py

1
from demo import value
同样用 dis 对语句进行反编译,我们得到以下字节码:
1
2
3
4
5
6
7
8
9
C:\Users\liuw\Desktop> python.exe -m dis .\test.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (('value',))
4 IMPORT_NAME 0 (demo)
6 IMPORT_FROM 1 (value) # 从栈顶模块中取出指定名字,并保存于栈顶
8 STORE_NAME 1 (value)
10 POP_TOP
12 LOAD_CONST 2 (None)
14 RETURN_VALUE
IMPORT_FROM 指令: 从栈顶模块中取出指定名字,并保存于栈顶。

注意到,value 以 元组 的形式保存于栈顶,IMPORT_NAME 指令如果发现 value 为 demo 模块的子模块,将同时加载 value 子模块。此外,IMPORT_FROM 与 STORE_NAME 这两个指令相互配合,从模块中取出给定名字并保存。

仅仅加载了模块下的一部分

from import as#

1
2
3
4
5
6
7
8
9
10
//from demo import func as f
C:\Users\liuw\Desktop> python.exe -m dis .\test.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (('func',))
4 IMPORT_NAME 0 (demo)
6 IMPORT_FROM 1 (func)
8 STORE_NAME 2 (f) # 存储名字的时候存储的是 f
10 POP_TOP
12 LOAD_CONST 2 (None)
14 RETURN_VALUE

reference#

  1. module-github
  2. Python 源码深度剖析/22 模块动态加载, import 背后哪些事儿

内部结构#

list 对象是一种 变长对象 ,因此包含变长对象公共头部。 除了公共头部, list 内部维护了一个动态数组,而 数组则依次保存元素对象的指针

  • **ob_item ,指向动态数组的指针,动态数组保存元素对象的指针;
  • allocated ,动态数组 长度,即列表 容量
  • ob_size ,动态数组 当前保存元素个数,即列表 长度

对象操作#

append#

python中的list的实现非常像 C++ 中的Vector

1
l = list() # 初始化一个空列表
字段 ob_size存储实际大小,其类型为Py_ssize_t,通常为64位,1 << 64可以表示的数非常大,通常在ob_size字段 溢出之前会耗尽 RAM

如果我们append('a') 1个元素:

ob_size 变为 1, ob_item 指向一个 大小为4的新内存块

相继append三个元素:

ob_size 变为 4, 内存块满了

大多数情况下, append 方法性能都足够好,时间复杂度是 O(1)

动态扩容#

继续append一个元素:

这是resize相关代码

1
2
3
4
/* cpython/Objects/listobject.c */
/* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... */
/* currently: new_allocated = 5 + (5 >> 3) + 3 = 8 */
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6); # 扩容时,预留一定的余量,(newsize >> 3)表明余量一般是 1/8 左右

如果 list 对象内部数组已用满,再添加元素时则需要进行扩容。 append 等方法在操作时都会对内部数组进行检查,如需扩容则调用 list_resize 函数。在 list_resize 函数, Python 重新分配一个长度更大的数组并替换旧数组

由于内部数组扩容时,需要将列表元素 从旧数组拷贝到新数组,时间复杂度为 O(n) ,开销较大。为此, Python 在为内部数组扩容时,会预留一定余量,一般是 1/8 左右

pop#

1
2
>>> l.pop()
'e'

我们可以看到这里只是简单将 元素e 弹出,内存布局并未发生变化。

动态减容#

实际上,每次pop,都会调用 resize 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* cpython/Objects/listobject.c */
/* allocated: 8, newsize: 3, 8 >= 3 && (3 >= 4?), no */
if (allocated >= newsize && newsize >= (allocated >> 1)) {
/* Do not realloc if the newsize deos not fall
lower than half the allocated size
不分配内存,仅改变ob_size的值 */
assert(self->ob_item != NULL || newsize == 0);
/* only change the ob_size field */
Py_SIZE(self) = newsize;
return 0;
}
/* newsize小于allocated的一半时,会重新分配内存 */
/* 3 + (3 >> 3) + 3 = 6 */
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
如果理解了pop 每次都会调用 resize 函数,下面情形就很容易理解了
1
2
>>> l.pop()
'd'

reference#

  1. list
  2. Python 源码深度剖析/11 list 对象,容量自适应的数组式容器

Unicode是什么

计算机存储的基本单位是 八位字节 ,由 8 个比特位组成,简称 字节 。由于英文只由 26 个字母加若干符号组成,因此英文字符可以直接用 字节 来保存。其他诸如 中日韩等语言,由于字符众多,则不得不用多个字节来编码。

Read more »