作者: chenpaopao
Linux文件分区和挂载相关知识
之前只是在实验室的服务器上跑代码,但因为将anaconda文件夹放置在了系统盘110G,所以系统爆满,想要解决,却无从下手,因此将linux中涉及文件挂载分区的知识进行整理总结,以备不时之需。

磁盘分区:
Linux 物理磁盘管理常用三个命令为 df、du 和 fdisk。
- df(英文全称:disk full):列出文件系统的整体磁盘使用量
- du(英文全称:disk used):检查磁盘空间使用量
- fdisk:用于磁盘分区
每次安装系统的时候我们都会进行分区,Linux下磁盘分区和目录的关系如下:
- 任何一个分区都必须挂载到某个目录上。
- 目录是逻辑上的区分。分区是物理上的区分(比如有个一1T的硬盘,我们先通过fdisk进行磁盘分区,并使用字母和数字的组合来指代磁盘分区,比如/dev/xxyN)。
- 磁盘Linux分区都必须挂载到目录树中的某个具体的目录上才能进行读写操作。
- 根目录是所有Linux的文件和目录所在的地方,需要挂载上一个磁盘分区

mount挂载
挂载的概念
当要使用某个设备时,例如要读取硬盘中的一个格式化好的分区、光盘或软件等设备时,必须先把这些设备对应到某个目录(这个目录可以不为空,但挂载后这个目录下以前的内容将不可用),而这个目录就称为“挂载点(mount point)”,这样才可以读取这些设备,而这些对应的动作就是“挂载”。
需要理解的是:
linux操作系统将所有的设备都看作文件,
它将整个计算机的资源都整合成一个大的文件目录。
我们要访问存储设备中的文件,必须将文件所在的分区挂载到一个已存在的目录上,
然后通过访问这个目录来访问存储设备
挂载分区:挂载的目录必须为空
mkdir /d1
mkdir /d2
mount /dev/sdc1 /d1
mount /dev/sdc2 /d2
查看文件(夹)所在分区(挂载点):
1、最简单的,直接 df <文件(夹)路径>
2、用df 或 fdisk -l查看分区挂载情况,直接输入mount或者也可以用cat /etc/mtab,然后pwd找最接近的挂载点信息
Linux修改磁盘挂载目录
比如想把已经挂载在home目录上的硬盘挂载到data目录上, 如下操作
#df -h(查看分区情况及数据盘名称)
# mkdir /data(如果没有data目录就创建,否则此步跳过)
# umount /home(卸载硬盘已挂载的home目录)
# mount /dev/sdb3 /data (挂载到data目录)
# vi /etc/fstab (编辑fstab文件修改或添加,使重启后可以自动挂载)
/dev/sdb3 /data ext3 auto 0 0
数据盘 新挂载目录
编辑/etc/fstab里面的/home为/data, 或创建让系统启动的时候自动挂载到/data
ln链接 命令—-linux常用命令
ln -s
你可以将链接简单地理解为 Windows 中常见的快捷方式(或是 OS X 中的替身),Linux 中常用它来解决一些库版本的问题,通常也会将一些目录层次较深的文件链接到一个更易访问的目录中。在这些用途上,我们通常会使用到软链接(也称符号链接)。
我为什么要用:有时候服务器的home目录下的磁盘容量不足,而用户的文件都在 /home/用户名下面,这样的话无法满足用户的使用,就需要ln软链接了
建立软连接
ln [参数][源文件或目录][目标文件或目录] 常用:软链接命令 ln -s 源文件或目录 目标文件或目录 (将源文件或目录 连接到 目标文件或目录) 其中: 源文件或目录:文件原始地址 目标文件或目录:文件想要建立链接的位置
Linux ln(英文全拼:link files)命令是一个非常重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接。
当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在 其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间。
命令功能 :
Linux文件系统中,有所谓的链接(link),我们可以将其视为档案的别名,而链接又可分为两种 : 硬链接(hard link)与软链接(symbolic link),硬链接的意思是一个档案可以有多个名称,而软链接的方式则是产生一个特殊的档案,该档案的内容是指向另一个档案的位置。硬链接是存在同一个文件系统中,而软链接却可以跨越不同的文件系统。
不论是硬链接或软链接都不会将原本的档案复制一份,只会占用非常少量的磁碟空间。
软链接:
- 1.软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
- 2.软链接可以 跨文件系统 ,硬链接不可以
- 3.软链接可以对一个不存在的文件名进行链接
- 4.软链接可以对目录进行链接
硬链接:
- 1.硬链接,以文件副本的形式存在。但不占用实际空间。
- 2.不允许给目录创建硬链接
- 3.硬链接只有在同一个文件系统中才能创建
必要参数:
- -b 删除,覆盖以前建立的链接
- -d 允许超级用户制作目录的硬链接
- -f 强制执行
- -i 交互模式,文件存在则提示用户是否覆盖
- -n 把符号链接视为一般目录
- -s 软链接(符号链接)
- -v 显示详细的处理过程
这里有两点要注意:
第一,ln命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化;
第二,ln的链接又分软链接和硬链接两种,软链接就是ln –s 源文件 目标文件,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接 ln 源文件 目标文件,没有参数-s, 它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。
ln指令用在链接文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则会把前面指定的所有文件或目录复制到该目录中。若同时指定多个文件或目录,且最后的目的地并非是一个已存在的目录,则会出现错误信息。
删除软连接的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # 以下这样的删除都没问题 ~ unlink link ~ rm link ~ rm -r link # 这里的参数 r 其实是没有意义的,因为link是一个软连接 不是目录 ~ rm -rf link # 这里的 rf 同样没有意义,只是rm 命令忽略了这里的参数 ~ ~ # 这样删除就会造成灾难 ~ rm -rf link/ # 这个时候你发现软连接并没有删除,但是 origin 目录下的文件是全部没删除了 ==! # 这些罪魁祸首是 参数 f,如果你没有使用f参数 这一切还可以挽回 ~ rm link/ rm: cannot remove `link/’: Is a directory # 这里 rm 通过你的参数 link/ 发现是要删除一个目录,这时候需要你添加参数 r ~ rm -r link/ rm: cannot remove `link’: Not a directory # 这里你添加了 r 参数,但是并不能找到目录 link/ 因为link并不是一个目录,他是一个软连接,只不过有些shell在补全的时候会将 `/` 补全上去 |
collections模块

collections模块实现一些特定的数据类型,可以替代Python中常用的内置数据类型如dict, list, set, tuple,简单说就是对基本数据类型做了更上一层的处理。
https://github.com/python/cpython/blob/3.10/Lib/collections/init.py
https://docs.python.org/zh-cn/3/library/collections.html?highlight=deque#collections.namedtuple
init.py:
'''This module implements specialized container datatypes providing
alternatives to Python's general purpose built-in containers, dict,
list, set, and tuple.
* namedtuple factory function for creating tuple subclasses with named fields
* deque list-like container with fast appends and pops on either end
* ChainMap dict-like class for creating a single view of multiple mappings
* Counter dict subclass for counting hashable objects
* OrderedDict dict subclass that remembers the order entries were added
* defaultdict dict subclass that calls a factory function to supply missing values
* UserDict wrapper around dictionary objects for easier dict subclassing
* UserList wrapper around list objects for easier list subclassing
* UserString wrapper around string objects for easier string subclassing
'''
__all__ = ['deque', 'defaultdict', 'namedtuple', 'UserDict', 'UserList',
'UserString', 'Counter', 'OrderedDict', 'ChainMap']
一、deque
用途:双端队列,头部和尾部都能以O(1)时间复杂度插入和删除元素。类似于列表的容器
所谓双端队列
,就是两端都能操作,与Python内置的list区别在于:头部插入与删除的时间复杂度为O(1),来个栗子感受一下:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
"""
保留最后n个元素
"""
from collections import deque
def search(file, pattern, history=5):
previous_lines = deque(maxlen=history)
for l in file:
if pattern in l:
yield l, previous_lines # 使用yield表达式的生成器函数,将搜索过程的代码和搜索结果的代码解耦
previous_lines.append(l)
with open(b'file.txt', mode='r', encoding='utf-8') as f:
for line, prevlines in search(f, 'Python', 5):
for pline in prevlines:
print(pline, end='')
print(line, end='')
d = deque()
d.append(1)
d.append("2")
print(len(d))
print(d[0], d[1])
d.extendleft([0])
print(d)
d.extend([6, 7, 8])
print(d)
d2 = deque('12345')
print(len(d2))
d2.popleft()
print(d2)
d2.pop()
print(d2)
# 在队列两端插入或删除元素时间复杂度都是 O(1) ,区别于列表,在列表的开头插入或删除元素的时间复杂度为 O(N)
d3 = deque(maxlen=2)
d3.append(1)
d3.append(2)
print(d3)
d3.append(3)
print(d3)
输出结果如下
人生苦短
我用Python
2
1 2
deque([0, 1, '2'])
deque([0, 1, '2', 6, 7, 8])
5
deque(['2', '3', '4', '5'])
deque(['2', '3', '4'])
deque([1, 2], maxlen=2)
deque([2, 3], maxlen=2)
因此,如果你遇到经常操作列表头的场景,使用deque最好。deque类的所有方法,自行操作一遍就知道了。
class deque(object):
"""
deque([iterable[, maxlen]]) --> deque object
A list-like sequence optimized for data accesses near its endpoints.
"""
def append(self, *args, **kwargs): # real signature unknown
""" Add an element to the right side of the deque. """
pass
def appendleft(self, *args, **kwargs): # real signature unknown
""" Add an element to the left side of the deque. """
pass
def clear(self, *args, **kwargs): # real signature unknown
""" Remove all elements from the deque. """
pass
def copy(self, *args, **kwargs): # real signature unknown
""" Return a shallow copy of a deque. """
pass
def count(self, value): # real signature unknown; restored from __doc__
""" D.count(value) -> integer -- return number of occurrences of value """
return 0
def extend(self, *args, **kwargs): # real signature unknown
""" Extend the right side of the deque with elements from the iterable """
pass
def extendleft(self, *args, **kwargs): # real signature unknown
""" Extend the left side of the deque with elements from the iterable """
pass
def index(self, value, start=None, stop=None): # real signature unknown; restored from __doc__
"""
D.index(value, [start, [stop]]) -> integer -- return first index of value.
Raises ValueError if the value is not present.
"""
return 0
def insert(self, index, p_object): # real signature unknown; restored from __doc__
""" D.insert(index, object) -- insert object before index """
pass
def pop(self, *args, **kwargs): # real signature unknown
""" Remove and return the rightmost element. """
pass
def popleft(self, *args, **kwargs): # real signature unknown
""" Remove and return the leftmost element. """
pass
def remove(self, value): # real signature unknown; restored from __doc__
""" D.remove(value) -- remove first occurrence of value. """
pass
def reverse(self): # real signature unknown; restored from __doc__
""" D.reverse() -- reverse *IN PLACE* """
pass
def rotate(self, *args, **kwargs): # real signature unknown
""" Rotate the deque n steps to the right (default n=1). If n is negative, rotates left. """
pass
这里提示一下,有些函数对队列进行操作,但返回值是None,比如reverse()
反转队列,rotate(1)
将队列中元素向右移1位,尾部的元素移到头部。
二、defaultdict
用途:带有默认值的字典。父类为Python内置的dict
字典带默认值有啥好处?举个栗子,一般来讲,创建一个多值映射字典是很简单的。但是,如果你选择自己实现的话, 那么对于值的初始化可能会有点麻烦,你可能会像下面这样来实现:
d = {}
for key, value in pairs:
if key not in d:
d[key] = []
d[key].append(value)
如果使用 defaultdict 的话代码就更加简洁了:
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)
defaultdict 的一个特征是它会自动初始化每个 key 刚开始对应的值,所以你只需要 关注添加元素操作了。比如:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
# 字典中的键映射多个值
from collections import defaultdict
d = defaultdict(list)
print(d)
d['a'].append([1, 2, 3])
d['b'].append(2)
d['c'].append(3)
print(d)
d = defaultdict(set)
print(d)
d['a'].add(1)
d['a'].add(2)
d['b'].add(4)
print(d)
输出结果如下:
defaultdict(<class 'list'>, {})
defaultdict(<class 'list'>, {'a': [[1, 2, 3]], 'b': [2], 'c': [3]})
defaultdict(<class 'set'>, {})
defaultdict(<class 'set'>, {'a': {1, 2}, 'b': {4}})
三、namedtuple()
用途:创建命名字段的元组。工厂函数
namedtuple主要用来产生可以使用名称来访问元素的数据对象,通常用来增强代码的可读性, 在访问一些tuple类型的数据时尤其好用。
比如我们用户拥有一个这样的数据结构,每一个对象是拥有三个元素的tuple。使用namedtuple方法就可以方便的通过tuple来生成可读性更高也更好用的数据结构。
from collections import namedtuple
websites = [
('Sohu', 'http://www.sohu.com/', u'张朝阳'),
('Sina', 'http://www.sina.com.cn/', u'王志东'),
('163', 'http://www.163.com/', u'丁磊')
]
Website = namedtuple('Website', ['name', 'url', 'founder'])
for website in websites:
website = Website._make(website)
print website
# 输出结果:
Website(name='Sohu', url='http://www.sohu.com/', founder=u'\u5f20\u671d\u9633')
Website(name='Sina', url='http://www.sina.com.cn/', founder=u'\u738b\u5fd7\u4e1c')
Website(name='163', url='http://www.163.com/', founder=u'\u4e01\u78ca')
注意,namedtuple是函数,不是类。
四、Counter
用途:统计可哈希的对象。父类为Python内置的dict
寻找序列中出现次数最多的元素。假设你有一个单词列表并且想找出哪个单词出现频率最高:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
from collections import Counter
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
'my', 'eyes', "you're", 'under'
]
word_counts = Counter(words)
# 出现频率最高的三个单词
top_three = word_counts.most_common(3)
print(top_three)
# Outputs [('eyes', 8), ('the', 5), ('look', 4)]
print(word_counts['eyes'])
morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes']
# 如果你想手动增加计数,可以简单的用加法:
for word in morewords:
print(word)
word_counts[word] += 1
print(word_counts['eyes'])
结果如下:
[('eyes', 8), ('the', 5), ('look', 4)]
8
why
are
you
not
looking
in
my
eyes
9
因为Counter继承自dict,所有dict有的方法它都有(defaultdict和OrderedDict也是的),Counter自己实现或重写了6个方法:
most_common(self, n=None)
,elements(self)
fromkeys(cls, iterable, v=None)
update(*args, **kwds)
subtract(*args, **kwds)
copy(self)
五、OrderedDict
用途:排序的字段。父类为Python内置的dict
OrderedDict在迭代操作的时候会保持元素被插入时的顺序,OrderedDict内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候,它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
需要注意的是,一个OrderedDict的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果你要构建一个需要大量OrderedDict 实例的数据结构的时候(比如读取100,000行CSV数据到一个 OrderedDict 列表中去),那么你就得仔细权衡一下是否使用 OrderedDict带来的好处要大过额外内存消耗的影响。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
from collections import OrderedDict
d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
# d['bar'] = 22 #对于一个已经存在的键,重复赋值不会改变键的顺序
for key in d:
print(key, d[key])
print(d)
import json
print(json.dumps(d))
结果如下:
foo 1
bar 2
spam 3
grok 4
OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])
{"foo": 1, "bar": 2, "spam": 3, "grok": 4}
OrderDict实现或重写了如下方法。都是干嘛的?这个留给大家当课后作业了^_^
clear(self)
popitem(self, last=True)
move_to_end(self, key, last=True)
keys(self)
items(self)
values(self)
pop(self, key, default=__marker)
setdefault(self, key, default=None)
copy(self)
fromkeys(cls, iterable, value=None)
六、ChainMap
用途:创建多个可迭代对象的集合。类字典类型
很简单,如下:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
from collections import ChainMap
from itertools import chain
# 不同集合上元素的迭代
a = [1, 2, 3, 4]
b = ('x', 'y', 'z')
c = {1, 'a'}
# 方法一,使用chain
for i in chain(a, b, c):
print(i)
print('--------------')
# 方法二,使用chainmap
for j in ChainMap(a, b, c):
print(j)
# 这两种均为节省内存,效率更高的迭代方式
一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。然后,这些字典并不是真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都是可以正常使用的,比如:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'
# 合并多个字典和映射
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
# 现在假设你必须在两个字典中执行查找操作
# (比如先从 a 中找,如果找不到再在 b 中找)。
# 一个非常简单的解决方案就是使用collections模块中的ChainMap类
from collections import ChainMap
c = ChainMap(a, b)
print(c)
a['x'] = 11 # 使用ChainMap时,原字典做了更新,这种更新会合并到新的字典中去
print(c) # 按顺序合并两个字典
print(c['x'])
print(c['y'])
print(c['z'])
# 对于字典的更新或删除操作影响的总是列中的第一个字典。
c['z'] = 10
c['w'] = 40
del c['x']
print(a)
# del c['y']将出现报错
# ChainMap对于编程语言中的作用范围变量(比如globals,locals等)
# 是非常有用的。事实上,有一些方法可以使它变得简单:
values = ChainMap() # 默认会创建一个空字典
print('\t', values)
values['x'] = 1
values = values.new_child() # 添加一个空字典
values['x'] = 2
values = values.new_child()
values['x'] = 30
# values = values.new_child()
print(values, values['x']) # values['x']输出最后一次添加的值
values = values.parents # 删除上一次添加的字典
print(values['x'])
values = values.parents
print(values)
a = {'x': 1, 'y': 2}
b = {'y': 2, 'z': 3}
merge = dict(b)
merge.update(a)
print(merge['x'], merge['y'], merge['z'])
a['x'] = 11
print(merge['x'])
输出结果如下:
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
ChainMap({'x': 11, 'z': 3}, {'y': 2, 'z': 4})
11
2
3
{'z': 10, 'w': 40}
ChainMap({})
ChainMap({'x': 30}, {'x': 2}, {'x': 1}) 30
2
ChainMap({'x': 1})
1 2 3
1
作为ChainMap的替代,你可能会考虑使用 update() 方法将两个字典合并。这样也能行得通,但是它需要你创建一个完全不同的字典对象(或者是破坏现有字典结构)。同时,如果原字典做了更新,这种改变不会反应到新的合并字典中去。
ChainMap实现或重写了如下方法:
get(self, key, default=None)
fromkeys(cls, iterable, *args)
copy(self)
new_child(self, m=None)
parents(self)
popitem(self)
pop(self, key, *args)
clear(self)
七、UserDict、UserList、UserString
这三个类是分别对 dict、list、str 三种数据类型的包装,其主要是为方便用户实现自己的数据类型。在 Python2 之前,这三个类分别位于 UserDict、UserList、UserString 三个模块中,需要用类似于 from UserDict import UserDict
的方式导入。在 Python3 之后则被挪到了 collections 模块中。这三个类都是基类,如果用户要扩展这三种类型,只需继承这三个类即可。
Python 3 新特性:类型注解
摘自:https://www.cnblogs.com/xzkzzz/p/11378842.html

在 Python 中定义函数非常简单,像这样:
def say(name): return f'Hello {name}!'
但是,有时候也会看到这样的代码:
def say_hi(name: str) -> str: return f'Hello {name}!'
函数定义似乎变得复杂些了:多出来这些 str
、 ->
都是什么意思?有什么作用?
本文将由浅入深,好好聊聊 Python 3.5 之后的类型注解。理解它将非常有益于优化你的代码。
变量注解
Python 是动态语言,其显著特点是在声明变量时,你不需要显式声明它的类型。
比如这个:
age = 20 print('The age is: ', age + 1) # Output: # The age is: 21
你看,虽然代码里没有明确指定 age
的类型,但是程序运行时隐式推断出它是 int
类型,因此可以顺利执行 age + 1
的动作。
除此之外,已经确定类型的变量,可以随时更改其类型,比如:
age = 20 print(type(age)) # Output: <class 'int'> age = '20' print(type(age)) # Output: <class 'str'>
Python 这种动态特性的好处是它非常的自由,大部分时候你不用纠结类型声明、类型转化等麻烦事,可以用很少的代码完成各种骚操作。但是缺点也在这里:如果你代码某些变量的类型有错,编辑器、IDE等工具无法在早期替你纠错,只能在程序运行阶段才能够暴露问题。
比如下面这个例子:
age = 20 # ... # 这里进行了一大串的其他指令 # 然后你忘记了 age 应该是 int # 错误地将其赋值为字符串 age = '20' print('The age is: ', age + 1) # Output: TypeError: can only concatenate str (not "int") to str
在项目代码逐渐膨胀之后,上面这种看似弱智的情况可能会经常发生。
因此,Python 3.5 之后引入了类型注解,其作用就是让你可以明确的声明变量的类型,使代码不再那么的自由(放飞自我)。
类型注解还在快速发展中,因此尽量用较新的 Python 版本去尝试它。
比如上面的代码,就可以用类型注解改写了:
age: int = 20
看似多此一举,但是编辑器可以凭借此,找出你那些错误的骚操作。
比如笔者用的 VS Code,安装好类型注解插件 Pylance 后,如果写出下面的代码:
age: int = 20 age = '20'
那么编辑器会用醒目的方式告诉你:孙子,你这里的类型写错了!(见下图)

Pylance 默认关闭了类型检查,你得在设置中手动打开。其他的编辑器/IDE(比如 Pycharm)也都提供了类似的类型检查,放心用吧。
很简单,但却带来了巨大的好处:
- 编辑器可以替你揪出代码中关于类型的错误,避免了程序运行过程中各种奇奇怪怪的 Bug 。
- 在你编写代码时,编辑器可以提示你对象的类型,免得你或者团队成员忘记了。(程序员通常记性不好)。

注意,类型注解仅仅是提供给编辑器进行类型检查的机会,也就是起提示的作用,对 Python 程序的运行不会产生任何影响。也就是说,Python 跟以前一样自由,即使你进行了错误的类型赋值,只要不直接引发错误,程序依旧可以运行。
最后,Python 中几种基本的变量类型都得到了支持:
a: int = 3 b: float = 3.14 c: str = 'abc' d: bool = False
很简单吧。让我们继续。
函数注解
文章开头提到的那个例子,就是简单的函数类型注解:
def say_hi(name: str) -> str: return f'Hello {name}!'
你可以很清楚的知道,这个函数应该接收一个字符串参数 name
,并且返回值应该也是字符串。
带默认值的函数像这样书写:
def add(first: int = 10, second: float = 5.5) -> float: return first + second
如果函数没有返回值,那么下面两种写法都可以:
def foo(): pass def bar() -> None: pass
自定义的对象也没问题,像下面这样:
class Person: def __init__(self, name: str): self.name = name def hello(p: Person) -> str: return f'Hello, {p.name}'
如果要避免循环导入或者注解早于对象定义的情况,可以用字符串代替类型:
def hello(p: 'Person') -> str: return f'Hello, {p.name}' class Person: def __init__(self, name: str): self.name = name
效果是相同的。
相比变量类型注解,函数里的类型注解更加有用,并且可能是你最频繁用到注解的地方了。
容器类型
列表、字典、元组等包含元素的复合类型,用简单的 list,dict,tuple 不能够明确说明内部元素的具体类型。
因此要用到 typing
模块提供的复合注解功能:
from typing import List, Dict, Tuple # 参数1: 元素为 int 的列表 # 参数2: 键为字符串,值为 int 的字典 # 返回值: 包含两个元素的元组 def mix(scores: List[int], ages: Dict[str, int]) -> Tuple[int, int]: return (0, 0)
如果你用的是 Python 3.9+ 版本,甚至连 typing
模块都不需要了,内置的容器类型就支持了复合注解:
def mix(scores: list[int], ages: dict[str, int]) -> tuple[int, int]: return (0, 0)
在某些情况下,不需要严格区分参数到底是列表还是元组(这种情况还蛮多的)。这时候就可以将它们的特征抽象为更泛化的类型(泛型),比如 Sequence(序列)。
下面是例子:
# Python 3.8 之前的版本 from typing import Sequence as Seq1 def foo(seq: Seq1[str]): for item in seq: print(item) # Python 3.9+ 也可以这么写 from collections.abc import Sequence as Seq2 def bar(seq: Seq2[str]): for item in seq: print(item)
例子中函数的参数不对容器的类型做具体要求,只要它是个序列(比如列表和元组)就可以。
类型别名
有时候对象的类型可能会非常复杂,或者你希望给类型赋予一个有意义的名称,那么你可以这样定义类型的别名:
from typing import Tuple # 类型别名 Vector2D = Tuple[int, int] def foo(vector: Vector2D): print(vector) foo(vector=(1, 2)) # Output: (1, 2)
Vector2D
这个名称清晰的表达了这个对象是一个二维的向量。
与类型别名有点类似的,是用 NewType
创建自定义类型:
from typing import NewType from typing import Tuple # 创建新类型 Vector3D = NewType('Vector3D', Tuple[int, int, int]) def bar(vector: Vector3D): print(vector)
乍一眼看起来与前面的类型别名功能差不多,但不同的是 NewType
创建了原始类型的“子类”:
# 类型检查成功 # 类型别名和原始类型是等价的 foo(vector=(1, 2)) # 类型检查失败 # NewType创建的是原始类型的“子类” bar(vector=(1, 2, 3)) # 类型检查成功 # 传入参数必须是 Vector3D 的“实例” v_3d = Vector3D((4, 5, 6)) bar(vector=v_3d)
具体用哪种,得根据情况而定。
更多类型
NoReturn
如果函数没有返回值,那么可以这样写:
from typing import NoReturn def hello() -> NoReturn: raise RuntimeError('oh no')
注意下面这样写是错误的:
def hello() -> NoReturn: pass
因为 Python 的函数运行结束时隐式返回 None
,这和真正的无返回值是有区别的。
Optional
猜猜下面的类型注解错在哪里:
def foo(a: int = 0) -> str: if a == 1: return 'Yeah'
答案:函数既有可能返回 None
,也有可能返回 str
。单凭返回值注解为 str
是不能准确表达此情况的。
这种“可能有也可能没有”的状态被称为可选值,在某些项目中非常常见。比如 web 应用中某个函数接受账号和密码作为参数,如果匹配则返回用户对象,若不匹配则返回 None
。
因此,有专门的可选值类型注解可以处理这种情况:
from typing import Optional def foo(a: int = 0) -> Optional[str]: if a == 1: return 'Yeah'
Union
比 Optional
涵盖面更广的是 Union
。
如果函数的返回值是多种类型中的一种时,可以这样写:
from typing import Union def foo() -> Union[str, int, float]: # .... # some code here
上面这个函数可以返回字符串、整型、浮点型中的任意一种类型。
可以发现 Optional
实际上是 Union
的特例:Optional[X]
和 Union[X, None]
是等价的。
Callable
我们知道, Python 中的函数和类的区别并不明显。只要实现了对应的接口,类实例也可以是可调用的。
如果不关心对象的具体类型,只要求是可调用的,那么可以这样写:
from typing import Callable class Foo: def __call__(self): print('I am foo') def bar(): print('I am bar') def hello(a: Callable): a() # 类型检查通过 hello(Foo()) # 同样通过 hello(bar)
Literal
即字面量。它在定义简单的枚举值时非常好用,比如:
from typing import Literal MODE = Literal['r', 'rb', 'w', 'wb'] def open_helper(file: str, mode: MODE) -> str: ... open_helper('/some/path', 'r') # 成功 open_helper('/other/path', 'typo') # 失败
Protocol
即协议。我们通常说一个对象遵守了某个”协议“,意思是这个对象实现了”协议“中规定的属性或者方法。
比如下面这个例子:
from typing import Protocol class Proto(Protocol): def foo(self): print('I am proto') class A: def foo(self): print('I am A') class B: def bar(self): print('I am B') def yeah(a: Proto): pass # 通过,A 实现了协议中的 foo() 方法 yeah(A()) # 不通过,B 未实现 foo() yeah(B())
Any
如果你实在不知道某个类型注解应该怎么写时,这里还有个最后的逃生通道:
from typing import Any def foo() -> Any: pass
任何类型都与 Any
兼容。当然如果你把所有的类型都注解为 Any
将毫无意义,因此 Any
应当尽量少使用。
泛型
要理解泛型,首先得知道没有它时所遇到的麻烦。
假设有一个函数,要求它既能够处理字符串,又能够处理数字。那么你可能很自然地想到了 Union
。
于是写出第一版代码:
from typing import Union, List U = Union[str, int] def foo(a: U, b: U) -> List[U]: return [a, b]
这样写有个很大的弊端,就是参数的类型可以混着用(比如 a: int
且 b:str
),即便你并不想具有这样的特性。看下面这个就明白了:
# 类型检查通过 # 因为 Union[str, int] 可以是其中任意一种类型 # 即便你并不想将 str 和 int 混用 foo('Joe', 19) # 通过 foo(19, 21) # 通过 foo('Joe', 'David')
泛型就可以解决此问题。来看第二版代码:
from typing import TypeVar, List # 定义泛型 T # T 必须是 str 或 int 其中一种 T = TypeVar('T', str, int) def bar(a: T, b: T) -> List[T]: return [a, b] # 类型检查不通过 # 函数的参数必须为同一个类型"T" bar('Joe', 19) # 通过 bar(19, 21) # 通过 bar('Joe', 'David')
可以看出,泛型类似于某种模板(或者占位符),它可以很精确地将对象限定在你真正需要的类型。
让我们再看看下面这个对泛型的应用:
from typing import Dict, TypeVar # 定义泛型 K 和 V # K 和 V 的具体类型没有限制 K = TypeVar("K") V = TypeVar("V") def get_item(key: K, container: Dict[K, V]) -> V: return container[key] dict_1 = {"age": 10} dict_2 = {99: "dusai"} print(get_item("age", dict_1)) # 例1 # 类型检查通过,输出: 10 print(get_item(99, dict_2)) # 例2 # 类型检查通过,输出: dusai print(get_item("name", dict_2)) # 例3 # 类型检查失败 # 因为"name"是字符串,而dict_2的键为整型
- 代码中定义了两个泛型 K 和 V,对它两的类型没有做任何限制,也就是说可以是任意类型。
- 函数
get_item()
接受两个参数。这个函数不关心参数container
字典的键是什么类型,或者字典的值是什么类型;但它的参数container
必须是字典,参数key
必须与字典的键为同类型,并且返回值和字典的值必须为同类型。仅仅通过查看函数的类型注解,就可以获得所有这些信息。
重点来看下例3的类型检查为什么会失败:
dict_2
定义时,其键被定义为整型。get_item("name", dict_2)
调用时,"name"
为字符串,而dict_2
的键为整型,类型不一致。而类型注解中清楚表明它两应该为同一个类型K
,产生冲突。- 编辑器察觉到冲突,友好地提示你,这里可能出错了。
泛型很巧妙地对类型进行了参数化,同时又保留了函数处理不同类型时的灵活性。
再回过头来看看类型注解的作用:
def get_item(key: K, container: Dict[K, V]) -> V: # ... def get_item(key, container): # ...
上面两个函数功能完全相同,但是没有类型注解的那个,显然需要花更多的时间阅读函数内部的代码,去确认函数到底干了什么。并且它也无法利用编辑器的类型检查,在早期帮助排除一些低级错误。
总结
最后再总结下,Python 社区为什么花了很大力气,去实现了类型注解这个仅仅起”提示作用“的功能:
- 让代码模块的功能更清晰。
- 让编辑器可以帮助你尽早发现问题。
JavaScript的作用
- 表单动态校验(密码强度检测) ( JS 产生最初的目的 )
- 网页特效
- 服务端开发(Node.js)
- 桌面程序(Electron)
- App(Cordova)
- 控制硬件-物联网(Ruff)
- 游戏开发(cocos2d-js)
https://www.electronjs.org/zh/docs/latest/tutorial/quick-start
https://wizardforcel.gitbooks.io/electron-doc/content/tutorial/quick-start.html
https://cordova.axuer.com/docs/zh-cn/latest/
https://cordova.axuer.com/
http://www.dba.cn/book/cordova/
https://zhuanlan.zhihu.com/p/101240692
Tmux使用教程–保持服务后台运行
Tmux 是一个终端复用器(terminal multiplexer),非常有用,属于常用的开发工具。
github: https://github.com/tmux/tmux
Tmux 快捷键 & 速查表 & 简明教程
启动新会话:
tmux [new -s 会话名 -n 窗口名]
恢复会话:
tmux at [-t 会话名]
列出所有会话:
tmux ls
tmux kill-session -t 会话名
tmux ls | grep : | cut -d. -f1 | awk '{print substr($1, 0, length($1)-1)}' | xargs kill
在 Tmux 中,按下 ctrl+b
,然后:
会话
:new<回车> 启动新会话
s 列出所有会话
$ 重命名当前会话
窗口 (标签页)
c 创建新窗口
w 列出所有窗口
n 后一个窗口
p 前一个窗口
f 查找窗口
, 重命名当前窗口
& 关闭当前窗口
调整窗口排序
swap-window -s 3 -t 1 交换 3 号和 1 号窗口
swap-window -t 1 交换当前和 1 号窗口
move-window -t 1 移动当前窗口到 1 号
窗格(分割窗口)
% 垂直分割
" 水平分割
o 交换窗格
x 关闭窗格
⍽ 左边这个符号代表空格键 - 切换布局
q 显示每个窗格是第几个,当数字出现的时候按数字几就选中第几个窗格
{ 与上一个窗格交换位置
} 与下一个窗格交换位置
z 切换窗格最大化/最小化
同步窗格
这么做可以切换到想要的窗口,输入 Tmux 前缀和一个冒号呼出命令提示行,然后输入:
:setw synchronize-panes
你可以指定开或关,否则重复执行命令会在两者间切换。 这个选项值针对某个窗口有效,不会影响别的会话和窗口。 完事儿之后再次执行命令来关闭。帮助
调整窗格尺寸
如果你不喜欢默认布局,可以重调窗格的尺寸。虽然这很容易实现,但一般不需要这么干。这几个命令用来调整窗格:
PREFIX : resize-pane -D 当前窗格向下扩大 1 格
PREFIX : resize-pane -U 当前窗格向上扩大 1 格
PREFIX : resize-pane -L 当前窗格向左扩大 1 格
PREFIX : resize-pane -R 当前窗格向右扩大 1 格
PREFIX : resize-pane -D 20 当前窗格向下扩大 20 格
PREFIX : resize-pane -t 2 -L 20 编号为 2 的窗格向左扩大 20 格
文本复制模式:
按下 PREFIX-[
进入文本复制模式。可以使用方向键在屏幕中移动光标。默认情况下,方向键是启用的。在配置文件中启用 Vim 键盘布局来切换窗口、调整窗格大小。Tmux 也支持 Vi 模式。要是想启用 Vi 模式,只需要把下面这一行添加到 .tmux.conf 中:
setw -g mode-keys vi
启用这条配置后,就可以使用 h、j、k、l 来移动光标了。
想要退出文本复制模式的话,按下回车键就可以了。然后按下 PREFIX-]
粘贴刚才复制的文本。
一次移动一格效率低下,在 Vi 模式启用的情况下,可以辅助一些别的快捷键高效工作。
例如,可以使用 w 键逐词移动,使用 b 键逐词回退。使用 f 键加上任意字符跳转到当前行第一次出现该字符的位置,使用 F 键达到相反的效果。
vi emacs 功能
^ M-m 反缩进
Escape C-g 清除选定内容
Enter M-w 复制选定内容
j Down 光标下移
h Left 光标左移
l Right 光标右移
L 光标移到尾行
M M-r 光标移到中间行
H M-R 光标移到首行
k Up 光标上移
d C-u 删除整行
D C-k 删除到行末
$ C-e 移到行尾
: g 前往指定行
C-d M-Down 向下滚动半屏
C-u M-Up 向上滚动半屏
C-f Page down 下一页
w M-f 下一个词
p C-y 粘贴
C-b Page up 上一页
b M-b 上一个词
q Escape 退出
C-Down or J C-Down 向下翻
C-Up or K C-Up 向下翻
n n 继续搜索
? C-r 向前搜索
/ C-s 向后搜索
0 C-a 移到行首
Space C-Space 开始选中
C-t 字符调序
杂项:
d 退出 tmux(tmux 仍在后台运行)
t 窗口中央显示一个数字时钟
? 列出所有快捷键
: 命令提示符
配置选项:
# 鼠标支持 - 设置为 on 来启用鼠标(与 2.1 之前的版本有区别,请自行查阅 man page)
* set -g mouse on
# 设置默认终端模式为 256color
set -g default-terminal "screen-256color"
# 启用活动警告
setw -g monitor-activity on
set -g visual-activity on
# 居中窗口列表
set -g status-justify centre
# 最大化/恢复窗格
unbind Up bind Up new-window -d -n tmp \; swap-pane -s tmp.1 \; select-window -t tmp
unbind Down
bind Down last-window \; swap-pane -s tmp.1 \; kill-window -t tmp
参考配置文件(~/.tmux.conf):
下面这份配置是我使用 Tmux 几年来逐渐精简后的配置,请自取。
# ----------------------------------------------------------------------------- # Tmux 基本配置 - 要求 Tmux >= 2.3 # 如果不想使用插件,只需要将此节的内容写入 ~/.tmux.conf 即可 # ----------------------------------------------------------------------------- # C-b 和 VIM 冲突,修改 Prefix 组合键为 Control-Z,按键距离近 set -g prefix C-z set -g base-index 1 # 窗口编号从 1 开始计数 set -g display-panes-time 10000 # PREFIX-Q 显示编号的驻留时长,单位 ms set -g mouse on # 开启鼠标 set -g pane-base-index 1 # 窗格编号从 1 开始计数 set -g renumber-windows on # 关掉某个窗口后,编号重排 setw -g allow-rename off # 禁止活动进程修改窗口名 setw -g automatic-rename off # 禁止自动命名新窗口 setw -g mode-keys vi # 进入复制模式的时候使用 vi 键位(默认是 EMACS) # ----------------------------------------------------------------------------- # 使用插件 - via tpm # 1. 执行 git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm # 2. 执行 bash ~/.tmux/plugins/tpm/bin/install_plugins # ----------------------------------------------------------------------------- setenv -g TMUX_PLUGIN_MANAGER_PATH '~/.tmux/plugins' # 推荐的插件(请去每个插件的仓库下读一读使用教程) set -g @plugin 'seebi/tmux-colors-solarized' set -g @plugin 'tmux-plugins/tmux-pain-control' set -g @plugin 'tmux-plugins/tmux-prefix-highlight' set -g @plugin 'tmux-plugins/tmux-resurrect' set -g @plugin 'tmux-plugins/tmux-sensible' set -g @plugin 'tmux-plugins/tmux-yank' set -g @plugin 'tmux-plugins/tpm' # tmux-resurrect set -g @resurrect-dir '~/.tmux/resurrect' # tmux-prefix-highlight set -g status-right '#{prefix_highlight} #H | %a %Y-%m-%d %H:%M' set -g @prefix_highlight_show_copy_mode 'on' set -g @prefix_highlight_copy_mode_attr 'fg=white,bg=blue' # 初始化 TPM 插件管理器 (放在配置文件的最后) run '~/.tmux/plugins/tpm/tpm' # ----------------------------------------------------------------------------- # 结束 # ---------------
tmux中多窗口表示时,没有滚动条,可以使用下面的方法翻页查看信息
- 绑定的快捷开始键 默认 ctrl +b + [
- 使用 PgUp PgDn 实现上下翻页
- 键 q 退出

meta 标签
在学习前端过程中,对于head标签内部的内容没有进行深入的了解过,今天来总结一下。
head里可以放这些标签:
<head>元素包含了所有的头部标签元素,在head中你可以插入脚本(scripts),样式文件(CSS),及各种meta信息,可以添加到头部区域的元素为
<title> 定义网页的标题(浏览器工具栏标题,搜素引擎结果页面标题,收藏夹标题)
<meta> 用来定义页面的特殊信息(页面关键字,页面描述) ,描述了一些基本的元数据
<link> 定义了文档与外部资源之间的关系,通常用来引入外部样式(css文件)
<style>用来定义元素的css样式
<script> 用来定义页面的JavaScript 代码 也可用来引入文件
<base> 可以用来统一设置当前页面上的超链接的跳转方式 使用了 <base> 标签,则必须具备 href 属性或者 target 属性或者两个属性都具备。
<noscript>是一个相当古老的标签,其被引入的最初目的是帮助老旧浏览器的平滑升级更替,因为早期的浏览器并不能支持 JavaScript。noscript 标签在不支持JavaScript 的浏览器中显示替代的内容。这个元素可以包含任何 HTML 元素。

在平常的使用中,一般利用vscode插件自动生成html框架,因此基本很少去编写meta标签。自动生成的meta标签:
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
重点:meta标签
meta 元素往往不会引起用户的注意,但是meta对整个网页有影响,会对网页能否被搜索引擎检索,和在搜索中的排名起着关键性的作用。
<meta http-equiv=" " name=" " content=" ">
meta有个必须的属性content用于表示需要设置的项的值。
meta存在两个非必须的属性http-equiv和name, 用于表示要设置的项。
比如<meta http-equiv=”Content-Security-Policy” content=”upgrade-insecure-requests”>,设置的项是Content-Security-Policy设置的值是upgrade-insecure-requests
1. http-equiv 属性
http-equiv一般设置的都是与http请求头相关的信息,设置的值会关联到http头部。也就是说浏览器在请求服务器获取html的时候,服务器会将html中设置的meta放在响应头中返回给浏览器。常见的类型比如content-type, expires, refresh, set-cookie, window-target, charset, pragma等等。
1.1 http-equiv 属性类型:content-type
比如:<meta http-equiv=”content-type” content=”text/html charset=utf8″>可以用来声明文档类型,设置字符集,content-type几乎所有的属性都可以在meta中进行设置。这样设置浏览器的头信息就会包含:content-type: text/html charset=utf8
1.2 expires
用于设置浏览器的过期时间, 其实就是响应头中的expires属性。
<meta http-equiv="expires" content="31 Dec 2021"> expires:31 Dec 2008
1.3 refresh
该种设定表示5秒自动刷新并且跳转到指定的网页。如果不设置url的值那么浏览器则刷新本网页。<meta http-equiv="refresh" content="5 url=http://www.zhiqianduan.com">
1.4 window-target
强制页面在当前窗口以独立页面显示, 可以防止别人在框架中调用自己的页面。
<meta http-equiv="window-target" content="_top >
1.5 pragma
禁止浏览器从本地计算机的缓存中访问页面的内容
<meta http-equiv="pragma" content="no-cache">
2. name 属性
name属性主要用于描述网页,与对应的content中的内容主要是便于搜索引擎查找信息和分类信息用的, 用法与http-equiv相同,name设置属性名,content设置属性值。
1. author
author用来标注网页的作者<meta name="author" content="aaa@mail.abc.com">
2. description
description用来告诉搜素引擎当前网页的主要内容,是关于网站的一段描述信息。<meta name="description" content="这是我的HTML">
3. keywords
keywords设置网页的关键字,来告诉浏览器关键字是什么。是一个经常被用到的名称。它为文档定义了一组关键字。某些搜索引擎在遇到这些关键字时,会用这些关键字对文档进行分类。<meta name="keywords" content="Hello world">
4. generator
表示当前html是用什么工具编写生成的,并没有实际作用,一般是编辑器自动创建的。<meta name="generator" content="vscode">
5. revised
指定页面的最新版本<meta name="revised" content="V2,2015/10/1">
6. robots
告诉搜索引擎机器人抓取哪些页面,all / none / index / noindex / follow / nofollow。<meta name="robots" content="all">
all:文件将被检索,且页面上的链接可以被查询;none:文件将不被检索,且页面上的链接不可以被查询;index:文件将被检索;follow:页面上的链接可以被查询;noindex:文件将不被检索,但页面上的链接可以被查询;nofollow:文件将不被检索,页面上的链接可以被查询。
3. scheme 属性
scheme 属性用于指定要用来翻译属性值的方案。此方案应该在由 head 标签的 profile 属性指定的概况文件中进行了定义。html5不支持该属性。
5. base 标签
base标签定义了文档的基础url地址,在文档中所有的相对地址形式的url都是相对于这里定义的url而言的。为页面上的链接规定默认地址或目标。
base标签包含的属性。
1. href
href是必选属性,指定了文档的基础url地址。例如,如果希望将文档的基础URL定义为https://www.abc.com,则可以使用如下语句:<base href=”http://www.abc.com”>如果文档的超链接指向welcom.html,则它实际上指向的是如下url地址:https://www.abc.com/welocme.html。
2. target
定义了当文档中的链接点击后的打开方式_blank,_self,_parrent,_top。
6. link 标签
link用于引入外部样式表,在html的头部可以包含任意数量的link,link标签有以下常用属性。<link type="text/css" rel="stylesheet" href="github-markdown.css">
1. type
定义包含的文档类型,例如text/css
2. rel
定义html文档和所要包含资源之间的链接关系,可能的值有很多,最为常用的是stylesheet,用于包含一个固定首选样式的表单。
3. href
表示指向被包含资源的url地址
7. style 标签
编写内部样式表的标签。<style>
body {
background: #f3f5f9;
}
</style>
8. script 标签
加载javascript脚本的标签。加载的脚本会被默认执行。默认情况下当浏览器解析到script标签的时候会停止html的解析而开始加载script代码并且执行。<script src="script.js"></script>
1. type
指示脚本的MIME类型。<script type="text/javascript">
2. async
规定异步执行脚本,仅适用于通过src引入的外部脚本。设置的async属性的script加载不会影响后面html的解析,加载是与文档解析同时发生的。加载完成后立即执行。执行过程会停止html文档解析。<script async src="script.js"></script>
3. charset
规定在外部脚本文件中使用的字符编码。<script type="text/javascript" src="script.js" charset="UTF-8"></script>
4. defer
规定是否对脚本执行进行延迟,直到页面加载为止。设置了defer属性的script不会阻止后面html的解析,加载与解析是共同进行的,但是script的执行要在所有元素解析完成之后,DOMContentLoaded事件触发之前完成。<script defer src="script.js"></script>
5. language
规定脚本语言,与“type“`功能类似,不建议使用该字段。
6. src
外部脚本的地址。<script src="script.js"></script>
9. bgsound
网站背景音乐。<bgsound src="music.mp4" autostart="true" loop="5">
1. src
表示背景音乐的url值。
2. autostart
是否自动播放ture自动播放,false不播放,默认为false。
3. loop
是否重复播放,值为数字或者infinite,表示重复具体次或无限次。
强化学习资料:
编码器—解码器(seq2seq)
当输入和输出都是不定长序列时(比如机器翻译),我们可以使用编码器—解码器(encoder-decoder) 或者seq2seq模型。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。
以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如
英语输入:“They”、“are”、“watching”、“.”
法语输出:“Ils”、“regardent”、“.”
下图描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)以表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。下图中使用了编码器在最终时间步的隐藏状态作为输入句子的表征或编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。我们希望解码器在各个时间步能正确依次输出翻译后的法语单词、标点和特殊符号”<eos>”。需要注意的是,解码器在最初时间步的输入用到了一个表示序列开始的特殊符号”<bos>”(beginning of sequence)。
编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量c,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。
根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率,并得到该输出序列的损失,在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。
个人理解:
最后decoder每一步输出的是一个字典大小的概率值向量,分别表示这一步输出所有值的概率,一般取最大的值作为输出。
因此那么在模型预测的时候就需要进行搜索,选择不同的搜索方式决定每一时间步的输出是字典里的那个值,选择不同的值会影响下一时间步的输出概率。
模型预测 Model Prediction
为了搜索该条件概率最大的输出序列,一种方法是穷举所有可能输出序列的条件概率,并输出条件概率最大的序列。我们将该序列称为最优序列,并将这种搜索方法称为穷举搜索 (exhaustive search)。
贪婪搜索 Greedy Search
我们还可以使用贪婪搜索 (greedy search) 。也就是说,对于输出序列任一时间步 ,从
个词中搜索出输出词
且一旦搜索出 “<eos>” 符号即完成输出序列。贪婪搜索的计算开销是 。它比起穷举搜索的计算开销显著下降。例如,当
且
时,我们只需评估
个序列。
下面我们来看一个例子。假设输出词典里面有 “A”、“B”、“C”和 “<eos>” 这四个词。下图中每个时间步下的四个数字分别代表了该时间步生成 “A”、“B”、“C”和 “<eos>” 这四个词的条件概率。在每个时间步,贪婪搜索选取生成条件概率最大的词。因此,将生成序列 “ABC<eos>” 。该输出序列的条件概率是 。
束搜索 Beam Search
束搜索 (beam search) 是比贪婪搜索更加广义的搜索算法。它有一个束宽 (beam size) 超参数。我们将它设为 。在时间步1时,选取当前时间步生成条件概率最大的
个词,分别组成
个候选输出序列的首词。在之后的每个时间步,基于上个时间步的
个候选输出序列,从
个可能的输出序列中选取生成条件概率最大的
个,作为该时间步的候选输出序列。
最终,我们在各个时间步的候选输出序列中筛选出包含特殊符号 “<eos>” 的序列,并将它们中所有特殊符号 “<eos>” 后面的子序列舍弃,得到最终候选输出序列。在这些最终候选输出序列中,取以下分数最高的序列作为输出序列:
其中 为最终候选序列长度,
一般可选为0.75。分母上的 是为了惩罚较长序列在以上分数中较多的对数相加项。分析可得,束搜索的计算开销为
。这介于穷举搜索和贪婪搜索的计算开销之间。
- 预测不定长序列的方法包括贪婪搜索、穷举搜索和束搜索。
- 束搜索通过灵活的束宽来权衡计算开销和搜索质量。
注意力机制
在普通的编码器-解码器模型中,有一个很大的局限性。那就是上下文变量对于 Decoding 阶段每个时间步都是一样的,这可能是模型性能的一个瓶颈。我们希望不同时间步的解码能够依赖于与之更相关的上下文信息,换句话说,Decoding 往往并不需要整个输入序列的信息,而是要有所侧重。于是,Bengio 团队的 Bahdanau 在 2014年首次在编码器-解码器模型中引入了注意力机制 (Attention Mechanism):
注意力机制通过注意力汇聚将查询(自主性提示)和键(非自主性提示)结合在一起,实现对值(感官输入)的选择倾向
动机 Motivation
以英语-法语翻译为例,给定一对英语输入序列 “They”、“are”、“watching”、“.” 和法语输出序列 “Ils”、“regardent”、“.”。解码器可以在输出序列的时间步1使用更集中编码了 “They”、“are” 信息的上下文变量来生成 “Ils”,在时间步2使用更集中编码了 “watching” 信息的上下文变量来生成“regardent”,在时间步3使用更集中编码了 “.” 信息的上下文变量来生成 “.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步编码的信息分配不同的注意力。这也是注意力机制的由来。它最早由 Bahanau 等人提出。
仍然以循环神经网络为例,注意力机制通过对编码器所有时间步的隐藏状态做加权平均来得到背景变量。解码器在每一时间步调整这些权重,即注意力权重,从而能够在不同时间步分别关注输入序列中的不同部分并编码进相应时间步的背景变量。
我们先描述第一个关键点,即计算背景变量。下图描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数a根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。
本质上,注意力机制能够为表征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 [2]。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 [3]。不久后,同样是基于变换器设计的GPT-2模型于新收集的语料数据集预训练后,在7个未参与训练的语言模型数据集上均取得了当时最先进的结果 [4]。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别
评价机器翻译结果
评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。
总结 Conclusions
让我们回顾一下带注意力机制的编码器-解码器的整个设计:
- Encoder 总结输入序列的信息,得到上下文变量
- Decoder 将上下文变量
中的信息解码生成输出序列
- 设计
函数
- 计算当前时间步的隐藏状态
- 计算当前时间步的解码器输出概率
- 得到输出序列的联合概率
并最大化
- 根据 MLE,就是最小化联合概率的负对数
- 得到 loss function
- 用优化方法降低 loss,学习模型参数
- 为了避免相同的上下文变量对模型性能的限制,给编码器-解码器模型加入了注意力机制。