collections模块

https://pixabay.com/

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

https://pixabay.com/

在 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)

  • Electron
  • https://www.electronjs.org/zh/docs/latest/tutorial/quick-start

    https://wizardforcel.gitbooks.io/electron-doc/content/tutorial/quick-start.html

  • Cordova
  • https://cordova.axuer.com/docs/zh-cn/latest/

    https://cordova.axuer.com/

    http://www.dba.cn/book/cordova/

  • cocos2d-js
  • https://docs.cocos.com/creator/manual/zh/

    https://zhuanlan.zhihu.com/p/101240692

  • Ruff
  • https://chain.ruffcorp.com/

    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中多窗口表示时,没有滚动条,可以使用下面的方法翻页查看信息

    1. 绑定的快捷开始键 默认 ctrl +b + [
    2. 使用 PgUp PgDn 实现上下翻页
    3. 键 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每一步输出的是一个字典大小的概率值向量,分别表示这一步输出所有值的概率,一般取最大的值作为输出。

    字典大小为4

    因此那么在模型预测的时候就需要进行搜索,选择不同的搜索方式决定每一时间步的输出是字典里的那个值,选择不同的值会影响下一时间步的输出概率。

    模型预测 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

    让我们回顾一下带注意力机制的编码器-解码器的整个设计:

    1. Encoder 总结输入序列的信息,得到上下文变量 [公式]
    2. Decoder 将上下文变量 [公式] 中的信息解码生成输出序列
    3. 设计 [公式] 函数
    4. 计算当前时间步的隐藏状态 [公式]
    5. 计算当前时间步的解码器输出概率 [公式]
    6. 得到输出序列的联合概率 [公式] 并最大化
    7. 根据 MLE,就是最小化联合概率的负对数
    8. 得到 loss function
    9. 用优化方法降低 loss,学习模型参数
    10. 为了避免相同的上下文变量对模型性能的限制,给编码器-解码器模型加入了注意力机制。

    Convolutional Neural Networks for Sentence Classification

    https://arxiv.org/abs/1408.5882

    github实现

    https://github.com/yoonkim/CNN_sentence

    https://github.com/Cheneng/TextCNN

    对于文本分类,我们能不能用CNN来做,用某种模型初始化,进而做fine-tune呢?答案是肯定的,用于文本分析的CNN—TextCNN。

    text-cnn用于情感分类:

    与二维卷积层一样,一维卷积层使用一维的互相关运算。在一维互相关运算中,卷积窗口从输入数组的最左方开始,按从左往右的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。

    多输入通道的一维互相关运算也与多输入通道的二维互相关运算类似:在每个通道上,将核与相应的输入做一维互相关运算,并将通道之间的结果相加得到输出结果。

    由二维互相关运算的定义可知,多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。

    类似地,我们有一维池化层。textCNN中使用的时序最大池化(max-over-time pooling)层实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。因此,时序最大池化层的输入在各个通道上的时间步数可以不同。

    简单来说,时序最大池化层就是沿着时序方向进行最大池化。

    textCNN模型主要使用了一维卷积层和时序最大池化层。假设输入的文本序列由n个词组成,每个词用d维的词向量表示。那么输入样本的宽为n,高为1,输入通道数为d。textCNN的计算主要分为以下几步。(输入通道就是每个词的d为维度表示,宽就是时序长度)

    词用d维的词向量表示 :一般使用词嵌入模型word2vec.

    1. 定义多个一维卷积核,并使用这些卷积核对输入分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。
    2. 对输出的所有通道分别做时序最大池化,再将这些通道的池化输出值连结为向量。
    3. 通过全连接层将连结后的向量变换为有关各类别的输出。这一步可以使用丢弃层应对过拟合。

    下图用一个例子解释了textCNN的设计。这里的输入是一个有11个词的句子,每个词用6维词向量表示。因此输入序列的宽为11,输入通道数为6。给定2个一维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,一维卷积计算后,4个输出通道的宽为11−2+1=10,而其他5个通道的宽为11−4+1=8。尽管每个通道的宽不同,我们依然可以对各个通道做时序最大池化,并将9个通道的池化输出连结成一个9维向量。最终,使用全连接将9维向量变换为2维输出,即正面情感和负面情感的预测。

    Dive-into-DL-PyTorch
    pytorch代码实现:
    https://github.com/chenpaopao/TextCNN

    总结:

    • 可以使用一维卷积来表征时序数据。
    • 多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。
    • 时序最大池化层的输入在各个通道上的时间步数可以不同。
    • textCNN主要使用了一维卷积层和时序最大池化层。
    https://www.pexels.com/zh-cn/photo/977739/

    torchtext.vocab的学习

    NLP常见的数据预处理工作如下:

    1. Load File:数据文件加载;
    2. Tokenization:分词;
    3. Create Vocabulary:创建字典;
    4. Indexify:将词与索引进行映射;
    5. Word Vectors:创建或加载词向量;
    6. Padding or Fix Length:按长度对文本进行补齐或截取;
    7. Dataset Splits:划分数据集(如将数据集划分问训练集、验证集、测试集);
    8. Batching and Iterators:将数据集按固定大小划分成Batch;

    使用torchtext完成以上工作:

    • 使用torchtext.data.Field定义样本各个字段的处理流程(分词、数据预处理等);
    • 使用torchtext.data.Example将torchtext.data.Field处理成一条样本;
    • 使用torchtext.data.Dataset将torchtext.data.Example处理成数据集,也可对数据集进行划分等工作;
    • 使用torchtext.data.Iterators将torchtext.data.Dataset按照batch_size组装成Batch供模型训练使用;
    • 使用torchtext.data.vocab和torchtext.data.Vectors创建词典、词和索引的一一对应、下载或使用预训练的词向量等;

  • vocab
  • 一句话概括主要是用来建立词汇表创建词典、词和索引的一一对应、下载或使用预训练的词向量等

    常见的词嵌入模型:word2vec Glove

    Pretrained Word Embeddings

    CLASS torchtext.vocab.GloVe(name='840B', dim=300, **kwargs)
    
    CLASS torchtext.vocab.FastText(language='en', **kwargs)
    
    CLASS torchtext.vocab.ChaarNGram(**kwargs)

    返回的实例主要有以下三个属性:

    • stoi: 词到索引的字典:
    • itos: 一个列表,索引到词的映射;
    • vectors: 词向量。

    通过上面的模块,实现由词到向量之间的转换!!!

    vocab.Vocab 是一个词汇表对象(由 下面的vocab 生成 Vocab 对象),使用counter创建词汇表

    collections.Counter 构建词汇表

    class collections.Counter([iterable-or-mapping])

    一个 Counter 是一个 dict 的子类,用于计数可哈希对象。它是一个集合,元素像字典键(key)一样存储,它们的计数存储为值。计数可以是任何整数值,包括0和负数。 Counter 类有点像其他语言中的 bags或multisets。Counter实际上也是dict的一个子类,之不多可以统计不同的值出现的次数。

    CLASS torchtext.vocab.Vocab(counter, max_size=None, min_freq=1, 
      specials=('<unk>', '<pad>'), vectors=None, unk_init=None, vectors_cache=None,
      specials_first=True)
      定义词汇表。属性:Vocab.freqs, Vocab.stoi, Vocab.itos
      __init__(counter,...)
          从 collections.Counter 构建词汇表
      load_vectors(vectors, **kwargs)
          vectors - GloVe, CharNGram, Vectors 实例,或者可用的预训练向量。
      set_vectors(stoi, vectors, dim, unk_init=...)
          从一个张量集合中设置词汇表实例的向量。
          stoi - 字符串 到 `vectors` 相应向量的索引 的字典
          vectors - 该参数支持索引 __getitem__。输入一个索引值,返回索引对应词条的向量(FloatTensor)。
                例如:vector[stoi["string"]] 应该返回 “string" 的词向量。
          dim - 词向量的维度

    torchtext.vocab.vocab 使用dict创建词汇表对象

    torchtext.vocab.vocabordered_dict: Dict , min_freq: int = 1 ) → torchtext.  vocab.Vocab[来源]

    用于创建将标记映射到索引的vocab对象的工厂方法。

    请注意,在构建vocab时,将遵守在ordered_dict中插入键值对的顺序。因此,如果按标记频率排序对用户很重要,则应以反映这一点的方式创建ordered_dict。

    参数

    • ordered_dict – 有序字典将标记映射到它们相应的出现频率。
    • min_freq – 在词汇表中包含一个标记所需的最小频率。

    Returns

    A Vocab objectReturn type

    torchtext.vocab.Vocab

    根据分好词的训练数据集来创建词典,过滤掉了出现次数少于5的词。

    
    #实例
    def get_vocab_imdb(data):
        tokenized_data = get_tokenized_imdb(data)
        counter = collections.Counter([tk for st in tokenized_data for tk in st])
        return Vocab.vocab(counter, min_freq=5)
    
    vocab = get_vocab_imdb(train_data)
    '# words in vocab:', len(vocab)
    
    输出:('# words in vocab:', 46151)
    
    

    SubwordVocab: 构建子词汇表

    CLASS torchtext.vocab.SubwordVocab(counter, max_size=None, specials='<pad>'
        vectors=None, unk_init=...)
      __init__(counter, ...)
          从 collections.Counter 构建子词词汇表,
          specials - padding or eos 等

    Vectors:返回词向量

    Look up embedding vectors of tokens

    CLASS torchtext.vocab.Vectors(name, cache=None, url=None, unk_init=None,
        max_vectors=None)
      __init__(name, cache=None, url=None, unk_init=None, max_vectors=None)
          name - 包含向量的文件名
          cache - 缓存向量文件的目录
          url - 如果缓存中没有找到向量文件,则从该链接下载
          max_vectors - 用于限制加载的预训练此向量的数量。大部分预训练的向量集都是按照词频降序排列
             在整个集合不适合内存或者不需要全部加载的情况下,可以加此限制。
      get_vecs_by_tokens(tokens, lower_case_backup=False)
          用于查找词条的嵌入向量。
          tokens - 一个词条或词条列表。如果一个词条,返回 self.dim 形状的一维张量;如果列表,返回
                 (len(tokens), self.dim)形状的二维张量。
          lower_case_backup -  是否查找小写的词条。如果为True,先按原格式在 `stoi` 查找,
                没找到的话,再查小写格式
    
    examples = ['chip', 'baby', 'Beautiful']
    vec = text.vocab.GloVe(name='6B', dim=50)
    ret = vec.get_vecs_by_tokens(tokens, lower_case_backup=True)

    build_vocab_from_iterator :从迭代器创建vocab

    torchtext.vocab.build_vocab_from_iterator(iterator, num_lines=None)
        从 迭代器 建立词汇表
        iterator - 必须产生词条的列表或迭代器
        num_lines - 迭代器返回元素的预期数量。
    
    
    torchtext.vocab.build_vocab_from_iterator(iterator: Iterable, min_freq: int = 1, specials: Optional[List[str]] = None, special_first: bool = True) → torchtext.vocab.vocab.Vocab[SOURCE]
    Build a Vocab from an iterator.
    
    Parameters
    iterator – Iterator used to build Vocab. Must yield list or iterator of tokens.
    
    min_freq – The minimum frequency needed to include a token in the vocabulary.
    
    specials – Special symbols to add. The order of supplied tokens will be preserved.
    
    special_first – Indicates whether to insert symbols at the beginning or at the end.
    https://pixabay.com/

    python 通过分享的链接下载抖音视频

    """下载抖音无水印的视频"""
    import re
    import requests
    
    useragent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36'
    headers = {'User-Agent': useragent}
    
    
    def dy_url_op(data):
        '''从data中找到url,返回str'''
        url=''
        url=re.findall(r'(https?://[^\s]+)',data)
        return url[0]
    
    def get_real_url(url,headers):
        session = requests.Session()
        req = session.get(url , timeout = 5 , headers = headers)
        vid = req.url.split("/")[4].split("?")[0]
        videoInfo = session.get("https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=" + vid,
            timeout = 5 , headers = headers)
        playAddr = re.findall(r'https://aweme.snssdk.com/aweme[\S]*?"',videoInfo.text)[0][:-1]
        parsedAddr = playAddr.replace("/playwm/","/play/")
        return vid, parsedAddr, session
    
    def downlowd_video(video_url):
        '''下载视频'''
        video_data=requests.get(video_url,headers=headers)
        with open('video.mp4','wb') as f:
            f.write(video_data.content)
    
    print("请输入如下格式:\n8.94 mdN:/ 孙红雷的瓜保熟么?%扫黑风暴开播   https://v.douyin.com/eEPNXH4/ 復制佌鏈接,打开Dou音搜索,直接观看视频!")
    data = input("请输入抖音视频链接:")
    vid, parsedAddr, session = get_real_url(dy_url_op(data),headers)
    downlowd_video(parsedAddr)
    print("下载完成!")

    爬虫过程中URL的变化:

    • 第1个URL(抖音分享的URL):https://v.douyin.com/JuGgK3M/
    • 第2个URL:https://www.iesdouyin.com/share/video/6893421572934159629/……
    • 第3个URL:https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=6893421572934159629
    • 第4个URL(视频URL):
      https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f350000bul5ktfae8n8kibjf990&ratio=720p&line=0