python面试题

独到科技Python笔试

建议时长 30-50min

0.简写3~4条PEP8.0规范

PEP8.0(Python Enhancement Proposal 8.0)协议[规范]

1.Python的内置 [数据结构] 有哪些?各有什么特点?

可以从存储的 [数据类型] 和操作上考虑

2.Python中list与array.array有什么区别?

可以从存储数据的方式和类型考虑

3.什么是迭代器?什么是生成器?

可以从两者的关系和如何避免无限迭代上考虑

4.按照顺序写出下列输出

可以从Python变量赋值方式角度考虑

1
2
3
4
5
6
7
8
temp_1 = [["_"] * 3] *3
temp_2 = [["_"] * 3 for _ in range(3)]

temp_1[0][0] = "X"
temp_2[0][0] = "O"

print(temp_1)
print(temp_2)

可以从三者获取值时所调用的(magic method)上考虑

PS: 如果AttributeError则写Error

1
2
3
4
5
6
7
8
9
list_1 = ["hello", "World", "!"]
list_2 = [1, "", 3]

temp_var = dict(zip(list_1, list_2))

print(temp_var[list_1[0]])
print(temp_var.hello)
print(temp_var.get("World"))
print(temp_var.get("hello, world"))

可以从python函数如何接受可变类型参数上考虑

1
2
3
4
5
6
7
def func(var_1,seq=[]):
seq.append(var_1)
return seq

func(1)
func(1)
print(func(1))

流畅的python

Part0.BulitIn

小知识

处理大数据

  1. 如果是类 -> 减少内存使用__slots__
  2. 如果是数值类型 使用Numpy array memoryview

为什么说repr是开发者模式的str

因为eval(repr(obj)) –> obj

如果class中有self.__attr私有变量, 则无法通过eval(repr(obj))获取obj

1
2
3
4
5
6
7
def repr(obj): # real signature unknown; restored from __doc__
"""
Return the canonical string representation of the object.

For many object types, including most builtins, eval(repr(obj)) == obj.
"""
pass

self|cls

pythoncls代表的是类的本身,相对应的self则是类的一个实例对象。

因为cls等同于类本身,类方法中可以通过使用cls来实例化一个对象。

cls通常于@classmethod搭配使用用于实例化类对象

反射

python的反射,它的核心本质其实就是利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,一种基于字符串的事件驱动!

林奈结构

没有采用一贯的下行二分法体系(系统)而是采用在一个界(kingdom)之内只含有四个阶元层次:纲(class),目(order),属(genus),种(species)的等级结构的体系。

帮助

想知道某个类或者函数有方法以及其例子时, 可以使用dir()或者help()

比如想了解pandas.read_excel如何读取其titledata

1
2
3
4
5
6
7
8
9
10
import pandas as pd
file = pd.read_excel("./students.xls")
columns = list(file.columns)

for data in file.values:
print(data)
>>
['Star' 46599201 85 20]
['Drink' 36784554 90 21]
['Fire' 12492483 96 23]

想了解一个函数或者对象要做什么事情可以使用__annotation__ __doc__ inspect.signature

annotation

1
{'dtype': 'DtypeArg | None', 'storage_options': 'StorageOptions'}

doc

signature

1
(io, sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=False, dtype: 'DtypeArg | None' = None, engine=None, converters=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, parse_dates=False, date_parser=None, thousands=None, comment=None, skipfooter=0, convert_float=None, mangle_dupe_cols=True, storage_options: 'StorageOptions' = None)

下划线

模式 例子 含义
单前导 _var 命名约定, 内部私有变量, 解释器不强制执行, 除import *
单末尾 var_ 命名约定, 避免与关键字冲突
双前导 __var 类的上下位中使用, 名称修饰为_class__var避免属性被意外修改
双前导|双末尾 __var__ magic method
单下划 _ 占位符

format

数字 格式 输出 描述
3.1415926 {:.2f} 3.14 保留小数点后两位
3.1415926 {:+.2f} +3.14 带符号保留小数点后两位
-1 {:+.2f} -1.00 带符号保留小数点后两位
2.71828 {:.0f} 3 不带小数
5 {:0>2d} 05 数字补零 (填充左边, 宽度为2)
5 {:x<4d} 5xxx 数字补x (填充右边, 宽度为4)
10 {:x<4d} 10xx 数字补x (填充右边, 宽度为4)
1000000 {:,} 1,000,000 以逗号分隔的数字格式
0.25 {:.2%} 25.00% 百分比格式
1000000000 {:.2e} 1.00e+09 指数记法
13 {:>10d} 13 右对齐 (默认, 宽度为10)
13 {:<10d} 13 左对齐 (宽度为10)
13 {:^10d} 13 中间对齐 (宽度为10)
11 '{:b}'.format(11) '{:d}'.format(11) '{:o}'.format(11) '{:x}'.format(11) '{:#x}'.format(11) '{:#X}'.format(11) 1011 11 13 b 0xb 0XB 进制

^, <**, **> 分别是居中、左对齐、右对齐,后面带宽度, : 号后面带填充的字符,**只能是一个字符[即只能填充一次]**,不指定则默认是用空格填充。

1
2
3
4
5
# 即长度为多少
str = "1{:15}".format("&")
print(string_1, len(str))
|1& |
16

+ 表示在正数前显示 **+**,负数前显示 **-**; (空格)表示在正数前加空格

b、d、o、x 分别是二进制、十进制、八进制、十六进制。

1
2
3
4
5
6
# {} format
# {0} format the index
# {0:2d} format the type --> d int 2d: remain two int
# {<2d} 左对齐,宽度为2d,不足补空格
ROW_FMT = "{0:2d} @ {1:2d} {2} {0:<2d}"
print(ROW_FMT.format(needle, posotion, offset))

Dis常用字节码

Name args Info
LOAD_GLOBAL namei 加载名称为 co_names[namei] 的全局对象推入栈顶。
LOAD_FAST var_num 将指向局部对象 co_varnames[var_num] 的引用推入栈顶。
LOAD_CONST consti co_consts[consti] 推入栈顶。
CALL_FUNCTION argc 调用一个可调用对象并传入位置参数。 argc 指明位置参数的数量。 栈顶包含位置参数,其中最右边的参数在最顶端。 在参数之下是一个待调用的可调用对象。 CALL_FUNCTION 会从栈中弹出所有参数以及可调用对象,附带这些参数调用该可调用对象,并将可调用对象所返回的返回值推入栈顶。在 3.6 版更改: 此操作码仅用于附带位置参数的调用。
POP_TOP 删除堆栈顶部(TOS)项
STORE_FAST var_num 将 TOS 存放到局部对象 co_varnames[var_num]
RETURN_VALUE 返回 TOS 到函数的调用者。

Attention

函数|方法返回

python 会对operation返回return None来表示对对象的就地更改, 即不会id(obj)不会变

  • 用返回None来表示就地改动有一个弊端, 即调用者无法将其串联起来; 而返回一个新对象的方法(比如说str里的所有方法)则正好相反, 他们可以串联起来调用

sort

sort分为两种, 一种是value sort即根据值进行排序, 另一种是time sort即就算两个元素比不出大小, 但在每次排序的结果里他们的相对位置是固定的

推导式

  • 一般推导式的作用 [] {}
    • 创建新的数据对象
    • 过滤作用

Q&A

1.python中list和array的区别

  • list是python的内置数据类型
  • list中的数据类不必相同的
  • array的中的类型必须全部相同
  • 在list中的数据类型保存的是数据存放的地址,简单的说就是指针,并非数据
  • 这种保存方式增加了存储和CPU的消耗例如list1=[1,2,3,’a’]需要4个指针和四个数据
  • 数组在背后存的并不是float对象,而是数字的机器翻译,也就是字节表述。这一点和c语言中的数组一样
  • array创建的数组不适用于数字操作(比如矩阵和矢量运算)。另外+=和*=运算符可以用于array的添加。
  • 从python3.4开始,数组(array)类型不再支持诸如list.sort()这种就地排序方法。要给数组排序的话,得用sorted函数新建一个数组:

Magic Method

表1:跟运算符无关的 magic method

类别 方法名
字符串/字节序列表示形式 repr str format bytes
数值转换 abs bool complex int float hash index
集合模拟 len getitem setitem delitem contains
迭代枚举 iter reversed next
可调用模拟 call
上下文管理 enter exit
实例创建/销毁 new init del
属性管理 getattr getattribute setattr delattr dir
属性描述符 get set delete
跟类有关的服务 prepare instancecheck subclasscheck

表2:跟运算符相关的 magic method

类别 方法名和对应的运算符
一元运算符 neg - pos + abs abs()
众多比较运算符 lt < le <= eq == ne != gt > ge >=
算术运算符 add + sub - mul * truediv / floordiv // mod % divmod divmod() pow **|pow() round round()
反向算术运算符 radd rsub rmul rturediv rfloordiv rmod rdivmod rpow
增量赋值算术运算符 iadd isub imul itruediv ifloordic imod ipow
位运算符 invert ~ lshift << rshift >> and & or | xor ^
反向位运算符 rlshift rrshift rand rxor ror
增量赋值位运算符 ilshift irshift iand ixor ior
1
注: 以下所有的keyword默认为 self.__xx__():

bool|len

默认情况下我们定义的类的实例总是被认为True,除非在define class中有实现bool或者len

bool()的背后其实是调用x.__bool__()的结果;如果x.__bool__()不存在, 则调用len()x.__len__(),如果为0-False other-True

调用优先级为bool > len

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# define a class
class temp:

def __init__(self):
self.value = 1

def __len__(self):
return 0

!def __bool__(self):
! return True
>
bool(temp) False
? bool(temp) True

and|mul

and --> + add

mul --> * multiple

str|repr

两者的却别在于str will called by str() and print(), repr can only called by print()

如果两者特殊方法保留一个repr会是更好的选择, 因为在没有str时, 解释器会调用repr

优先级str > repr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# define a class
class temp:

def __init__(self):
self.value = 1

def __repr__(self):
return f"repr value is {self.value}"

def __str__(self):
return f"str value is {self.value}"

test = temp()
print(test)

H:\Python\python.exe H:/MY_TXM/test.py
str value is 1

Moudle

functools

recude

对参数序列中元素进行累积。

函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

1
2
3
sum2 = reduce(lambda x, y: x+y, [1,2,3,4,5])
>>
15 = 1 + 2 + 3 + 4 + 5

os

listdir

列出目录下的所有文件和文件夹不包括. ..隐藏目录

1
2
3
4
import os
print(os.listdir("H:\MY_TXM"))
>>
['.git', '.gitignore', '.idea', 'static', 'venv']

locale

locale 是根据计算机用户所使用的语言,所在国家或者地区,以及当地的文化传统所定义的一个软件运行时的语言环境。通常情况下它可以按照涉及使用习惯分为12大类:

  • 语言符号及其分类(LC_CTYPE)
  • 数字(LC_NUMBERIC)
  • 比较习惯(LC_COLLATE)
  • 时间显示格式(LC_TIME)
  • 货币单位(LC_MONETARY)
  • 信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES)
  • 行么书写方式(LC_NAME)
  • 地址书写方式(LC_ADDRESS)
  • 电话号码书写方式(LC_TELEPHONE)
    -度量衡表达方式(LC_MEASUREMENT)
  • 默认纸张尺寸大小(LC_PAPER)
  • 对locale 自身包含信息的概述(LC_IDENTIFICATION)
  • 除此之外还有一个LANGUAGE参数,它与LC_MESSAGES相似

time

time

process_time

process_time_ns

perf_counter

perf_counter_ns

time()精度上相对没有那么高,而且受系统的影响,适合表示日期时间或者大程序程序的计时。

perf_counter()适合小一点的程序测试,会计算sleep()时间。

process_counter()适合小一点的程序测试,不会计算sleep()时间。

此外Python3.7开始还提供了以上三个方法精确到纳秒的计时。分别是:

1
2
3
time.perf_counter_ns()
time.process_time_ns()
time.time_ns()

arrary

数组所接受的typecode以及其对应的存储数据类型

Type code C Type Python Type Minimum size in bytes Notes
‘b’ signed char int 1
‘B’ unsigned char int 1
‘u’ Py_UNICODE Unicode character 2 (1)
‘h’ signed short int 2
‘H’ unsigned short int 2
‘i’ signed int int 2
‘I’ unsigned int int 2
‘l’ signed long int 4
‘L’ unsigned long int 4
‘q’ signed long long int 8 (2)
‘Q’ unsigned long long int 8 (2)
‘f’ float float 4
‘d’ double float 8

bisect

bisect是一个根据二分算法写的库,其中主要的俩个方法是bisect和insert,根本上根据二分算法算出有序序列的索引,可以当做快速定位index使用

bisect|bisect_left

接受一个有序序列,一个元素,返回该元素在有序序列的索引,索引以前全≤该元素,bisect(bisect_right)|bisect_left主要区别是如果俩对比元素相等,前者返回index是从右边插入,后者左边

1
2
3
4
5
6
7
8
9
import bisect
temp_sequence = [0, 2, 3]
index = bisect.bisect(temp_sequence, 2)
_index = bisect.bisect_left(temp_sequence, 2)

print(index)
print(_index)
2
1

insort

根据bisect或者bisect_left返回的index进行插入,接受有序序列,返回有序序列

itertools

groupby

nlargest() nsmallest()一样支持key=

【聚合】

有时候我们需要给一个sequence按照某个属性分组,可以借助groupby来实现,groupby常常和lambda map operator.itemgetter一起使用,因为在分组前,大多希望相关的数据聚集在一起, 这样对于groupby来说分组才有意义

语法:

1
2
3
4
5
groupby(seqence, key)
args:
sequence must iterable
key sort the sequence 聚合
return iterator contains key, data --> type tuple

注意:

  1. 如果groupby中的key=None,那么group后的key是被group中的element, 如果key=fun那么group后的key是fun(element)
  2. 返回的数据
    • type(key) type(element)
    • type(data) iterator

实列:

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
a = [{'severity': '严重'}, {'severity': '严重'}, {'severity': '不严重'}, {'severity': '严重'}]

# 如果不将数据进行排序直接group
for key, data in groupby(a):
print(key, list(data))

# 将会输出三组key
"""
{'severity': '严重'} [{'severity': '严重'}, {'severity': '严重'}]
{'severity': '不严重'} [{'severity': '不严重'}]
{'severity': '严重'} [{'severity': '严重'}]
"""

# 对数据进行聚合(排序)但不对group的key进行筛选
a.sort(key=itemgetter("severity"))
for key, data in groupby(a):
print(key, list(data))

# 此时的key时a中的单个element
"""
{'severity': '不严重'} [{'severity': '不严重'}]
{'severity': '严重'} [{'severity': '严重'}, {'severity': '严重'}, {'severity': '严重'}]
"""

# 对数据进行聚合(排序)且对group的key进行筛选
a.sort(key=itemgetter("severity"))
for key, data in groupby(a, key=itemgetter("severity")):
print(key, list(data))

# 此时的group返回的key是经过滤后的key
"""
不严重 [{'severity': '不严重'}]
严重 [{'severity': '严重'}, {'severity': '严重'}, {'severity': '严重'}]
"""

operator

itemgetter

使用[]运算符, 不仅支持序列还支持映射和任何实现了__getitem__方法的类

operator模块提供的itemgetter函数主要用于获取某一对象 特定维度的数据,其中的参数为特定维度的索引

operator.itemgetter函数获取的并不是某一个数值,而是某一个函数常常可以使用lambda 函数替换

用法:

1
2
3
4
itemgetter(var)
args:
var can accept index and key
return fun()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import operator

a = [{"age": 10, "socre": 90}, {"age": 9, "socre": 98}, {"age": 0, "socre": 50}, {"age": 15, "socre": 56}]

# 定义一个获取维度数据的函数
# 获取sequence的index=0&1的数据
condition = operator.itemgetter(0, 1)

# 将sequence传入获取函数中
res = condition(a)
print(res, type(a))
({'age': 10, 'socre': 90}, {'age': 9, 'socre': 98}) <class 'tuple'>

# 对a的age进行排序
a.sort(key=itemgetter("age"))
a.sort(key=lambda x: x["socre"], reverse=True)
print(a)
[{'age': 0, 'socre': 50}, {'age': 9, 'socre': 98}, {'age': 10, 'socre': 90}, {'age': 15, 'socre': 56}]

# 先对age排序在对score排序
a.sort(key=itemgetter("age", "socre"), reverse=True)

headq

nlargest

nsmallest

groupby()一样支持key=

介绍:

这两个函数可以帮助我们在某个集合中找出最大或最小的N个元素

语法:

1
2
3
4
def nlargest(n, iterable, key=None):
"""Find the n largest elements in a dataset.
Equivalent to: sorted(iterable, key=key, reverse=True)[:n]
"""

random

  • 注意如果在random.method之前使用了seed,那么random.method每次都将按照一定规则返回相同的数值

shuffle

语法:

1
2
3
4
5
random.shuffle(iterable)
args:
iterbale a list or tuple (sequence)
return:
the origin sequence but the position of element is random ==> None

实列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random

a = [1, 2, 3, 4, 5]
print(a, id(a))

# operation in ori return None
random.shuffle(a)

print(a, id(a))

>>>
[0, 1, 2, 3, 4] 2200356865544
None
[0, 3, 4, 1, 2] 2200356865544

seed

调用 random.random()|shuffle 生成随机数时,每一次生成的数都是随机的。
但是,当使用 random.seed(x) 设定好种子之后,其中的 x 可以是任意数字,这个时候,先调用seed的情况下,使用 random() 生成的随机数将会是同一个

randrange

从指定范围内,按指定基数递增的集合中 获取一个随机数。

random.randrange([start], stop[, step])

1
2
# 结果相当于从[10, 12, 14, 16, … 96, 98]序列中获取一个随机数。
random.randrange(10, 100, 2)

sample

random.sample的函数原型为:random.sample(sequence, k)从指定序列中随机获取指定长度的片断。sample函数不会修改原有序列。

1
2
3
4
5
6
7
8
9
temp = [i for i in range(10)]
print(temp, id(temp))
random_choice = random.sample(temp, 3)
print(random_choice, id(random_choice))
print(id(temp))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 2483596469896
[7, 8, 3] 2483594327944
2483596469896

dis

dis.dis

Python代码在执行的时候,会被编译为Python字节码,再由Python虚拟机执行Python字节码。有时候就我们执行python文件的时候会生成一个pyc文件,这个pyc文件即用于存储Python字节码指令,而这些字节码是一种类似于汇编指令的中间语言,但是每个字节码对应的不是机器指令,而是一段C代码。

而dis模块,就是用于查看这些字节码的运行轨迹,因此我们可以用dis模块判断两个函数的内存占用谁会更大,谁会更消耗CPU性能,不仅如此,通过指令,我们还可以知道Python中一些内置函数、变量的取值过程、运行逻辑,对于我们代码性能并优化代码很有帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
import dis
dis.dis("ori_tuple[index] += mutable_sequence")

1 0 LOAD_NAME 0 (ori_tuple)
2 LOAD_NAME 1 (index)
4 DUP_TOP_TWO
6 # 实现 TOS = TOS1[TOS]
8 LOAD_NAME 2 (mutable_sequence)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE

字节码操作:

  • 假设

    • ori_tuple = (1, 2, [10, 20])
    • index = 2
    • mutable_sequence = [30, 40]
  • 6 BINARY_SUBSCR 将 ori_tuple[index]的值存入TOS(Top Of Stack)

  • 10 INPLACE_ADD 计算ori_tuple[index] += mutable_sequence, 这一步骤可以完成, 是因为TOS指向的是一个可变对象

  • 14 STORE_SUBSCR 赋值, 这一步骤失败, 是因为tuple属于不可变序列(immutable)


我们从这个操作中可以得到三个教训:

  1. 不要把可变对象放在元组中
  2. 增量赋值(+=)不是一个原子操作, 在计算完后的赋值阶段如果抛出异常, 计算仍然会完成

譬如:

1
2
3
4
5
6
7
b = (1, 2, [10, 20])
b += [30, 40]
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
print(b)
(1, 2, [10, 20])

在上述实列中, 即使在最后的赋值阶段tuple抛出异常, 但是此时tuple的数据依然发生了改变!

Python字节码 name(variable) 执行操作
LOAD_NAME 将与 co_names[namei] 相关联的值推入栈顶。
DUP_TOP_TWO 复制堆栈顶部的两个引用,使它们保持相同的顺序。
BINARY_SUBSCR 实现 TOS = TOS1[TOS]
INPLACE_ADD 就地实现 TOS = TOS1 + TOS
ROT_THREE 将第二个和第三个堆栈项向上提升一个位置,顶项移动到位置三。
STORE_SUBSCR 实现 TOS1[TOS] = TOS2
LOAD_CONST co_consts[consti] 推入栈顶
RETURN_VALUE 返回 TOS 到函数的调用者。

sys

getsizeof

1
2
getsizeof(object, default) -> int
Return the size of object in bytes.

获取对象所消耗的内存大小

argv

是一个从程序外部获取参数的桥梁,从外部取得的参数可以是多个,所以获得的是一个列表(list),也就是说sys.argv其实可以看作是一个列表,所以才能用[]提取其中的元素。其第一个元素是程序本身,随后才依次是外部给予的参数。

memoryview

memoryview**[内存视图]是一个内置类, 它能让用户在不复制内容的情况下操作同一个数组的不同切片,其实内存试图时泛化和去数学化的numpy数组**,它允许你在不复制内容的前提下,在数据结构之间共享内存, 其中数据结构可以是任何形式,这个功能在处理大型数据集合的时候十分重要

memoryview 语法:

1
memoryview(obj)

参数说明:

  • obj – 对象

返回元组列表(物理地址)。

.cast

memoryview.cast的概念和数组模型差不多,能用不同的方式读写同一块内存地址,而且内容字节不会随意移动,和C语言中类型转换的概念相似.cast会把同一块内存里的内容打包成一个全新的memoryview对象给你

Method

all|any

都接受一个可迭代对象, 其中all是当可迭代对象中的元素都是True时返回True, any是有一个为True时则返回True

1
2
3
4
5
6
7
d = all(map(lambda x: x > 10, [1, 10, 11]))
print(d)
f = any(map(lambda x: x > 10, [1, 10, 11]))
print(f)
>>
False
True

eval

描述

eval() 函数用来执行一个字符串表达式,并返回表达式的值。

语法

以下是 eval() 方法的语法:

1
eval(expression[, globals[, locals]])

参数

  • expression – 表达式。
  • globals – 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
  • locals – 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。

返回值

返回表达式计算结果。

isatty

isatty() 方法检测文件是否连接到一个终端设备,如果是返回 True,否则返回 False。

语法

isatty() 方法语法如下:

1
fileObject.isatty()

assert

python断言和try-except用法相反

断言函数是对表达式布尔值的判断,要求表达式计算值必须为真。可用于自动调试。

如果表达式为假,触发异常;如果表达式为真,不执行任何操作。

1
2
3
assert condition
if not condition:
raise AssertionError()

reverse

reversed

  1. s.reverse()就地排序, id(s)不变
  2. reversed(s)返回s倒序迭代器

id

返回对象的物理存储地址, 一般用作操作符是或否创建了一个新对象, 比较两个对象的物理地址是否相同使用is

map

提供的函数对指定序列做映射。

1
map(function, iterable, ...)

filter

filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。

Python2.7 返回列表,Python3.x 返回迭代器对象

1
2
filter(function, iterable)
# 通常和map函数一起使用

Python2.x 中返回的是过滤后的列表, 而 Python3 中返回到是一个 filter 类。

filter 类实现了 iternext 方法, 可以看成是一个迭代器, 有惰性运算的特性, 相对 Python2.x 提升了性能, 节约内存。

abs|fabs[math]

Python fabs(x) 方法返回 x 的绝对值。虽然类似于 abs() 函数,但是两个函数之间存在以下差异:

  • abs() 是一个内置函数,而 fabs()math 模块中定义的。

  • fabs() 函数只适用于 floatinteger 类型,而 abs() 也适用于复数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import math

a = -1
b = -1.3232
c = b
d = 1 + 1.0j
e = 3 + 4.0j
print("a的绝对值是:", abs(a))
print("b的绝对值是:", abs(b))
print("c的绝对值是:", math.fabs(c))
print("d的的绝对值是:", abs(d))
print("e的绝对值是:", math.fabs(e))

Traceback (most recent call last):
File "H:/MY_TXM/test.py", line 80, in <module>
print("e的绝对值是:",math.fabs(e))
TypeError: can't convert complex to float

hypot[math]

hypot() 返回欧几里德范数 sqrt(x*x + y*y)

hypot()是不能直接访问的,需要导入 math 模块,然后通过 math 静态对象调用该方法。

1
2
3
4
5
6
7
8
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import math

a = int(input("请输入直角三角形的一条直角边长:"))
b = int(input("请输入直角三角形的另一条直角边长:"))
print("该直角三角形的斜边长为: ", int(math.hypot(a, b)))
print("Done")

bool

bool() 函数用于将给定参数转换为布尔类型,如果没有参数,返回 False用于判断真假

bool int 的子类。

1
2
>>> issubclass(bool, int)  # bool 是 int 子类
True

issubclass

issubclass(class, classinfo) --> return True|False

issubclass() 方法用于判断参数 class 是否是类型参数classinfo的子类

Part1.DataModel

1.具名元组

Python元组的升级版本 – namedtuple(具名元组)

因为元组的局限性:不能为元组内部的数据进行命名,所以往往我们并不知道一个元组所要表达的意义,所以在这里引入了 collections.namedtuple 这个工厂函数,来构造一个带字段名的元组。具名元组的实例和普通元组消耗的内存一样多,因为字段名都被存在对应的类里面。这个类跟普通的对象实例比起来也要小一些,因为 Python 不会用 dict 来存放这些实例的属性。

namedtuple 对象的定义如以下格式:

collections.namedtuple(typename, field_names, verbose=False, rename=False)
返回一个具名元组子类 typename,其中参数的意义如下:

  • typename:元组名称
  • field_names: 元组中元素的名称
  • rename: 如果元素名称中含有 python 的关键字,则必须设置为 rename=True
  • verbose: 默认就好

下面来看看声明一个具名元组及其实例化的方法:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import collections

两种方法来给 namedtuple 定义方法名
User = collections.namedtuple('User', ['name', 'age', 'id'])
User = collections.namedtuple('User', 'name age id')
user = User('tester', '22', '464643123')

print(user)
collections.namedtuple('User', 'name age id') 创建一个具名元组,需要两个参数,一个是类名,另一个是类的各个字段名。后者可以是有多个字符串组成的可迭代对象,或者是有空格分隔开的字段名组成的字符串(比如本示例)。具名元组可以通过字段名或者位置来获取一个字段的信息。

输出结果:

User(name='tester', age='22', id='464643123')
具名元组的特有属性:

类属性 _fields:包含这个类所有字段名的元组 类方法 _make(iterable):接受一个可迭代对象来生产这个类的实例 实例方法 _asdict():把具名元组以 collections.OrdereDict 的形式返回,可以利用它来把元组里的信息友好的展示出来
from collections import namedtuple

# 定义一个namedtuple类型User,并包含name,sex和age属性。
User = namedtuple('User', ['name', 'sex', 'age'])

# 创建一个User对象
user = User(name='Runoob', sex='male', age=12)

# 获取所有字段名
print( user._fields )

# 也可以通过一个list来创建一个User对象,这里注意需要使用"_make"方法
user = User._make(['Runoob', 'male', 12])

print( user )
# User(name='user1', sex='male', age=12)

# 获取用户的属性
print( user.name )
print( user.sex )
print( user.age )

# 修改对象属性,注意要使用"_replace"方法
user = user._replace(age=22)
print( user )
# User(name='user1', sex='male', age=21)

# 将User对象转换成字典,注意要使用"_asdict"
print( user._asdict() )
# OrderedDict([('name', 'Runoob'), ('sex', 'male'), ('age', 22)])
以上实例输出结果为:

('name', 'sex', 'age')
User(name='Runoob', sex='male', age=12)
Runoob
male
12
User(name='Runoob', sex='male', age=22)
OrderedDict([('name', 'Runoob'), ('sex', 'male'), ('age', 22)])

2.列表表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 两者相等
print([(i, j) for i in range(1, 10) for j in range(11, 20)])
res = []
for i in range(1, 10):
for j in range(11, 20):
res.append((i, j))
print(res)

# essential
fun() for-in-loop_1 for-in-loop_2
<==>
for-in-loop_1:
for-in-loop_2:
fun()

3.random choice

1
2
3
from random import choice
choice(obj) -> obj: can iterable
==> this module can choice a random element from obj

4.Magic Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
obj[key]
本质上调用的是__getitem__

magic method 又称为 dunder method 及双下滑线魔法方法

for-in-loop
==> __contains__ 迭代通常是隐式的

obj[key] --> self.__getitem__

迭代通常是隐式的,如果一个集合类型没有实现slef.__contains__,
那么in运算符就会按照顺序做一次迭代搜索

len(obj) --> self.__len__
注意如果obj是python内置的类型比如(liststr、bytearry)等Cpython会抄个近路,__len__实际上会直接返回PyVarObject的self.ob_size这个属性,而不是执行self.__len__这个方法

for item in x: --> self.__iter__
x 必须是可迭代的

5.迭代器与生成器

5.1迭代器

迭代是Python最强大的功能之一,是访问集合元素的一种方式。

迭代器是一个可以记住遍历的位置的对象。

迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

迭代器有两个基本的方法:iter() 和 **next()**。

字符串,列表或元组对象都可用于创建迭代器:

1
2
3
4
5
6
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象
print (next(it)) # 输出迭代器的下一个元素
>1
print (next(it))
>2

迭代器对象可以使用常规for语句进行遍历

1
2
3
4
5
6
7
8
#!/usr/bin/python3

list=[1,2,3,4]
it = iter(list) # 创建迭代器对象
for x in it:
print (x, end=" ")

> 1 2 3 4

也可以使用 next() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python3

import sys # 引入 sys 模块

list=[1,2,3,4]
it = iter(list) # 创建迭代器对象

while True:
try:
print (next(it))
except StopIteration:
sys.exit()

5.2创建一个迭代器

把一个类作为一个迭代器使用需要在类中实现两个方法 iter() 与 next()

在定义类的时候都有一个构造函数,Python 的构造函数为 init(), 它会在对象初始化的时候执行。

iter() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 next() 方法并通过 StopIteration 异常标识迭代的完成。

next() 方法(Python 2 里是 next())会返回下一个迭代器对象。

创建一个返回数字的迭代器,初始值为 1,逐步递增 1:

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
35
36
class Numbers():
"""
抽象一个迭代器对象,初始属性【value=0每次next自增1,并通过【value>20】来rais StopInteration
"""

def __iter__(self):
self.value = 0
# 注意这里返回的不是属性self.value而是self这个抽象对象
return self

def __next__(self):
if self.value <= 20:
self.value += 1
# 这里我们需要输出值,print进行查看
return self.value
else:
raise StopIteration

# ------

temp = Numbers()
temp_iter = iter(temp) # 这一步骤可以省略,因为temp在实例化的时候已经是一个迭代器了(contains magic method) iterm | next

# 可以直接for-in-loop temp
for item in temp_iter:
print(item)
> 1 2 ... 21

# 将俩者的信息进行打印如下:
print(f"the obj is {temp} , type is {type(temp)}")
print(f"the iter is {temp_iter} , type is {type(temp)}")

>>
the obj is <__main__.Numbers object at 0x0000016FFBD51088> , type is <class '__main__.Numbers'>
the iter is <__main__.Numbers object at 0x0000016FFBD51088> , type is <class '__main__.Numbers'>
# 可以看到两者的物理地址相同,因此iter(OBJ)和OBJ一样都是属于迭代器

5.3生成器

在 Python 中,使用了 yield 的函数被称为生成器(generator)。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。

以下实例使用 yield 实现斐波那契数列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python3

import sys

def fibonacci(n): # 生成器函数 - 斐波那契
a, b, counter = 0, 1, 0
while True:
if (counter > n):
return
yield a
a, b = b, a + b
counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成

while True:
try:
print (next(f), end=" ")
except StopIteration:
sys.exit()
# 每次next(generator)所生成的迭代器对象公用和一个物理地址

5.4迭代器和生成器对比

  • 迭代器(iterator)是一个特殊的类,其中必有iterm和next方法,本质上是一个类|对象

    • 除了使用class来定义一个迭代器外, 还可以使用iterm(obj)来快速生成一个迭代器对象
    • 可以使用rais StopIteration来抛出exception阻止迭代器进入死循环
    • 是一个可以记住遍历的位置的对象(本质上含有iterm和next)
  • 生成器是一个含有yield的函数(generator),生成器是一个返回迭代器的函数,只能用于迭代操作,其实生成器也可以理解成一种特殊的迭代器

    • 生成器是一个返回迭代器的函数
    • 在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
    • 调用一个生成器函数,返回的是一个迭代器对象

6.len为什么不是普通方法

如果x是一个python内置类型的实例, 那么len(x)中Cpython会直接调用这个类的属性,解释器将不走__len__()这个方法

杂谈

  1. 数据模型|对象模型

    • python文档中大多使用数据模型, 而大多数喜欢使用对象模型
    • 对象模型:计算机编程语言中对象的属性<==>数据模型
  2. 魔法方法

    • python用这个方法来提供更多的元对象协议,目的是让语言的使用者和开发者拥有并使用同样的工具
  3. 元对象

    the art of the MetaObject Protocal | AMOP

    • 元对象协议:那些对构建语言本身来讲很重要的对象, 以此为前提, 协议也可以看成是接口, 即是说, 元对象协议是对象模型(数据模型)的同义词, 他们的本质都是构建核心语言的API

Part2.DataStructure

2.0前言

在创建poython之前Guido曾为ABC语言贡献过代码, ABC语言是一个致力于为初学者设计变成环境的长达10的研究项目, 其中很多点子在现在看来任然具有python的特性如:

  1. 序列的泛型操作
  2. 内置的元组
  3. 映射类型
  4. 用缩进来构建源码
  5. 无需变量声明的强类型

python也从ABC继承了用统一风格处理序列数据这一特点, 无论是中数据结构(string|list|bytes|tuple|array|xml|element|database query)他们都公用一套操作:

  1. 迭代
  2. 切片
  3. 排序
  4. 拼接

2.1内置序列类型

python使用C实现了两大类序列类型:容器序列 扁平序列

两者的却别前者支持存储不同类型的数据; 后者仅支持一种类型的数据

容器序列: list tuple collection.deque

  • 存放的是任意类型的对象的引用
  • 支持存储不同数据类型
  • 非连续内存存储

扁平序列: str bytes bytearray memoryvivew arrary.arrary

  • 存放的是值而不是引用
  • 只能存储一种数据类型[字符 字节 数值]
  • 连续存储

根据是否可以被修改可以分为:可变序列 不可变序列

从UML图中可以看出两者之间的部分继承关系,但是内置的序列类型不是直接sequencemutale sequence这两个抽象基类继承来的Abstarct Base Class | ABC

immutable sequence mutable sequence
*getitem y
*setitem y
*delitem y
*contains y
*iter y
*reversed y
*iadd y
index y
insert y
append y
reverse y
extend y
pop y
remove y
count y

2.2 列表推导和生成器表达

list comprehension and generator expression

[]: 列表推导式

(): 生成器表达式

列表推导式: 快速构建list

使用原则:

  1. 列表推导式的作用只有一个:生成列表

  2. 创建新列表

  3. 尽量保持简短

  4. py3.x中修复了列表推导是中的变量泄露问题(通过局部作用域修复)

  5. 使用filter + map也可以达到相同效果

  6. 注意: filter返回的是一个迭代器

  7. 两者的效率根据不同情况表现不一样

1
2
3
4
5
6
7
temp = '!@#$%'

m1 = [ord(s) for s in temp if ord(s) > 35]
print(m1)

m2 = list(filter(lambda x: x > 35, map(ord, temp)))
print(m2)

2.2.1 笛卡尔积

1
2
3
4
5
6
7
8
9
col_1 = ["A", "B", "c"]
col_2 = ["!", "@", "#"]

col_3 = [(i, j) for i in col_1 for j in col_2]
print(col_3)

for i in col_1:
for j in col_2:
print(i, j)

列表推导式和双层for循环俩者执行顺序一样

2.2.2生成器表达式

使用列表推导也可以初始化元组|数组|其他序列,但是生成器表达式是一个更好的选择, 因为背后支持迭代器协议,可以逐个产出元素而非先建立一个完整的list,可以更好的节省内存

生成器表达式和列表推导式的区别是:

  • [] ()
  • 生成一个完整的list; 迭代器

2.3 元组

元组: 是一个不可变的list; 记录一定的维度[位置]的数据信息

元组的拆包

本质上是将元组中的数据提取

  1. 元组的拆包方式在任意可迭代对象中都是通用的

  2. for-loop

  3. *

    • *拆包返回的是list
  4. 平行赋值

  5. a, b = b, a 本质上也是拆包

  6. 可以使用_占位符来过滤掉不需要的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.for-loop
a = (1, 2, 3)
for i in a:
print(i)

# 2.*
# 提取元素
i, *j = a
# 将可迭代对象拆开作为函数的参数
b = (3, 4)
divmod(*b)

# 平行赋值
i, j, m = a

2.3.1 具名元组

collections.namedtuple构建的类的实例所消耗的内存和元组一样, 因为字段名都存在了相应的类里

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
# namedtuple
from collections import namedtuple

# define a class
# namedtuple(str, iterator)
# >>> str: the name of class
# >>> iterator: define the attributes of class
# >>> iterator(if accept str): "name age score" split whith ' '
student = namedtuple('student', ['name', 'age', 'score'])

# create some instances
drink = student('Drink', '21', '90')
starfire = student('starfire', '23', '92')
spark = student('spark', '24', '95')

print(drink)
print(starfire)
print(spark)

student(name='Drink', age='21', score='90')
student(name='starfire', age='23', score='92')
student(name='spark', age='24', score='95')

# we can get the specify value by [] or .attr
print(drink[0])
print(drink.name)

Drink
Drink

除了从普通元组继承的属性之外, 具名元组还有独特的属性:_fields类属性|类方法|_make(iterable)和实列方法_asdict()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
# get all attrs of drink
print(drink._fields)

('name', 'age', 'score')

# exchange drink into dict
drink_dict = drink._asdict()
for key, value in drink_dict.items():
print(key, ":", value)

name : Drink
age : 21
score : 90

# accept date generate an instance
data = ("star", "20", "60")
star = student._make(data)
print(star)

student(name='star', age='20', score='60')

2.3.2 不可变列表[元组]

元组支持列表的大多数操作除了增删改元素之外

2.4切片

  1. 切片支持大多数可变序列
  2. 切片的数学表达式为[<= <)
  3. 区间运算start default=0
  4. s[a:b:c] start end step其中step可以为负即反向取值

本质上在使用slice取值时, python会调用seq.__getitem__(slice(start, end, step))

或者说slice()是一个切片对象, 可以改切片命名就像excel--sheet一样

1
2
3
4
5
name_slice = slice(0, 5)
a = "drink 21 98"
print(a[name_slice])

drink

2.4.1 多维切片|省略

多维切片所支持的数据序列也是多维度,而python内置的数据序列都是一维的

本质上多维切片seq[i, j]| seq[m:n, i:j]调用的是seq.__getitem__((i, j))或者seq.__setitem((i, j))

省略...在python解释器中是一个符号,其实本质上Ellipsis对象的别称

如果x是四维数组,那么x[i:...]就是x[i:::]的缩写

2.4.2 切片赋值

  • 通过切片赋值右边必须是可迭代对象

2.5 对序列使用+和*

  • +*都遵守一条规律即不修改原有的操作对象而是创建一个全新的序列

  • +通常是相同类型的数据

  • 在操作中python会创建一个包含相同类型数据的序列作为拼接的结果

  • 注意在使用seq * n操作时, 如果seq里的元素是对其他可变对象的引用的话, 虽然* n拼接了, 但本质上还是对seq的引用, 对其进行操作时很可能会出现”多重操作”

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = [["_"] * 3 for i in range(3)]

b = [["_"] * 3] * 3

a[1][2] = "X"
b[1][2] = "O"

print(a)
print(b)

[['_', '_', '_'],
['_', '_', 'X'],
['_', '_', '_']]

[['_', '_', 'O'],
['_', '_', 'O'],
['_', '_', 'O']]

其中b时三个指向同一对象的引用

b所犯的错误和下面的错误一样

1
2
3
4
5
6
7
8
9
10
11
# list中追加的是对同一对象的三次引用
row = ["_"] * 3
board = []
for i in range(3):
board.append(row)

# should optimize to this
board = []
for i in range(3):
row = ["_"] * 3
board.append(row)

2.6 序列的增量赋值

即就地增量 += *= (仅针对可变序列), 不可变序列增量拼接的话实现的是 + * 每次都会创建一个新对象,效率会很低(str除外, 因为对于str的拼接操作过于频繁, CPython对其进行优化)

在调用这些运算符中, python优先调用__iadd__|__imul__如果没有会次级调用__add__|__mul__

需要注意的是:

如果a实现了__iadd__那么+=会实现就地增量赋值,像a.extend(iterator) 否则会调用__add__, 此时不是就地增加,而是首先计算a + b得到一个新的对象, 然后再将对象赋值给a,即是否实现就地完全取决于这个类型有没有实现__iadd__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
aa = [1, 2, 3]
print(id(aa))
aa += [1, 23]
print(id(aa))

ss = "123"
print(id(ss))
ss += "qwer"
print(id(ss))

1168530238600
1168530238600
1168257348464
1168257157616

对不可变序列进行重复拼接操作:

  1. 创建一个新对象
  2. 将原来对象的元素复制到新对象中
  3. 追加寻元素

str有优化:在为str初始化内存的时候,程序会为他流出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置的这一操作

2.6.1 元组的增量赋值

1
2
3
4
5
6
7
8
t = (1, 2, [1, 2])
t[2] += [3, 4]

Traceback (most recent call last):
File "<input>", line 2, in <module>
TypeError: 'tuple' object does not support item assignment
print(t)
(1, 2, [1, 2, 3, 4])
  1. 元组中的元素被成功拼接
  2. 解释器抛出异常

其实写成t[2].extend([3, 4])就不会抛出异常了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用dis.dis展示操作的python字节码
dis.dis('tuple[i] += iter')

1 0 LOAD_NAME 0 (tuple)
2 LOAD_NAME 1 (i)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_NAME 2 (iter)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE

  • BINARY_SUBSCR: 将值存入stack (DONE)
  • INPLACE_ADD 完成 += (DONE)
  • STORE_SUBSCR 赋值 (ERROR)

可以得出:

  1. 不要把可变对象放在元组里
  2. 增量赋值不是一个原子操作
    • 先计算
    • 后赋值

2.7 排序.sort和sorted()

两者都是排序, 且内部算法使用的是Timsort,它是一种自适应算法,会根据原始数据的顺序特点交替使用插入排序和并归排序,而且Timsort算法是稳定的(相等元素的相对位置保持不变)

  • seq.sort: 就地排序, 在原始数据的基础上排序, id不会改变, 这个方法返回None

    • python operation返回None表示就地修改, 不会创建新对象
    • 如果函数或者方法对对象进行就地修改,那么就应该让他返回None,通知调用者传入的参数发生改变,但并未创建新对象
    • random.shuffle(iterator)|id(iterator)不会修改
    • 返回None表示就地改动又个弊端, 就是调用者无法串联接口,即始终使用的是一个对象引用
  • sort(obj) 会返回一个新建的list

    • 可以接收任何形式的可迭代对象作为参数,包括不可变序列和生成器
    • 最后返回的是一个list
  • 两者都接受两个参数

    1. reverse=False: 是否反向排序
    2. key=identity function:接收一个排序fun通常使用lambda函数
      • 恒等函数(identity funtion)默认用元素自己的值进行排序
      • key=str.lower:忽略大小写排序
      • key=len: 基于长度排序
  • 接收key参数:

    1. min|max
    2. sort|.sort
    3. itertools.groupby()
    4. heapq.nlargest()|heapq.nsmller()
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
35
36
37
38
39
import random
from itertools import groupby
from operator import itemgetter
from heapq import nlargest, nsmallest


random.seed(7)

a = [i for i in range(5)]

# 就地随机打乱seq
random.shuffle(a)

print(a)
a.sort()
print(a)
print(nlargest(3, a))
print(nsmallest(3, a))


temp_data = [
{"name": 1, "age": 1},
{"name": 2, "age": 32},
{"name": 1, "age": 45},
]

# 根据list中每个ele中的name进行排序
temp_data.sort(key=itemgetter("name"))

# 过滤key=name
for name, data in groupby(temp_data, key=itemgetter("name")):
print(f"name---[{name}]-----> [{list(data)}]")

[4, 0, 3, 1, 2]
[0, 1, 2, 3, 4]
[4, 3, 2]
[0, 1, 2]
name---[1]-----> [[{'name': 1, 'age': 1}, {'name': 1, 'age': 45}]]
name---[2]-----> [[{'name': 2, 'age': 32}]]

2.8 bisect管理有序序列

bisect模块包含两个主要的函数bisectinsort, 两者都是通过二分法获取index并进行操作

bisect(haystack, needle), 在haystack中搜索needle其中:

  1. haystack必须是有序的
  2. return index

bisect可以建立一个用数字作为索引的查询表格, 比如把表格和成绩对应起来

1
2
3
4
5
6
7
8
import bisect


def grade(socre, grades=None, breakpoints="FDCBA"):
if grades is None:
grades = [50, 60, 70, 80, 90]
socre_index = bisect.bisect(grades, score)
return breakpoints[socre_index]

bisect.insort(seq, item) 把变量item插入到有序序列seq[本质上是通过二分法获取index后插入]

  • 如果只是处理数字列表的话, array是一个更好的选择

2.9 当list不是首选时

list可以处理多个数据类型,但当面对大量的数字类型时, array是一个更好的选择

  1. 数组存储的不是num对象,而是数字的机器翻译(字节表述)
  2. 和C语言的数组一样, 如果需要频繁的对序列进行进出操作, deque的速度会更快
  3. 在处理包含操作时,set是个更好的选择,python对此进行过优化,需要注意的是,set不是序列,他是无序的

2.9.1 数组

array.array在存储数字方面效率胜于list且支持list的基本操作,此外数组还提供从文件读取(.frombytes)和存入文件(.tofile)的快速方法

python中的数组和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
27
28
29
30
31
from array import array
from random import random

# init a array
floats = array("d", [random() for _ in range(10 ** 7)])
# get ele by index
print(floats[1])

# init the path
file_path = "./floats.bin"

# open the file with "wb"
fp = open(file_path, "wb")

# write the data to the specify file
# by binaryIO
floats.tofile(fp)

# close the file
fp.close()

# init an empty array
floats2 = array("d")

# open the file with "rb
op = open(file_path, "rb")

# open the file 10 ** 7 from file
# by binaryIO
floats2.fromfile(op, 10**7)
print(floats2[1])

method区别:

  1. 数组不支持浅拷贝|但支持__deepcopy__
  2. 数组支持seq.reverse()但不支持seq.__reversed__()
  3. 数组不支持就地排序seq.sort()

从python3.4开始数组不再支持就地排序seq.sort(), 如果需要排序得用sorted函数重新建立一个数组

a = array.array(a.typecode, sorted(a))

2.9.2 内存视图

memoryview是一个内置类, 可以让用户在不复制内容的情况下操作同一数组的不同切片

  • 本质上是泛化和去数学化的Numpy数组
  • 可以在不需要复制内容的前提下,在任何数据结构之间共享内存
  • 处理大数据很实用 –> 节省了数据copy内存和时间
  • 需要注意是对原数据直接进行操作

memoryview.cast能用不同的方式读写同一块内存数据,并且内容字节不会随意移动,会把同一块内存里的内容打包成一个全新的memoryview.cast对象给你

不同方式:可以用不同的读写方式操作数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# init array 类型码“h”
temp_array = array('h', [1, 2, 3, 4, 5, 6])

# 利用数组创一个memoryview实例
view = memoryview(temp_array)

# 创建一个memoryview实例,其中把这块内存内容用“B”类型存储[无符号字符]
cast = view.cast("B")

# .tolist()以列表的形式查看|return --> list
temp = cast.tolist()

# 把位于1的字节赋值为6, 因为把占2个字节的整数高位字节改成了6
# 那么之歌有符号整数的值就变为了 1537
cast[1] = 6

2.9.3 NumPy|SciPy

NumPy和SciPy提供了高阶数组和矩阵操作, python内置数据类型都是一维数据

维度: row

每个维度所含元素: col

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy

a = numpy.arange(12)
print(a.shape)
# 表述1维row 12元素
(12,)

# using unpacking tuple
# 将1维12元素转化成4维3元素
a.shape = 4, 3
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]

# get index_row = 1 index_col = 2
a[1, 2]

# get all index_row index_col = 1
a[:, 1]

# transpose() matrix
# return new view
a.transpose()

2.9.4 双向队列

用列表和append pop(0)可以实现队列 先进先出, 但是删除list中的第一个或者在第一元素前添加一个元素都是十分耗时的, 因为这涉及到所有元素的移动

collection.deque双端队列实现了队列两端元素操作的优化, 但是涉及到中间元素的操作依然会耗时, 并且是一个thread safe

  • 快速从两端对元素进行操作
  • 存储最近使用的几个元素
  • collection.deque可以实现简单的过期机制<通过设置maxlen>
  • append(item)|popleft(index)都是原子操作, 因此deque可以在多线程程序中安全地当作先进先出使用, 而且使用者不需要担心资源锁问题
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
from collections import deque

# create dequeue maxlen=10 eles=range(10)
dq = deque(range(10), maxlen=10)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# append one ele - right
dq.append(10)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# append one ele - left
dq.appendleft(0)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# rotate num ele default=right | negative=left
# return None ==> will change ori data
dq.rotate(3)
# [7, 8, 9, 0, 1, 2, 3, 4, 5, 6]

# extend iterator ele default=right
dq.extend([10, 11, 12])
# [0, 1, 2, 3, 4, 5, 6, 10, 11, 12]

# extendleft iterator ele
# attention the order!
dq.extendleft([-1, -2, -3])
# [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6]

2.10 线程安全

除了collections.deque外还有其他python标准库也有对队列的实现

queue

提供了同步线程安全Queue|LifoQueue|PriorityQueue,不同的线程可以利用这些数据类型来交换信息

  • 线程通信
  • 三个类构造时都有一个可选参数maxsize用来限制队列大小
  • 在满员时这些类并不会丢掉旧的元素腾出位置
  • 而是会锁住资源, 直到另外的线程移除了某个元素
  • 适合控制活跃线程的数量

multiprocessing

这个包实现了自己的Queuequeue.Queue类似, 是设计给进程通信用的还有一个multiprocessiong.JoinableQueue

  • 进程通信
  • 任务管理

asyncio

python3.4及以上提供, 里面有Queue LifoQueue PriorityQueueJoinableQueue这些类受到queuemultiprocessing的影响,用于异步变成任务管理

  • 异步编程

heapq

和上面三个模块不同, heapq没有队列类,而是提供了heappushheappop方法, 可以让用户把可变序列当作堆队列或者优先队列使用

本章小结

可变序列 不可变序列

容器序列: 存储引用 非连续存储

扁平序列: 存储值 连续存储 原子性数据

列表: 列表中数据最好是具有相同特性的数据

拆包: 获取元组内数据最安全可靠的方法,*拆包的利器

具名元组: 将元组与数据关联

  • namedtuple()._asdict将具名元组转化成ordereddoct
  • dict(zip(iter_key, iter_value))普通元组转化dict

序列切片是python中最受欢迎的语言特性之一

对切片赋值是一个修改可变序列的捷径

重复拼接n*seq要在正确的前提下使用!!!!!

  • 初始化含有不可变元素的序列

增量赋值+= 和 *=的操作行为和序列本身有关(可变序列|不可变序列)

PS: string类型有优化不会重复复制原数据

  • 可变序列: 就地修改

  • 不可变序列: 生成新的序列

    sortsorted()key的用法

  • 背后的逻辑是timsort算法, 它是一种自适应算法, 会根据原始数据的特点交替使用插入排序和并归排序


元组的鼻祖是ABC语言中的compounds, tuple更准确的说是frozenlist

key参数

再使用默认函数key进行排序时Python总会比较两个键, 但是这一计算阶段发生在C语言层, 这也是比调用用户自定义比较函数快的原因

  • eleint 或者str时, key支持int或者str
    • int: 比较整个数值
    • str: 只比较第一位数值
1
2
3
4
5
6
temp_1 = [3, 45, 2, "10", "34"]
print(sorted(temp_1, key=int))
print(sorted(temp_1, key=str))

[2, 3, '10', '34', 45]
['10', 2, 3, '34', 45]

Part3.Dict|Set

dict的内置函数_builtins_.dict_, Python对字典实现了高度优化 –> hashtable

3.1 泛映射类型

collections.abc中含有MappingMutableMapping这俩个抽象类,为dict和其他类似的类型定义形式接口

非抽象类一般不会直接继承这些抽象基类, 而是直接对dict或者collections.User.Dict进行扩展,这些抽象基类的主要作用是作为形式化的文档, 定义构建一个映射类型所需要的最基本接口.而且还可以与isinstance一起使用来判断某个数据是否是广义上的mapping类型

1
2
my_dict = {}
isinstance(my_dict, abc.mapping)
  • 使用isinstance而不是type
  • 可以判断除dict外的广义映射类型

标准库的所有映射类型都是通过dict来实现的,他们有个共同的限制, 即key必须是可散列的

可散列数据

1
An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() method). Hashable objects which compare equal must have the same hash value. 
  • 含有__hash__ 散列
  • 含有__eq__ 比较key
  • 哈希值在整个声明周期不会变化
  • 散列对象相等==>散列值一定相等

可散列的数据类型:

  • str
  • bytes
  • 数值类型
  • frozenset
  • tuple(所含的元素全为可散列)

一般用户自定义的类型的对象都是可散列的, 其中散列值就是id(obj), 所以所有这些对象在比较时都是不相等的. 如果一个对象实现了__eq__方法, 并且在方法中用到了这个对象的内部状态的话, 那么只有当所有这些内部状态都是不可变的情况下, 这个对象才是可散列的


构造字典方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>
dict(name="Drink", age="23", score="98")

>>
{"name": "Drink", "age": "23", "score": "98"}

>>
dict(zip(["name", "age", "score"], ["Drink", "23", "98"]))

>>
dict([("name", "Drink"), ("age", "23"), ("score", "98")])

>>
dict({"name": "Drink", "age": "23", "score": "98"})

>>
{key, value for key, value in iterator(key-value-pair)}

3.2 字典推导

推导式的作用:

创建新的数据对象

过滤作用

3.3 常见的映射方法

映射类型的方法很丰富, dict defalutdictOrderedDict的常见方法, 后面两个数据类型是dict的变种,位于collections模块内

default_factory不是一个方法而是一个可调用对象, 他的值在defaultdict初始化中由用户设置

OrderedDict.popitem()会移除最先插入的元素**(先进先出), 如果.popitem(last=True)则会移除最后插入的元素(后进先出)**

d.update(m, [**kargs]), 在处理m时, 函数首先检验m是否有keys方法,

  • 有–>update会把它当作映射对象处理
  • 无–>update会把它当作含有(key-vaule)键值对元素的迭代器
  • python大多数映射类型的构建都采用了相似的逻辑

可调用对象

将类当作函数调用instance()

  • 类中实现__call__
  • 快速调用带有某一实例化属性的类的函数
1
2
3
4
5
6
7
8
9
class eval_some:

def __init__(slef, g):
self.g = g
def __call__(self, t):
return (self.g * 2) / t

earth = eval_some(9.8)
print(earth(10))

3.3.1 更新字典kV

用setdefault处理找不到的键

d[k] --> getitem找不到键时, 会有异常抛出KeyError

  • 符合python的快速失败哲学
  • 可以使用d.get(k, default)代替异常
  • 当在更新某个键对应的值的时候, 以上俩个方式效率都很低

如果要更新字典的一个键值对(包括不存在)使用setdefault(key, default) [operate]这样只会query 1,如果使用if else []则会查询2~3

1
2
3
4
5
6
7
8
9
10
11
a = {"name": "Drink"}

# 这里只会查询一次key并更新或初始化
# 如果key存在则对key-value更新
# 不存在将key-default放进映射中
a.setdefault("things", []).append("reading")

# 当存在key时查询2次, 不存在会查询3次
if "things" not in a:
a["things"] = []
a["things"].append("reading")

3.4 映射的弹性键查询

在映射中查找不存的key时, 我们希望可以返回一个default value而不是error, 以下俩途径可以实现弹性键查询

  1. 通过defaultdict这个类而不是普通的dict
  2. 自己定义一个dict子类, 并在在子类中实现__missing__方法

3.4.1 defaultdict处理不存在的key

defaultdictcollections中的一个类, 在实例化它时,可以接受一个defaultfoctory不接受参数(一定是可调用的且不接受参数的方法类|函数)

  1. 在调用d[key]中如果__getitem__找不到对应的key会调用__missing__查看是否定义了defaultfactory

  2. defaultfactory返回的值赋值给d[key]

  3. 更新dict

1
2
3
4
5
6
7
8
from collections import defaultdict

temp = defaultdict(list)

temp["name"]
print(temp)
>>
defaultdict(<class 'list'>, {"name": []})
  • 如果defaultfactory=None在没有找到对应的key时会抛出KeyError

  • defaultfactory只会在__getitem__中调用, 比如dict[key]才会被调用, 其中连接两者桥梁的时__missing__

3.4.2 特殊方法missing

所有的映射类型在找不到key的时候都会牵扯到missing, python基类虽然直到这个方法,但并没有定义这个方法, 不过, 如果一个class继承了dict并且这个类提供了missing方法,那么在getitem找不到值的时候不会抛出异常

特点:

  1. missing方法只会被getitem调用
  2. 不会对.get() __contains__方法产生影响

dict[key] -- __getitem__ -- __missing__

dict.get() -- get -- try except


如果要自定义一个映射类型, 更合适的策略是继承collections.UserDict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# define the new mapping class by user based on dict
class StrKeyDict(dict):

def __missing__(self, key):
# 如果键不存在且本身是str则抛出异常
if isinstance(key, str):
raise KeyError(key)
# 如果键不存在且本身不是str则调用getitem
return self[str(key)]

# get方法吧查找工作用self[key]的形式委托给getitem,这样
# 在宣布查找失败之前还missing再给某个键一个机会
def get(self, key, default=None):
try:
return self[key]
except KeyError:
# 如果cast异常则missing也失败了,返回default
return default

def __contains__(self, key):
# 按照传入键的原本值来查找,如果没找到则转化成str再次查找
return key in self.keys() or str(key) in self.keys()

k in dict.keys()操作在python3中是十分快速的, 即使映射类型对象很庞大, 因为:

  • dict.keys()返回的值是一个视图, 就像集合, 在里面查找一个元素是十分快的
  • key in dict扫描的是一个list, 处理大数据效率低dict.keys()

3.5 字典的变种

都是基于collections

除了bultin中定义的dict外, 其余字典的变种都是collections库中的

UserDict

比较特殊的一种dict, 这个类其实是把builtin中的dict用纯Pyhton又实现了一遍

一般用于用户自定义子类的继承

OrderedDict

这个类型会在添加键的时候保持顺序, 因此键的每次迭代次序总是一致的

OrderedDictpopitem方法默认删除并返回的时字典里的最后一个元素(先进后出), 如果popitem(last=False)则会删除并返回第一个元素(先进先出)

ChainMap

什么是chainmap?为什么要搞chainmap

该类型可以容纳数个不同的映射对象,然后在进行键查找操作时, 这些对象会被当作一个整体逐个进行查找,直到键被找到

在给有嵌套功能作用域的语言做解释器的时候很有用, 可以用一个映射对象来代表一个作用域的上下文

ChainMap可以将多个字典合并为一个独有的字典,这样的操作 并不是对源数据的拷贝,而是指向源数据,假如原字典数据修改,ChainMap映射也会改变;如果对ChainMap的结果修改,那么原数据一样也会被修改

使用update进行合并字典,是对源数据的拷贝

  1. ChainMap 可接受多个映射然后在逻辑上使它们表现为一个单独的映射结构;它只是维护了一个记录底层映射关系的列表,然后去重定义常用的字典操作

  2. 如果有重复的键,会采用第一个映射中键对应的值

  3. 修改 ChainMap 映射结构,会同时作用在自己和原始字典结构上

  4. 可以使用字典的 update() 方法,来替代上面的合并方案;但是这就需要创建一个新的字典对象(或者修改原字典,破坏了原始数据),并且原始字典做了修改,并不会反映到新建的字典上

  5. ChainMap 使用的就是原始字典,因此原字典变,它也会改变。

  6. 如果在使用 ChainMap 合并多个字典时,字典中有重复的 key 值,默认取第一个字典中 key 对应的 value ,从原理上面讲,ChainMap 实际上是把放入的字典存储在一个队列中,当进行字典的增加删除等操作只会在第一个字典上进行,当进行查找的时候会依次查找(以查找的第一个key-value返回)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
m1 = {"name": "Drink", "age": 23}
m2 = {"name": "Drinkle", "score": 93}
m3 = {"name": "Spark", "age": 23, "score": 98, "sex": "man"}

cm = collections.ChainMap(m1, m2, m3)
print(cm)
print(cm["name"])

for key, value in cm.items():
print(key, " <--> ", value)

ChainMap({'name': 'Drink', 'age': 23}, {'name': 'Drinkle', 'score': 93}, {'name': 'Spark', 'age': 23, 'score': 98, 'sex': 'man'})
Drink
name <--> Drink
age <--> 23
score <--> 93
sex <--> man

Counter

这个类型会给键准备一个整数计数器, 每次更新一个键的时候都会增加这个计数器.因此这个类型可以用来给可散列对象计数(str bytes frozenset 数值型)或者当成多重集来用–>多重集合就是集合里的元素可以出现不止一次, Counter实现了+ - 运算符来和并记录

还有像most_common(num)这类方法, 会按照次序返回映射里最常见的num个键和他们的计数

1
2
3
4
5
6
7
8
9
10
11
counter = collections.Counter("asdxzcsdfwerewrfewzsqawd")

print(counter)

counter.update("asdsassd")

print(counter.most_common())

Counter({'w': 4, 's': 3, 'd': 3, 'e': 3, 'a': 2, 'z': 2, 'f': 2, 'r': 2, 'x': 1, 'c': 1, 'q': 1})

[('s', 7), ('d', 5)]

3.6 子类化UserDict

自定义mapping类型,以UserDict为基类更加方便,因为UserDict是原生的方法, 而dict可能会走写捷径,导致我们不得不在子类中重写这些方法, 而UserDict则不会出现这个问题

需要注意的是, UserDict并不是Dict的子类,但在data属性是基于Dict的实例实现的, 这个属性实际上是UserDict最终存储数据的地方

  • UserDict的子类在实现__setitem__避免了不必要的递归
  • 可以让__contains__的代码更加简洁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import collections

class StrKeyDict(collections.UserDict):

def __missing__(self, key):
if isinstance(key, str):
raise KeyError (key)
return self[str(key)]

def __contains__(self, key):
return str(key) in self.data

def __setitem__(self, key, value):
self.data[str(key)] = value

UserDict继承的是MutableMapping,所以StrKeyDict里剩下的映射类型方法都是从UserDict MutableMappingMapping这些超类中继承的

Mapping类虽然是一个抽象基类ABC,但它提供了许多实用方法

1.MutableMapping.update

  • 可以直接使用dict.update(obj)
  • 定义在__init__中, 实列化时可以接受多个不同类型的参数构造dict
  • 本质上调用的是__setitem__ –> self[key]=value

2.Mapping.get

  • .get() –> def get() –> try except

3.7 不可变映射类型

接受一个mapping类型, 返还一个动态只读视图, 在原数据上更改可以反映到视图上,但不能对视图进行写操作, 只能读取 types.MappingProxyType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from types import MappingProxyType

temp = {"name": "Drink"}

proxy_dict = MappingProxyType(temp)

>>
proxy_dict["name"]
proxy_dict["age"]
proxy_dict["score"] = 90
temp["age"] = 23
proxy_dict
>>
<Dirnk>
<KeyError: 'age'>
<TypeError: 'mappingproxy' object does not support item assignment>
<SUCCESS>
<{'name': 'Drink', 'age': 23}>

3.8 集合论

集合从2.3以module出现, 到2.6才成为built-in type, 包括set frozenset

集合的本质是许多唯一对象的聚集(去重)

拥有极快的查找速度 ==> 散列表

空集 –> set()

{} –> dict 本质上是一个空字典

集合内元素必须是can hash即可散列的

  • str
  • bytes
  • 数值类型
  • frozenset

集合的操作

| –>

& –> –> set.intersection(set)

- –> 差集

3.8.1 集合字面量「仅set」

除了空集合set()外, 集合的字面量如同{1} {1, 2}

字面量语句构造集合的方法比set([1, 2, "3"])更快, 后者python需要新建一个列表, 最后吧这个列表传入到构造方法中, 如果是{1, 2, "3"}的字面量构造, python会利用一个BUILD_SET的字节码创建集合

使用反汇编函数查看字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from dis import dis

dis("{1}")
dis("set([1])")

0 LOAD_CONST 0 (1)
2 BUILD_SET 1
4 RETURN_VALUE

0 LOAD_NAME 0 (set)
2 LOAD_CONST 0 (1)
4 BUILD_LIST 1 --
6 CALL_FUNCTION 1 --
8 RETURN_VALUE

python对于frozenset没有特殊字面量句法, 只能采用构造的方式

3.8.2 集合推导

同列表推导字典推导一样

  1. 创建一个新的序列
  2. 起到过滤条件的作用

3.8.3 集合的操作

1
2
3
4
5
6
7
8
9
10
a = {1, 2, 3}

# KeyError
a.remove(4)

# pop the first one ele
a.pop()

# if number not in set do nothing
a.discard(4)

下表中, 缀运算符需要两侧的对象都是集合类型, 但是其他的方法则只要求所传入的参数是可迭代对象

集合的比较运算符, 返回布尔值

集合的其他操作方法

3.9 dict和set的原理

principles of dict and set, 了解python里的字典和集合类型,他们背后所涉及的hash table是必不可少的

这节将会回答以下几个问题

  1. python中的dictset效率有多高?
  2. 为什么他是无序且不稳定的?
  3. 为什么并不是所有的python对象都可以当作dictkeyset里的number?
  4. 为什么dictkeysetnumber的顺序是根据它们被添加的次序而定的,以及为什么在映射对象的生命周期中,这个顺序并不是一成不变的?
  5. 为什么不应该迭代循环dictset的同时往里面添加元素?
1
2
3
4
5
6
7
8
temp_list = [1, 2, 3, 4]
a = {"name": "Drink"}.fromkeys(temp_list, "")
print(a)

dict.fromkeys(iterator, values)
# Create a new dictionary with keys from iterable
# and values set to value.
{1: '', 2: '', 3: '', 4: ''}

3.9.1 效率实验

使用in set(&) 操作符,分别在set dict list查询是否含有某个元素

其中结果如下:

最快的是&

最慢的是list in

由于列表背后没有散列表来支持in运算符, 每次搜索都会扫描一次完成的列表 [说明了, dictset在使用散列表存储的时候使用了类似排序的功能]

3.9.2 dict中的散列表

这一节只是笼统的描述python如何使用hashtable来实现dict, 其中Cpython对dict有过优化

散列表

  • 本质上是一个稀疏数组(总是有空白元素的数组称为稀疏数组)

  • 散列表中的单元叫做表元(bucket)

  • 在dict的散列表中, 每个键值对占用一个表元,一个表元有俩部分, 分别是对值的引用和对键的引用

  • 表元大小一致, 可以通过偏移量读取特定的表元

  • python会保证散列表中大约三分之一的表元是空的,如果达到阈值, 原有的散列表会被复制到一个更大的空间中

  • 把对象放入散列表, 首先使用hash()计算这个元素键的散列值

有关散列表的概念:

1.散列值和相等性

hash() ==

  • hash()可以用于任何内置类型对象, 自定义对象调用自定义实现的__hash__
  • 如果俩对象在比较时是相等的, 则它们的散列值一定相等(但对象的类型和存储结构不一定相等)
  • 为了让散列值可以当作散列表的索引, 越是相似但不相等的对象,他们散列值的差别就越大

从python3.3开始 str bytes datetime对象的散列值计算过程中多了随机加盐 所加盐值是python进程内的一个常量每次启动python解释器都会生成一个不同的盐值,目的是为了防止DOS攻击而采取的安全措施

2.散列表算法

dict[key]为例:

  • python首先会调用hash(key)计算key的散列值
  • 把计算的散列值最低的几位数字当作偏移量, 在散列表中查找表元bucket
    • 具体取几位要看散列表的大小
    • 若对应的表元为空 –> KeyError
    • 若非空 –> 表元里会有key-value-pairs
  • python会校验key-value-pairs中的key与输入的key是否相等==
    • 相等 –> 返回对应的value
    • 不等 –> 散列冲突
  • 散列冲突: 因为散列表所做的是把随机的元素映射到只有几位的数字上,而散列表本身的索引又依赖于这个数字的一部分
    • 为了解决冲突,算法会在散列值中另外再取几位数字用特殊方法处理后把新的数字当作索引寻找表元,并重复之前的操作

添加新元素:

发现表元为空时会放入一个新元素

更新元素:

找到表元后会更新元素

需要注意的是,python会按照散列表的拥挤程度来决定是否要重新分配内存为它扩容,如果增加散列表的大小,那散列值所占的位数和用作索引的位数会增加,减少了散列冲突


**注意:**Cpython中规定如果有一个整型对象,而且他能存进入一个机器字中,那么他的散列值就是它本身

3.9.3 dict的实现和限制

主要讨论hashtable给dict带来的优势和限制

1.键必须是可散列的

包括: str bytes frozenset 数值类型

一个可散列的对象必须包括

  • 支持hash()函数并且通过__hash__所得到的散列值不变(在他的生命周期中)
  • 支持通过__eq__方法检测相等性
  • a==b那么hash(a)==hash(b)
  • 所有由用户自定义的对象默认都是可散列的,因为他们的散列值都是id()来获取的,而且他们不相等

**注意:**如果自定义的类实现了_eq_,并且希望他是可散列的, 那么必须定义一个恰当的_hash_保证a==bhash(a)==hash(b)另一方面,如果一个含有自定义_eq_依赖的类处于可变状态, 那么就不要在这个类中实现_hash_,因为他的实例是不可散列的


2.字典在内存中开销大

  • 字典使用散列表实现
  • 散列表是稀疏的
  • 在自定义类中_slots_属性可以改变实例属性的存储方式, 由dict-->tuple
  • optimization优化往往和可维护相对立maintain

3.键查询很快

  • 空间换时间
  • hash table实现了无视数据量大小的快速访问(在数据能放入内存的前提下)

4.键的次序取决于添加次序

  • 在添加新键时, 可能会由哈希冲突, 此时新键会被安排到另一个表元(bucket)中
  • 但是和俩个值相同的键添加顺序有关
  • 虽然逻辑和面向用户表现形式上如此,在判断俩上依旧是相等的
1
2
3
4
5
6
a = {"name": "Drink", "age": 23}
b = {"age": 23, "name": "Drink"}
if a == b:
print("you are right")
>>
right

5.添加新键会改变已有键的顺序

尽量不要边遍历边添加新键

  • 添加新键
  • python解释器为字典扩容
  • 新键散列表并把已有元素添加到新表中
  • 这个过程可能会发生散列冲突,导致新散列表中键的次序变化
  • 如果在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环可能会跳过一些键甚至是字典中已有的键

因此:不要对字典同时进行迭代和修改,如果扫描并修改一个字典,最好分成俩步 :

​ [迭代–得出要添加的内容–并把内容放到新字典]

​ [对原有字典进行更新]

不过在python3中对此进行了优化,.keys() .items() .values()方法返回的都是字典视图, 换句话说这些方法返回的更像set而不是list,此外视图还可以动态的反馈字典的变化

3.9.4 set的实现和限制

set 和 frozenset的实现也依赖于散列表

  • 在散列表中只存放元素的引用(类似只存放key的引用)
  • 集合中的元素必须是可散列的
  • 集合很消耗内存
  • 可以很高效的判断元素是否在集合中
  • 元素的次序取决于被添加到集合中的顺序
  • 在集合里添加元素会改变集合里已有元素的次序

本章小结

dictpython的基石, 除了dictbuiltin里还提供了特殊映射比如collections里的defaultdict OrderDict ChainMap Counter UserDict以及types里的MappingProxyTypes

映射类型的重要方法

1.setdefault

  • 更新字典里存档的可变值(比如列表)
  • 避免重复搜索键

2.update

  • 让批量更新字典成为可能

3.missing

  • 可以自定当找不到对应的key时, 对象如何处理

collections.abc模块提供了MappingMutableMapping俩大抽象基类,利用它可以进行类型查询或者引用,此外还有SetMutableSet俩大抽象基类

Part4.TextAndBytes

human use text and computer use bytes.

本章节主要讨论:

  • 字符 码位 字节表述
  • bytes bytearraymemoryview等二进制序列的独特性
  • 全部unicode和陈旧字符集的编解码器
  • 避免和处理编码错误
  • 处理文本文件的最佳实践
  • 默认编码的陷阱和标准I/O问题
  • 规范化 Unicode文本 进行安全比较
  • 规范化 大小写折叠和暴力移除音调符号的实用函数
  • 使用locale模块和PyUCA库正确地排序Unicode文本
  • Unicode数据库中的字符元数据
  • 能处理字符串和字节序列的双模式API

4.1 字符问题

字符串:一个字符序列, 本质上是一个序列; 问题是什么是字符?

2015年字符的定义为Unicode字符(面向人)

Unicode标准把字符的标识-和具体的字节表述进行了区分:

  • 字符的标识即码位:是01114111的数字(10进制)在unicode标准中以46个十六进制数字标识,并且加前缀U+
  • 字符的具体表述取决于所用的编码:编码是在码位和字节序列之间转换时使用的算法

把码位转化成字节序列的过程时编码,反之是解码

简单的理解:

码位是数字映射后的准备编码的字符标识;

字节序列是通过一定编码规则(算法)编码后的用于计算机存储和传播的字节序列

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
# init string
a = 'advdv我'

# advdv我字符串有四个Unicode字符
print(len(a))

# 使用UTF-8把str对象编码成bytes对象
b = a.encode('utf-8')

# check b and len of b
# \xe6 --> one 16-base
# 我 -encode-> three bytes
# -> \xe6\x88\x91
print(b, len(b))

# decode b with utf-8
# 使用UTF-8把bytes对象解码成str对象
print(b.decode('utf-8'))

>>
6
# bytes对象字面量以b开头
# 字节序列b有8个字节 我被编码成了三个字节
b'advdv\xe6\x88\x91' 8
advdv我

python3str类型基本相当于python2unicode类型, 但是前者的bytes类型却不是把str类型换个名字那么简单

4.2 字节概要

python内置了两个基本的二进制序列类型其中有py3引入的不可变bytes类型和py2.6添加的可变bytesarray类型

bytesbytearray对象的各个元素是介于0~255(含)之间的整数而不是py2str对象那样的单个字符.

需要注意的是二进制序列的切片始终是同一类型的二进制序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# bytes对象可以从str对象加给定的编码方法构建
cafe = bytes("cafの", encoding="utf_8")
print(cafe)

# bytes对象中的各个元素是range(256)的整数
print(cafe[0])
# bytes对象的切片依然是bytes
print(cafe[:1])

# bytesarray对象没有字面量语句而是以bytearray()
# 和字节序列字面量参数的形式显示
cafe_arr = bytearray(cafe)
print(cafe_arr)
# bytearray对象的切片还是bytearray对象
print(cafe_arr[-1:])

>>
b'caf\xe3\x81\xae'
99
b'c'
bytearray(b'caf\xe3\x81\xae')
bytearray(b'\xae')

二进制序列本质上整数序列,但是他们的字面量表示含有ASCII文本,因此各个字节的值会有以下三种不同的方式显示 –> 例如b'xxxxx\xx\xx\t'

  • 可打印的ASCII范围内的字节(从空格到~)使用ASCII字符本身
  • 制表符 换行符 回车符和\ 使用转义序列 \t \n \r \\
  • 其他字节的值,使用十六进制转义序列(\x00空字节)

1
2
3
4
5
6
7
a = bytes.fromhex('31 4b ce a9')
print(a)
b'1K\xce\xa9'
31(16进制) --> 49(10进制) --> ASCII(1K)
"""
在ASCII中K代表10进制
"""

4.2.1 str方法

1.casefold

lower() 只对 ASCII 也就是 'A-Z'有效,但是其它一些语言里面存在小写的情况就没办法了。文档里面举得例子是德语中'ß'的小写是'ss'

1
2
3
s = 'ß'
s.lower() # 'ß'
s.casefold() # 'ss'

汉语 & 英语环境下面,继续用 lower()没问题;要处理其它语言且存在大小写情况的时候再用casefold()

2.isdecimal

判断string是否是十进制, 返回bool空字符为False

3.isidentifier

Python 对各种变量、方法、函数等命名时使用的字符序列称为标识符

检查string是否是python标识符

标准为:

  • 由 26 个英文字母大小写,0-9 ,_ 组成,不能以数字开头,且严格区分大小写

  • 不能包含空格、@、% 以及 $ 等特殊字符,不能以系统保留关键字作为标识符一共有25 个

  • 以下划线开头的标识符有特殊含义

对应keyword.iskeyword(str)检查是否是python保留的关键字

4.isnumeric

isnumeric() 方法检测字符串是否只由数字组成,数字可以是Unicode数字,全角数字(双字节) 罗马数字 汉字数字。指数类似 ² 与分数类似 ½ 也属于数字。返回bool

5.isprintable

判断是否是可打印字符,不可打印的有:

  • 换行符制表符等\t \n

6.translate

s.transelate(table)

tablestr.maketrans(in-str, out-str)生成用于创建字符映射的转换表

转换表本质上整数映射

1
2
3
print(transtable)

{115: 109, 111: 97, 109: 110, 101: 121}

table作为基础替换s中所有命中的字符

1
2
3
4
5
6
7
8
a = "i like something relaxing!"
in_str = "some"
out_str = "many"
transtable = str.maketrans(in_str, out_str)

print(a.translate(transtable))
>>
i liky manything rylaxing!

str类型的其他方法都支持bytesbytearray类型

二进制序列并不是指字节序列全是二进制编码,而是指用于计算机存储和传播的二进制字节序列即字节序列,其本质上还是整数序列

  • ASCIIK表示十进制
  • 1k表示十进制1

构建bytesbytearray实例还可以调用各自的构造方法,传参如下:

  • 一个str对象(码位)和encoding关键字参数(编码规则)
  • 一个可迭代对象, 提供0~255数值
  • 一个实现缓冲协议的对象(bytes bytearray memoryview array.array) 此时把源对象中的字节序列复制到新建的二进制序列中

注意使用缓冲类对象构建二进制序列是一个底层操作,可能涉及类型转换

下面是将array.array转化成字节序列表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import array
# 创建一个短整数16位数组
numbers = array.array('h', [-2, -1, 0, 1, 2])
# 保存组成的numbers的字节序列副本
to_bytes = bytes(numbers)
# 表示5个短整数的10个字节
print(to_bytes)
>>
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'

# 创建一个空array接收bytes
# 并输出结构化的bytes
temp_array = array.array('h')
print(temp_array.frombytes(to_bytes))
print(temp_array)
>>
None
array('h', [-2, -1, 0, 1, 2])

需要注意

  • 使用缓冲类对象(bytes bytearray array.array memoryview)创建bytes bytearray对象时, 始终复制的是源对象中的字节序列

  • 不过memoryview对象允许在二进制数据结构之间共享内存

  • 从二进制序列提取结构化信息需要struct模块

4.2.2 结构体和内存视图

struct模块提供了一些函数, 把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转化成打包的字节序列.可以处理bytes bytearray memoryview对象

注意:

memoryview类不是用于创建或存储字节序列的, 而是共享内存,让你访问其他二进制序列 打包的数组和缓冲中的数据切片,而无需复制字节序列

memoryview 对象的切片是一个新 memoryview 对象,而且不会复制字节序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import struct
# 结构体格式: < 小字节序列
# 3s3s 俩个3字节序列
# HH 俩个16位二进制整数
fmt = "<3s3sHH"
whith open("filter.gif", "rb") as fp:
# 使用内存中的文件内容创建一个memoryview对象
img = memoryview(fp.read())
# 使用他的切片再创建一个mv对象(这里不会复制字节序列)
header = img[:10]

# 转化成字节序列(10字节)
bytes(header)

# 拆包mv对象获得一个元组包括类型 版本 宽度 高度
struct.unpack(fmt, header)

# 删除引用 释放mv实列占用的内存
del header
del img

如果经常处理二进制数据推荐阅读:

memoryview

struct interpret bytes as packed binary data

4.3 基本的编解码器

python自带了超过100种编解码器(编码算法)用于在文本和字节之间相互转换

其中后三者是可变长度的多字节编码, 其余都是单字节编码

图中星号表示某些编码(ASCII和多字节编码GB2312)不能表示所有的Unicode字符, UTF编码的设计目的是处理每一个Unicode码位

4.4 编解码问题

编解码涉及到unicode或者string或称码位byte的编解码转换

编解码异常有三种:

1.UnicodeEncodeError

strbyte异常

2.UnicodeDecodeError

bytestr异常

3.SynatxError

如果在编解码中,源码的编码与预期不符, 在加载模块时会抛出语法异常

4.4.1 UnicodeEncodeError

4.4.2 UnicodeDecodeError

4.4.3 SyntaxError

多数非UTF编解码器只能处理Unicode字符的一小部分子集, 把文本转化成字节序列时, 如果目标编码中没有定义某个字符就会抛该异常, 处理方式是把errors参数传给编码方法或函数,对错误进行特殊处理

不是每个字节都包含有效的ASCII字符,也不是每个字符序列都是有效UTF-8UTF-16 因此,在把二进制序列转化成文本时, 如果假设是这俩个编码中的一个, 遇到无法转换的字节序列时会抛出UnicodeDecodeError

str.en|decode(encoding=””, errors=””)

errors接收四个参数

  • strict: 遇到无法编码的字符raise error

  • ignore: 忽略不能编码的字符

  • replace: 使用?代替不能编码的字符

  • xmlcharrefreplace: 把无法编码的字符替换成XML实体

需要注意的是errors接受的参数是可以扩展的, 你可以位errors参数注册额外的字符串方法codecs.register_error函数

  • python3允许在源码中使用非ASCII标识符
1
2
β = 1
print(β)

4.4.4 找出字节序列编码

简单的说一般找不出字节序列的编码, 有些通讯协议和文件格式, 比如 HTTP和XML包含明确指明内容编码的首部. 可以肯定的是, 某些字节流不是ASCII, 因为其中包含大于127的字节值, 而且制定UTF-8和UTF-16的方式也限制了可用的字节序列.即使如此我们也无法根据特定的位模式来确定二进制文件的编码时ASCII或UTF-8

统一字符编码侦测包Chartdetect,可以识别30中编码

二进制序列编码通常不会指明自己的编码, 但是UTF格式可以在文本内容的开头添加一个字节序列标记

1
2
3
4
5
import chardet
a = chardet.detect(b"sddssfsd")
print(a)

{'encoding': 'ascii', 'confidence': 1.0, 'language': ''}

4.4.5 BOM有用的鬼符

BOM (bytes-Order-Mark)字节序列标记, 指明编码时使用的是IntelGPU的小子节序

  • utf-16有俩种编码格式UTF-16LE小子节序UTF-6BE大字节序
  • 小子节序各个码位的最低有效字节在前面, 大字节序相反(Unicode码位)
  • 因此在utf-16编码中使用开头的BOMb'\xff\xfe'十进制(255 254)表示小子节编码
  • utf-8无论设备使用哪种字节序,生成的字节序列始终一致, 因此不需要BOM

4.5 处理文本文件

处理文本文件最好的方式是三明治处理法, 首先将输入的字节序列转码成字符串, 在对字符串进行处理, 最后把处理好的字符串编码成字节序列进行输出

Python3中的Open函数会在读取文本进行必要的解码, 在以文本模式写入文件时也会进行必要的编码, 所以my_file.read()方法得到的以及传递给my_file.write(text)方法的都是字符串对象

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
35
36
# 使用utf-8编码打开(创建)txt文件, 模式位写
# 此时使用open函数创建一个TextIowrapper对象
fp = open("text.txt", "w", encoding="utf-8")
print(fp)
<_io.TextIOWrapper name='text.txt' mode='w' encoding='utf-8'>

# 将字符串对象写入TextIo对象中
fp.write("Drink")

# 关闭TextIo对象
fp.close()

import os
# 调用系统stat,查看路径文件的字节大小
print(os.stat("text.txt").st_size)
5

# 打开文本文件, 返回一个TextIoWrapper对象
# 编码是区域设置中的默认值
fp2 = open("text.txt")
# TextIO对象有encoding属性, 查看所使用的编码
print(fp2.encoding)
cp936
fp2.read()

# 以特定的编码方式打开文本文件流
fp3 = open("text.txt", encoding="utf-8")
fp3.read()

# 使用open函数创建一个BytesIoWrapper对象
fp4 = open("text.txt", "rb")
# 在二进制模式即字节流中返回的是BufferedReader对象
# 其满足buffer-protocol协议
print(fp4)
<_io.BufferedReader name='text.txt'>
fp4.read()

除非想要判断编码否则不要使用二进制模式打开文本文件, 即便想要查看编码类型也不要重复发明轮子, 可以使用chardet

通常不建议使用默认编码方式取处理二进制或文本流

编码默认值

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
import locale
import sys


expressions = """
locale.getpreferredencoding()
type(my_file)
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""


my_file = open('dummy', 'w')


for expression in expressions.split():
value = eval(expression)
print(expression.rjust(30), "-->", repr(value))

sys.stdin.isatty()
sys.stderr.isatty()
1
2
3
4
5
6
7
8
9
10
locale.getpreferredencoding() --> 'cp936'
type(my_file) --> <class '_io.TextIOWrapper'>
sys.stdout.isatty() --> False
sys.stdout.encoding --> 'UTF-8'
sys.stdin.isatty() --> False
sys.stdin.encoding --> 'UTF-8'
sys.stderr.isatty() --> False
sys.stderr.encoding --> 'UTF-8'
sys.getdefaultencoding() --> 'utf-8'
sys.getfilesystemencoding() --> 'utf-8'

sys.getfilesystemencoding()用于编解码文件名而非文件内容, 把字符串参数作为文件名传给open()函数会使用它, 如果是字节序列则直接传给OS API

locale.getprefferedencoding()返回的编码即是打开文件的默认编码,同时也是重定向文件的sys.stdout/stdin/stderr

不要依赖默认编码尤其是windows用户

4.6 规范化Unicode字符

因为Unicode有组合字符(变音字符和附加到前一个字符上的记号, 打印时作为一个整体)所以字符串比较起来比较复杂

比如这个词, 是e后加U+0301得来的, 在unicode中称为标准等价物, 但是python看到的是不同的码位序列, 因此判断两者不相等, 这时就需要一个统一的标准, 来规范化Unicode就像规范两种方言一样

使用unicodedata.normalize函数

第一个参数:

  • NFC (Normalization Form C)

使用最少的码位构成等价的字符串

  • NFD

把组合字符分解成 [基字符] 和 [单独的组合字符]

1
2
3
4
5
6
7
8
9
10
11
12
13
s1 = "café"
s2 = "cafe\u3001"

print(unicodedata.normalize("NFC", s1))
print(unicodedata.normalize("NFC", s2))

print(unicodedata.normalize("NFD", s1))
print(unicodedata.normalize("NFD", s2))

café
cafe、
café
cafe、

用户输入的文本默认是NFC格式, 在保存文本前, 最好使用normalization(“NFC”, text)清洗字符串

需要注意的是, 在使用NFC清洗字符串时, 有些单字符会被规范成另一个单字符, 比如电阻的单位Ω会被规范成欧米茄, 在视觉上是一样的, 但在比较时并不相等

  • NFKC

  • NFKD

这俩个规范化方法是兼容性规范化, 这些规范化更适合用在用户搜索或索引上, 需要注意的是, 经过NFKC或者NFKD兼容性规范化后的字符可能会丧失原本的意义, 尽量不要用于数据的持久化

1
2
3
s3 = "¼"
print(unicodedata.normalize("NFKC", s3))
>> 1/4

4.6.1 大小写折叠

即将所有的字符转化成小写, 相比于lower()只支持Unicode, casefold()支持更多的大小写转化, 尽管如此, 两者不同的字符仅有116个码位, 占比0.11%

4.6.2 规范化文本匹配实用函数

NFC和NFD可以很合理的比较Unicode字符串, 并且NFC是最好的规范化形式, 不区分大小写应该使用casefold()

1.去掉变音字符号

unicodedata.combining (unichr) 将分配给Unicode字符 unichr 的规范组合类 返回 为整数。 如果未定义组合类,则返回 0 。

1
2
3
4
5
6
7
8
9
10
11
12
13
from unicodedata import normalize


def shave_marks(txt):
"""
去掉所有的变音符号
"""
# 把所有的字符拆成基字符和组合记号
norm_txt = unicodedata.normalize("NFD", txt)
# 过滤掉所有组合记号
shaved = "".join(c for c in norm_txt if not unicodedata.combining(c))
# 重组所有字符
return unicodedata.normalize("NFC", shaved)

2.比较字符是否相等|忽略大小写

1
2
3
4
5
6
7
8
9
10
from unicodedata import normalize


def nfc_equal(str1, str2):
return normalize("NFC", str1) == normalize("NFC", str2)


def fold_equal(str1, str2):
return (normalize("NFC", str1).casefold() ==
normalize("NFC", str2).casefold())

4.7 Unicode文本排序

对于ASCII字符串来说比较的是码位, 但在非ASCII字符时会使用locale.strxfrm函数, 这个函数会把string转化成适合的区域进行比较

需要注意的是, 一般情况下, 变音符不会对排序造成影响, 如果有影响, 变音符号常常排在常规词后面

在使用locale.strxfrm函数作为排序key前, 要先调用setlocale(LC_COLLATE, your_locale)

需要注意:

  1. 区域设置是全局的, 不建议库中调用, 且应用或框架应该在进程中调用
  2. 操作系统必须支持区域设置

使用unicode排序算法排序

pyuca

Python Unicode Collator Algorithm

1
2
3
4
5
6
import pyuca

coll = pyuca.Collator()
fruits = ["apple", "alle", "banana", "bba"]
sorted_fruits = sorted(fruits, key=coll.sort_key)
print(sorted_fruits)

4.8 Unicode 数据库

本质上已经一个关于unicode以及个中映射关系的数据库, 就如码位与string bytes的映射一样, 同样基于此提供了许多双模式API对外调用, 出名的有unicodedata

regex库正在替代re库

双模式API是一个趋势: 即支持字符和字节参数,并根据参数的格式做出自适应的调整

4.9 支持str和bytes的双模式API

标准库中一些函数能接受字符串或字节序列作为参数, 然后根据类型自适应做不同的行为, 其中re和os为例

4.9.1 正则str和bytes

unicode bytes 次数|注
* {0,}
+ {1,}
? {0,1}
\d 匹配任何一个十进制数字, 包括[0-9]和其他数字字符 只匹配[0-9] 和re.ASCII
\w 任何Unicode字符, 所有语言|数字|下横线 [a-zA-A0-9] 和re.ASCII一样
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
import re


re_numbers_str = re.compile(r"\d+")
re_words_str = re.compile(r"\w+")

re_numbers_bytes = re.compile(rb"\d+")
re_words_bytes = re.compile(rb"\w+")

# 这里会默认拼接str
text_str = "Drink see \u0be7\u0be6\u0be5\u0be4" "as 1999 = 1000 + 999"

text_bytes = text_str.encode("utf-8")

print("TEXT", repr(text_str), sep="\n ")
print("NUMBERS")
print(" str :", re_numbers_str.findall(text_str))
print(" bytes:", re_numbers_bytes.findall(text_bytes))

print("WORDS")

print(" str :", re_words_str.findall(text_str))
print(" bytes:", re_words_bytes.findall(text_bytes))

>>
TEXT
'Drink see ௧௦\u0be5\u0be4as 1999 = 1000 + 999'
NUMBERS
str : ['௧௦', '1999', '1000', '999']
bytes: [b'1999', b'1000', b'999']
WORDS
str : ['Drink', 'see', '௧௦', 'as', '1999', '1000', '999']
bytes: [b'Drink', b'see', b'as', b'1999', b'1000', b'999']

4.9.2 os中的字符串和字节序列

GNU/linux内核不理解Unicode, 因此文件名中使用字节序列都是无效的

为了方便处理字符串或字节序列形式的文件名或路径名, os模块提供了特殊的编码和解码函数

fsencode(filename)

当filename是str时使用sys.getfilesystemencoding()编码成bytes

fsdecode(filename)

当filename是bytes时使用sys.getfilesystemencoding()解码成str

在unix及其衍生平台中使用surrogateescape错误处理方式, 避免碰到意外字节卡住, windows使用strict, 这种处理方式在py3.1时被引进

这种错误的处理方式实际上是将无法编码的字节替换成UnicodeU+DC00U+DCFF之间的码位(low surrogate area), 这些码位是保留的,没有分配字符, 只供程序内部使用

4.10 本章小结

1.一个字符不等于一个字节, 两者的对应关系取决于编码所用的算法

2.chardet包可以在没有元数据的情况下检测编码的大概方法

3.在str-bytes的编解码中, 避免使用系统默认编解码

4.文本比较由于编解码的不同, 在比较之前需要进行规范化和大小写折叠

5.双模式API的自适应调整是以后的趋势

杂谈

Q1:纯文本是什么?

A: 由特定标准的码位序列组成的没有任何字节表示除文本之外的信息的结构化或非结构化计算机编码文本

Unicode从广义上讲比较复杂, 因为要面对多种实际情况, 但在日常使用中不需要有这种担心

Q2:在RAM中表示字符串

A: 在内存中, 使用固定数量的字节序列存储字符串的各个码位, 在py3.3之前, 编译Cpython时可以配置在内存中使用16|32位, 存储各个码位

  • 16: 窄构建 sys.maxunicode 65535
  • 32: 宽构建

从py3.3后, 创建str对象时, 解释器会检查里面的字符并自适应选择最适合的内存布局

Part5 Fun is Obj

5.0 1st Fun

在python中函数被称为一等对象

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

我们熟悉的int float string dict都属于一等对象

在python中所有函数都是一等对象

DocStrings 文档字符串是一个重要工具,用于解释文档程序,帮助你的程序文档更加简单易懂。

我们可以在函数体的第一行使用一对三个单引号 ‘’’ 或者一对三个双引号 “”” 来定义文档字符串。

你可以使用 doc(注意双下划线)调用函数中的文档字符串属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def test(n):
"""
test function
"""
return 1 if n < 2 else n * test(n-1)

print(test.__doc__)


# 将函数赋值给变量
fact = test

# 通过变量调用函数
print(fact(5))

# treat fun as params
for i in map(fact, [10, 1]):
print(i)
>>
test function

5.2 Higher Order Fun

接受函数为参数或者把函数作为结果返回的函数都是高阶函数(Higher-Order-Functions), 常见的有map sorted filter reduce apply(已经过时|使用不定量的参数调用函数, 已经使用解包代替)

为了使高阶函数具有更多的可读性, 一般会在高阶函数中使用匿名函数

reduce() 函数会对参数序列中元素进行累积。

函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

注意:Python3.x reduce() 已经被移到 functools 模块里,如果我们要使用,需要引入 functools 模块来调用 reduce() 函数:

  • 此函数多用求和
  • 现在已经出现sum()内置函数代替此函数
1
from functools import reduce

5.3 匿名函数

lambda用于创建匿名函数

  • 使用lambda创建
  • 多用作高阶函数的传参函数
  • lambda函数十分简单, 无法在定义体中赋值, 也不能使用while try等函数
  • 建议除了给高级函数作为传参函数外, 不要过度使用匿名函数

需要注意: lambdadef一样都会创建一个函数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import functools

a = [1, 3, 45, 5, 4, 23]

b = filter(lambda x: x > 19, a)
for i in b:
print(i)

c = functools.reduce(lambda x, y: x + y, [1, 2, 3, 4])
print(c)

d = all(map(lambda x: x > 10, [1, 10, 11]))
print(d)

f = any(map(lambda x: x > 10, [1, 10, 11]))
print(f)
>>
45
23
10
False
True

5.4 可调用对象

除了用户自定义的函数, 调用运算符()还可以用在其他对象上, 如果要判断一个函数是否可以调用, 可以使用callable()

python中的可调用对象可以分为以下几类:

  • 自定义的deflambda
  • 内置函数: Cpython实现的函数如len time.strftime
  • 内置方法: 使用C语言实现的, 比如dict.get
  • 类定义体中定义的函数
  • 类: 调用类会运行类的__new__创建一个实例, 之后运行__init__初始化实例, 最后把实例返回给调用方
  • 类的实例: 在类中定义了__call__
  • 生成器函数: yield 的关键字函数或方法, 生成器函数往往返回生成器对象

注意的是: 生成器函数在很多方面与其他可调用对象不同, 生成器函数还可以作为协程来使用

5.5 定义的可调用类型

python其实模糊了类和函数的界限, 要实现内部call的magic method, 在实现这个magic mthod时需要在内部维护一个状态, 让它在调用之间可用, 通常使用俩种方法 –> 闭包 | 装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Callable Obj
import random


class BingoCage:

def __init__(self, items):
# 根据PEP规范软性约束属性为私用属性|内部使用|import * 执行|解释器不强制执行
self._elems = list(items)
random.shuffle(self._elems)

def pick(self):
try:
return self._elems.pop()
except IndexError:
return LookupError("pick from empty BingoCage!")

def __call__(self):
return self.pick()


bingo = BingoCage(range(5))
print(bingo.pick())

5.6 函数内省

内省: 即通过函数名, 可以得到函数的更多信息, 通常需要借助fun_name.__doc__ dir(fun_name)

5.7 定位参数|关键词参数

1
def tag(name, *content, cls=None, **attrs):

name: 定位参数, 当没有key时按照位置赋值

*content: 接受除定位参数后的所有参数, 一般是一个可迭代对象, 并将这些参数解包成元组

cls=None: 关键词参数, 一般需要设置一个default_value

**attrs: 关键词参数, 可以接受多个关键词, 通常使用字典封装

定位参数

  • arg
  • *args common –> tuple(iterator)

关键词参数

  • key=value
  • **dict common –> dict(Obj)

py3接受了一个特性, 只接受关键词参数|即只接受特定的key

1
2
3
4
5
6
7
8
9
def f(a, *, b):
print(a, b)

f(1, 2)
>>
TypeError: f() takes 1 positional argument but 2 were given

----------
you should modify this function --> f(1, b=2)

5.8 获取参数的信息

获取函数对象的参数信息__code__可以获取参数的名字以及其他属性, 它本身返一个对象, 也有其他属性, __defaults__返回一个元组, 里面存储的是函数定位参数的缺省值, __kwdefaults__返回一个字典, 里面存储的关键词参数的缺省值

python中的代码对象是实现代码自省的重要概念, 代码对象通过fun.__code__访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def temp(x=1, y=2, *, z=3):
return x, y, z


temp(1, 32, z=3)
print(temp.__defaults__)
print(temp.__kwdefaults__)
print(temp.__code__.co_argcount)
print(temp.__code__.co_varnames)
>>
(1, 2)
{'z': 3}
2
('x', 'y', 'z')

代码对象

代码对象 code object 是一段可执行的 Python 代码在 CPython 中的内部表示。
可执行的 Python 代码包括:

  • 函数
  • 模块
  • 生成器表达式

当你运行一段代码时,它被解析并编译成代码对象,随后被 CPython 虚拟机执行。
代码对象包含一系列直接操作虚拟机内部状态的指令。
这跟你在用 C 语言编程时是类似的,你写出人类可读的文本,然后用编译器转换成二进制形式,二进制代码C 的机器码或者是 Python 的字节码被 CPU对于 C 语言来说或者 CPython 虚拟机虚拟的 CPU 直接执行。

代码对象除了包含 指令,还提供了虚拟机运行代码所需要的一些 额外信息。


Python 2中,函数的代码对象通过 函数.func_code 来访问;而 Python 3 中,则需要通过 函数.__code__ 来访问。
Python 3 的代码对象增加了一个新属性 co_kwonlyargcount,对应强制关键字参数 keyword-only argument

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
def clip(text, max_len=80):
"""
clip the text in the first whitplace
:param text:
:param max_len:
:return:
"""
end = None
if len(text) > max_len:
space_before = text.rfind(" ", 0, max_len)
if space_before >= 0:
end = space_before
space_after = text.rfind(" ", max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()


# 获取函数对象
print(clip.__code__)
# 获取函数的局部变量名
print(clip.__code__.co_varnames)
# 获取函数的定位参数数量
print(clip.__code__.co_argcount)
# 获取函数的字节码
print(clip.__code__.co_code)

即使我们通过函数对象__code__实现了函数自省, 但是其数据和效率并不高, 而且所获取到的信息可读性比较差

inspect模块

为了解决通过函数对象实现函数自省的效率和可读性问题, 我们可以通过inspect模块

1
2
3
4
5
6
7
8
9
10
from inspect import signature
sig = signature(clip)
print(sig)

for name, param in sig.parameters.items():
print(param.kind, ":", name, "=", param.default)
>>
(text, max_len=80)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

inspect.signature函数返回一个inspect.Signature对象, 这个对象有个很重要的属性是parameters, 是一个有序映射, 可以把参数名和inspect.Parameters对应起来.

Signature.Parameters

1
2
3
4
5
6
7
# 查看inspect.signature.parameters的数据
print(sig.parameters)
a = sig.parameters.items()
>>
OrderedDict([('text', <Parameter "text">), ('max_len', <Parameter "max_len=80">)])

odict_items([('text', <Parameter "text">), ('max_len', <Parameter "max_len=80">)])

各个Parameters也有自己独立的属性(封装在inspect.signature.parameters的有序映射里的对象体)

OrderedDict([('text', <Parameter "text">)]每个<Parameter>

kind : 函数对象的变量种类

name: 函数对象的变量名字

default: 函数对象的默认值

annotation: 注解属性|提供函数签名元数据

Signature.bind

inspect.Signature对象有一个bind方法, 可以将任意参数绑定到签名中的形参上, 所用的规则与实参到形参的匹配方式一样, 可以使用此方法进行调用函数前的验参(验证是否少参|多参)

1.inspect.Signature获取函数签名

2.讲特定函数签名通过Signnature.bind(**params)绑定进行验参

3.终端输出是否存在少参多参的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def check_sign(name, age, sex, *, book="Drink", title):
return name, age, sex, book, title


params = {"name": "Drink", "age": 21, "sex": "man", "book": "StarFire", "title": "SINUO", "temp": 1}

check_params = signature(check_sign)

try:
temp_sign = check_params.bind(**params)
print(temp_sign)
for name, value in temp_sign.arguments.items():
print(name, "=", value)
except Exception as e:
print(f"-----error---> something error {e}")
>>
-----error---> something error got an unexpected keyword argument 'temp'

5.9 函数注解

函数注解是python3的一种语法, 用于声明函数中的参数和返回值附加的元数据, 注解并不会对代码逻辑做任何处理, 只是将其作为[可视化]的元数据存储在函数对象的__annotations__属性里(表现形式是字典)

注解如下:

1
2
3
4
# 无注解形式
def clip(text, max_len=80):
# 注解形式
def clip(text:str, max_len:'int > 0'=80) -> str:
  • 注解加在par:后, 一般声明参数类型
  • 如果有默认值, 注解放在par:=之间
  • python对注解所作的唯一件事是将其放在fun obj的__annotation__属性里
  • 注解对python解释器没有任何意义, 注解只是元数据,供IDE框架和装饰器使用
1
2
3
4
5
6
def test_ann(name: str, age: int, book: str = 'StarFire') -> str:
return name, age, book

print(test_ann.__annotations__)
>>
{'name': <class 'str'>, 'age': <class 'int'>, 'book': <class 'str'>, 'return': <class 'str'>}
1
2
3
4
5
6
7
8
9
10
11
sign = signature(test_ann)
print(sign.return_annotation)
for param in sign.parameters.values():
# note = repr(param.annotation)
note = param.annotation
print(note, ":", "=", param.default)
>>
<class 'str'>
<class 'str'> : = <class 'inspect._empty'>
<class 'int'> : = <class 'inspect._empty'>
<class 'str'> : = StarFire

1.signature函数返回一个Signature对象, 这个对象拥有return_annotation属性和parameters属性

2.return_annotation返回函数对象注解

3.parameters属性返回一个字典, 其中字典的key是参数, value是Parameter对象

4.每个Parameter对象有独属于自己的属性

5.10 函数式编程

Python的不目标并不是称为函数式编程语言, 但是operatorfunctools等包的支持让其函数式风格十分快捷

Operator

其中绝大多数方法, 用于<算术运算>和<代替匿名函数从序列中取出元素或读取对象>, 即大多数其方法式实列化的匿名函数

其用法就如该目录一样即提供函数式编程|换句话说是所返回的实例本质上式函数|创建函数

1.算数运算

1
2
3
4
5
6
7
8
9
10
from operator import mul
from functools import reduce

def fact(n):
return reduce(lambda a, b: a * b, range(1, n + 1))

# optimization

def fact(n):
return reduce(mul, range(1, n + 1))

2.itemgetter取值

1
from operator import itemgetter

3.attrgetter取值

1
2
3
4
5
6
7
8
9
10
11
12
from operator import attrgetter
import pandas as pd

header = ["name", "id", "score", "age"]
temp = []
for item in sorted(students, key=attrgetter("id", "score", "age"), reverse=True):
temp.append([item.name, item.id, item.score, item.age])

file_path = "./students.xls"
print(temp)
df = pd.DataFrame(temp, columns=header)
df.to_excel(file_path, index=False)
1
2
3
4
    name        id  score  age
0 Drink 68491562 90 21
1 Star 21719561 85 20
2 Fire 10069164 96 23

4.methodcaller

methodcaller(fun_name, fun_args)

fun_name: str

fun_args: *args **args

本质上式访问的参数的类属性(get atteration)

1
2
3
4
5
6
7
8
9
10
11
12
# methodcaller
from operator import methodcaller

s = "The time has come"
upcase = methodcaller("upper")
print(upcase(s))
replace_case = methodcaller("replace", " ", "-")
print(replace_case(s))

>>
THE TIME HAS COME
The-time-has-come

5.10.2 使用functools.partial冻结参数

又称为偏函数, 即根据一个函数通过functools创建一个可调用对象, 其中实现了原函数的功能, 但是只接受其参数的子集, 否则抛出exception

functools提供了一系列的高阶函数, 其中使用频率最多的是reducepartial以及其变体partialmethod

使用partial冻结参数类似于将函数的部分参数设置了一个可变的缺省值

基于一个函数创建一个新的可调用对象, 并把原函数的某些参数固定, 使这个函数可以接受一个或者多个参数的函数改编成需要回调的API

1
2
3
4
5
6
7
8
9
from operator import mul
from functools import partial

triple = partial(mul, 3)
triple(7)
>> 21

list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]

partial的第一个参数接受一个可调用对象, 后面需要跟任意一个要绑定的定位参数和关键字参数

functools提供的高阶函数除了reduce partial partialmethod之外, 还有可用于缓存的lru_cache以及装饰器singledispatchwraps以及构造选择的函数itemgetter attrgetter

5.11 本章小结

函数的一等性

  • 可赋值给变量
  • 传给其他函数
  • 存储在数据结构中
  • 方位函数的属性
  • 供框架和一些工具使用

规约函数

函数调用规约,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数

偏函数

即根据一个函数通过functools创建一个可调用对象, 其中实现了原函数的功能, 但是只接受其参数的子集, 否则抛出exception

可调用对象

0.函数即是对象, 对象也是函数

1.lambda实现的方法

2.自定义或实现__call__的函数或对象

Part6 Imp DP Use 1 FunObj

6.0 抽象基类

定义一个接口或抽象类,并且通过执行类型检查来确保子类实现了某些特定的方法
运用 abc模块实现抽象基类

抽象方法: 表示基类的一个方法,没有实现,所以基类不能实例化,子类实现了该抽象方法才能被实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from abc import abstractmethod, ABCMeta

class Http_Base(metaclass=ABCMeta):
@abstractmethod
def get(self):
pass

@abstractmethod
def set(self, key, value):
pass

class Http(Http_Base):
def get(self):
pass
def set(self):
pass
  • 抽象类的一个特点是它不能直接被实例化
  • 抽象类的目的就是让别的类继承它并实现特定的抽象方法

Implement design patterns using first-class functions

上下文

把一些计算委托给实现了不同算法的可互换组件, 提供服务

策略

实现不同算法的组件共同的接口

具体策略

策略的子类, 用于实现不同的算法

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple("Customer", "name fidelity")


class LineItem:

def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price

def total(self):
return self.price * self.quantity


class Order:

def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion

def total(self):
if not hasattr(self, "__total"):
self.__total = sum(item.total() for item in self.cart)
return self.__total

def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount

def __repr__(self):
fmt = "<Order total: {:.2f} due: :{:.2f}>"
return fmt.format(self.total(), self.due())


# 策略: 抽象基类
class Promotion(ABC):

@abstractmethod
def discount(self, order):
"""
返回折扣金额(正值)
"""


class FidelityPromo(Promotion):
"""
第一个具体的策略
为积分1000以及上的顾客提供5%折扣
"""

def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):
"""
第二个策略
单个商品为20个及以上时提供10%折扣
"""

def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount


class LargeOrderPromo(Promotion):
"""
第三个具体策略
订单中的额不同商品达到10个及以上时提供7%折扣
"""

def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0

需要注意的是: 我们把Promotion定义为抽象基类(ABCMeta), 这么做是使用@abstractmethod装饰器除此之外还可以使用class Promotion(metaclass=ABCMeta)实现

1
2
3
4
5
6
7
8
9
10
11
12
13
joe = Customer("john doe", 0)
ann = Customer("ann smith", 1100)
cart = [
LineItem("banana", 4, .5),
LineItem("apple", 4, 1.5),
LineItem("watermellon", 5, 5.0)
]

print(Order(joe, cart, FidelityPromo()))
print(Order(ann, cart, FidelityPromo()))
>>
<Order total: 33.00 due: 33.00>
<Order total: 33.00 due: 31.35>

可以完全使用一等对象函数来实现这种策略, 并且代码更少

6.1 函数实现策略模式

在上面的例子中, 每一个实例化的抽象类Promotion都只实现了一个方法, 我们对此重构如下
使用作为一等对象的函数, 可以使某些设计模式进行简化

什么是策略模式: 定义一系列算法, 把他一一封装起来, 并且可以使他们相互替换(使算法可以独立于使用它的客户而变化)

策略模式对类的编排

策略模式示例:

电商策略模式:

  • 1000积分的顾客, 每单5%折扣
  • 同一个订单, 单个商品数量达到20个, 10%折扣
  • 订单中不同商品达到10, 7%折扣

假设每个订单只享受一个折扣

使用order类和函数实现的折扣策略

把具体的策略换成简单的函数, 去除抽象类

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from collections import namedtuple

Customer = namedtuple("Customer", "name fidelity")


class LineItem:

def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price

def total(self):
return self.price * self.quantity


class Order:

def __init__(self, customer, cart, promoition=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promoition

def total(self):
if not hasattr(self, "__total"):
self.__total = sum(item.total() for item in self.cart)
return self.__total

def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount

def __repr__(self):
fmt = "<Order total: {:.2f} due: {:.2f}>"
return fmt.format(self.total(), self.due())


def fidelity_promo(order):
return order.total() * .5 if order.customer.fidelity >= 1000 else 0


def bulk_item_promo(order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount


def lager_order_promo(order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
  • 计算折扣只需要调用self.promotion()
  • 再次表明了函数是一等对象:可以作为参数传递
  • 取消了抽象类
  • 各个策略都是函数
  • 为了把策略用到实例上, 只需要传入对应的策略函数即可

策略对象通常是很好的享元(flyweight)

  1. 享元是可以共享的对象, 可以同时在多个上下文中使用
  2. 享元不需要在每个上下文实例化, 减少消耗

显然易见, 即使是使用了函数作为策略实现, 但是在选择策略时,任然时人工选择, 接下来我们考虑实现 元策略, 即让它自动化实现最优方案

1
2
3
4
5
6
# 实例化函数:并非将其输入为string, 而是创建一个list包含了实例化的函数对象, 方便调用
promos = [fidelity_promo, bulk_item_promo. large_order_promo]

def meta_promo(order):
"""auto select best promo"""
return max(promo(order) for promo in promos)
  • promos列出函数实现的各个策略
  • 于其他*_promo函数一样, meta_promo函数的参数是一个order的实例
  • 使用生成器表达式, 将order作为参数传给fun list

这里的promos是一个存储函数对象的列表, 当你深刻意识到了函数的一等对象性, 你就不难发现, 这样构建函数列表的simple and right

1.作为参数传递

2.可以被数据结构化存储

如下例所示:

1
2
3
4
5
6
7
8
def temp():
pass


fun_list = [temp]
print(fun_list)
>>
[<function temp at 0x000001D96DA478C8>]

6.1.1 find all strategy in module

在python中, 模块也属于一等对象, 并且标准库提供了许多处理模块的函数

Globals()

返回一个字典, 表示当前的全局符号表, 这个符号表始终针对当前模块(对函数或方法来说是定义他们的模块而不是调用他们的模块)

动态收集促销品折扣函数更简单的一个方式是使用简单的装饰器

6.2 命令模式

命令模式也可以通过把函数作为参数而传递

主要解决的是解耦调用者和接收者, 通过中间对象command对象实现|如果要实现保存函数状态, 可以使用函数闭包

1
2
3
4
5
6
7
8
9
class Macrocommand:

def __init__(self, commands):
# init commands (list)
self.commands = list(commands)

def __call__(self):
for command in self.commands:
command()

6.3 本章小结

Recommand Books

  • Learning python design patterns

Part7 Decorators And Closuer

装饰器用于在源码中标记函数, 用于增强函数行为, 装饰器的基础时闭包(回调式异步编程和函数式编程的基础)

7.0 Target

  • 如何计算装饰器句法
  • 如何判断变量不是局部的
  • 闭包存在的原因和工作原理
  • nonlocal解决的问题
  • 行为良好的装饰器如何实现
  • 标准库中常见的装饰器
  • 实现参数化装饰器

7.1 DecoratorBasic

装饰器是一个可调用对象, 其参数是一个被装饰的函数, 其结果可能会处理被装饰的函数并将其返回, 或者将其替换成一个函数或可调用对象

在运行时改变程序的行为

装饰器

  • 接受: 函数
  • 返回: 处理后的函数|替换的函数 |可调用对象

特性:

  1. 能把被装饰的函数换成其他函数
  2. 装饰器在加载模块时立即执行
  3. 在被装饰函数定义之后立即运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def deco(func):
# deco函数返回的其实时inner
def inner():
print("running inner()")
return inner


@deco
def target():
print("running target()")
return

# 调用被装饰的target函数其实会运行inner
target()
# 审查对象发现target现在时inner的引用
print(target)

7.2 Py何时执行装饰器

在被装饰函数定义后立即执行, 通常是在导包时(加载模块)中执行

  • importrunning code时装饰器会首先运行, 被装饰的函数只有在调用时才运行
  • 通常情况下, 装饰器独自写成一个模块, 被其他模块通过import调用
  • 大多数情况下装饰器会在内部定义一个函数

7.3 装饰器改进策略模式

使用装饰器改进策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
promos = []

def promotion(promo_func):
promos.append(promo_func)
return promo_func

@promotion
def prom_1(order):
return

@promotion
def prom_2(order):
return

@promotion
def prom_3(order):
return

def best_promos():
for item in promos:
print(item)

best_promos()
  • 策略函数不需要一个特别名称
  • 装饰器突出了被装饰的函数的作用, 可以临时禁用或者注释, 只需要注释掉装饰器即可
  • 可以在其他模块中定义促销函数, 只需要添加装装饰器

转装饰器如果使用内部函数, 必须依赖于闭包实现

7.4 变量作用域规则

variable scoping rule

1.python在定义函数中, 默认函数中的变量都是local, 如果在执行中没有在local中找到对应的变量, 或者变量没有绑定值, python将会通过code crash的方式告诉用户, 而非像JavaScript一样找global代替

  • 在函数体中没有进行赋值操作
1
2
3
4
5
6
7
8
9
def test(a):
print(a)
print(b)

b = 1
test(1)
>>
1
1
  • 在函数体中进行了赋值操作
1
2
3
4
5
6
7
8
9
10
def test(a):
print(a)
print(b)
b = 2

b = 1
test(1)
>>
1
UnboundLocalError: local variable 'b' referenced before assignment

两个函数的字节码如下

可以看到俩这的差别, 一个是将b视为FAST另一个视为GLOBAL

将fun_block中的变量加载为global可以使用global关键词声明variable

7.5 闭包

只有涉及到嵌套函数时才会有闭包, 通常情况出现在定义的函数体中使用了lambda函数, 或者定义的装饰器内部含有函数

闭包的本质: 闭包是指延伸了作用域的函数, 其中包含函数定义体中引用, 但是不在定义体中定义的非全局变量, 闭包的关键问题不是匿名函数, 而是它能访问定义体之外定义的非全局变量

函数中创建的变量是一个局部变量。

局部变量的生命周期是等局部作用结束之后就会被释放掉。

如果内函数使用了外函数的局部变量,那么这个变量就与闭包函数发生了绑定关系,就延长该变量的生命周期。实际上就是内存给它存储了这个值,暂时不释放。


要实现一个函数不断计算系列值的均值

需要记录器历史值

方法一 类实现

使用类中和属性存储历史数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Averager:

def __init__(self):
self.series = []

def __call__(self, new_value):
self.series.append(new_value)
return sum(self.series) / len(self.series)


avg = Averager()

print(avg(10))
print(avg(18))
print(avg(17))
print(avg(22))
print(avg.series)

方法二 高阶函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个函数(类)
def make_averager():

# 在初始化时给函数(类)赋予属性
series = []

# 实现函数中的一个方法(enclosure)
def average(new_value):
series.append(new_value)
return sum(series) / len(series)

# 调用函数实例时返回的方法
return average

# 实例化一个函数并初始化其属性
avg = make_averager()
# 调用函数的实例实际上根据函数的定义
# 调用的是函数中的average方法
avg(2)
avg(12)

我们可以在fun.__code__(表示编译后的函数定义体)属性中查看变量作用域

需要注意的是

seriesmake_averager函数的局部变量, 因为在函数的定义体中已经初始化了series: series = [], 可是在调用avg(10)时, make_averager函数已经返回了, 而且它的本地作用域也消失了

averager函数中, series是自由变量freevariable, 指未在本地作用域中绑定的变量

averager的闭包延伸到那个函数的作用域外, 包含自由变量series的绑定

1
2
3
4
5
6
7
# 查看局部变量
print(avg.__code__.co_varnames)
# 查看自由变量
print(avg.__code__.co_freevars)
>>
('new_value',)
('series',)

series的绑定在返回的avg函数的__closure__属性中, avg.__closure__中的各个元素对应的avg.__code__.co_freevars中一个名称, 这些元素都是一个cell对象, 可以通过cell_contents属性访问保存的值

1
2
3
4
5
6
avg.__code__co_freevars
>> ("series", )
avg.__closure__
>> (<cell at 0x00000193478E8AF8: list object at 0x00000193479273C8>,)
avg.__closure__[0].cell_contents
>> [19, 20]

闭包本质上是一个函数, 它会保留定义函数时存在的自由变量的绑定, 这样在调用函数时, 虽然定义作用域不可用了, 但是使用那些绑定

需要注意的是, 只有嵌套在其他函数中的函数才可能需要处理不在全局

7.6 NoncalDeclaration

改进计算累加均值的方式, 只记录总值不再记录历史数据

nonlocal declaration not allowed at module level

1
2
3
4
5
6
7
8
9
def make_average():
count = 0
total = 0

def average(new_value):
count += 1
total += new_value
return total / count
return total / count

由于str int tuple属于不可变类型, +=本质上var = var + other, 因此这样会隐式创建一个局部变量, 这样count total就无法作为自由变量存在于闭包中, 因此在实例化函数对象时会失败

1
2
3
4
5
6
7
8
9
10
def make_average():
count = 0
total = 0

def average(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return total / count

nonlocal会把变量标记为自由变量, 即使在函数中为变量赋予了新值, 也会称为自由变量

nonlocal的作用可以简单理解为: 允许在闭包定义的变量从局部变量转变为自由变量, 从而实现调用定义体外的变量. 从宏观上看延长了变量的生命周期(调用完函数即释放), 并使变量具有记忆

7.7 实现一个简单的装饰器

1.decorator(func)等价于

1
2
@decorator
def func()

2.被装饰器装饰的函数, 其引用指向对应装饰器的对应内部函数

实例

这里实现了装饰器一个典型的行为, 把被装饰的函数替换成新函数, 两者接受一样的参数, 并且返回被装饰的函数本应该返回的值, 同时在这一个过程中会进行额外的操作

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
import time

def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
args_str = ", ".join(repr(arg) for arg in args)
print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result))
return result
return clocked


@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n-1)

factorial(1)
factorial(3)

print(factorial.__name__)

>>
[0.00000060s] factorial(1) -> 1
[0.00000030s] factorial(1) -> 1
[0.00000860s] factorial(2) -> 2
[0.00001510s] factorial(3) -> 6
clocked

需要注意的是:

1.装饰器会抹去被装饰器的__name____doc__属性, 可以使用functools.wraps装饰器把接受参数(func)的属性复制到被装饰函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def clock(func):
#
@functools.wraps(func)
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
args_str = ", ".join(repr(arg) for arg in args)
print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result))
return result
return clocked

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n-1)

7.8 标准库中的装饰器

标准库中常见的装饰器函数有:

property classmethod staticmethod

lru_cache singledispatch

7.8.1 lru_cache备忘

functools.lru_cache实现了备忘(memoization)的功能, 是一项优化技术, 它将耗时的函数结果保存起来, 避免传入相同参数时重复计算

LRU(least Recently Used)最近使用, 表示缓存不会无限增长, 一段时间不用会自动丢掉

lru_cache装饰器非常适合递归函数使用, 而且从web中获取信息也有很大优势

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
import functools
import time

def clock(func):
@functools.wraps(func)
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
args_str = ", ".join(repr(arg) for arg in args)
print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result))
return result

return clocked

@functools.lru_cache()
@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)

factorial(1)
factorial(9)
factorial(10)
print(factorial.__name__)
>>
[0.00000050s] factorial(1) -> 1
...
[0.00000040s] factorial(10) -> 3628800
factorial
  • 显示调用了10次
  • 不加装饰器会调用(19次)
  • 必须像常规函数一样调用lru_cache(), 加括号的原因是其可以接受配置参数
  • 叠放装饰器@lru_cache()@clock之前
  • 需要注意的是lru_cache可以使用两个可选参数, 签名如下:
1
2
3
4
from inspect import signature
print(signature(functools.lru_cache))
>>
(maxsize=128, typed=False)
  • maxsize:指定最大存储缓存数目, 满了之后旧的会被丢弃(为了最佳性能, maxsize应该设置成2的幂)
  • typed: 为True时会把不同参数类型得到的结果分开保存(即会把通常认为相等的浮点和整形分开保存)
  • lru_cache使用字典储存结果, key根据传入的定位参数和关键字参数创建, 所以被lru_cache都必须时hashable
1
2
3
4
5
6
7
8
9
@functools.lru_cache()
@clock
def factorial(n):
m = n.pop
return 1 if m < 2 else m * factorial(n)

factorial([1, 10])
>>
TypeError: unhashable type: 'list'

7.8.2 单分派泛函数

分派函数: 对于不同的传参类型进行不同的处理输出, 一般使用(if-else), 时间一长就会显得十分笨拙, 而且各个模块的耦合度逐渐增高

泛函数: 根据第一个传参类型进行自适应处理输出函数组

单分派函数: 只根据第一个传参类型进行自适应判断的泛函数

多分派函数: 根据多个参数进行自适应判断的泛函数

python3.4新增的functools.singledispatch装饰器会多个函数组合成一个函数组, 甚至可以为无法修改的类提供专门的函数, 使用@singledispatch装饰的普通函数会被成泛函数

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
from functools import singledispatch
from collections import abc
import numbers
import html

# 装饰器标记处理object类型的基函数
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return f"<pre>{content}<pre>"

# 各个专门函数使用@<base_function>.register(<type>)装饰
@htmlize.register(str)
# 使用占位符代替不必要的函数名
def _(text):
content = html.escape(text).replace("\n", "<br>\n")
return f"<p>{content}</p>"

# 为每个需要特殊处理的类型注册一个函数, numbers.Integral是int的虚拟超类
@htmlize.register(numbers.Integral)
def _(n):
return f"<pre>{n}</pre>"

# 可以叠放多个装饰器让同一个函数处理多个类型
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = "</in>\n<li>".join(htmlize(item) for item in seq)
return "<ul>\n<li>" + inner + "</li>\n<ul>"

只要可能, 注册的专门函数应该处理抽象基类(numbers.Integral) 和(abs.MutaleSequence), 不要处理具体实现(int | list) , 这样代码具有更高的兼容性

@singledispatch装饰器不是把java重载函数带入pyhton. 实际上在一个类中为同一个方法定义多个重载变体,比在函数中使用if-elif-elif块更好用, 但是他们有个共同的缺陷, 都将代码单元(类或函数)承担了太多责任, 装饰器的好处是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门的函数

装饰器是函数, 因此可以组合起来使用类似于泛函数

连接两者的桥梁是[fun oneself] – params – [regsiter fun]

7.9 StackDecorator

有时需要对一个函数进行多次装饰以满足现实的需求

7.10 参数化装饰器

解析源码中的装饰器时, python会把被装饰的函数作为第一个参数传递给装饰器, 如果想将其他参数传递给装饰器, 我们需要创建一个装饰器工厂函数把参数传递给工厂函数, 他将返回一个带参装饰器(函数), 再将这个装饰器运用即可

装饰器的参数化必须通过函数工厂的模式实现

7.10.1参数化注册装饰器

启用或禁用register执行的函数注册功能, 提供一个active参数, 设置为false, 不注册被装饰函数, 从概念上看, register函数不是装饰器, 而是装饰器工厂函数

如果装饰器工厂函数带有 [定位参数], 在使用装饰器时, 必须使用()带参, 否则装饰器并不会运行

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
registry = set()

def register(active):
# decorate 这个内部函数是真正的装饰器
# 其参数是一个函数
def decorate(func):
print(f"running register {func} --> {active}")
# 只有activate(从闭包中获取)为True时才注册func
if active:
registry.add(func)
else:
registry.discard(func)
# decorate是装饰器必须返回一个函数
return func
# register是装饰器工厂函数, 返回decorate
return decorate

# @register()
# @register(active=True)
# @register
def f1():
print(f"running 1")
return False

print("running main")
print(f"registry -> {registry}")
>> #1 TypeError: register() missing 1 required positional argument: 'active'
>> #2 registry -> {<function f1 at 0x00000212715E8158>}
>> #3 registry -> set()
  • @register工厂函数必须作为函数调用,并且传入所需参数
  • 即使不传入参数, register也必须作为函数@register()调用, 即要返回真正的装饰器decorate, 而不是作为函数对象调用@register
  • 我们需要调用装饰器工厂函数返回的装饰器函数, 而不是调用装饰器对象

关键在于使用@register(param)返回一个函数作为装饰器而不是使用@register的函数对象装饰

使用@register装饰器, 装饰函数时等价于register()(func)

本质上是register()返回一个函数对象在将(func)返回带入返回的函数对象**(装饰器)**


参数化的装饰器原理相当复杂, 参数化装饰器通常会把被装饰的函数替换, 而且结构上要多一层嵌套(形成装饰器工厂函数)

7.10.2 参数化的clock装饰器

在调用装饰器时, 是否使用函数化调用@decorate()取决于两个方面

1.装饰器是否需要传参

2.是否需要调用装饰器函数(否则调用装饰器函数对象)

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
35
import time

DEFAULT_FMT = "[{elapsed:0.8f}s] {name} ({args}) -> {result}"


# factory function
def clock(fmt=DEFAULT_FMT):
# the real decorate
def decorate(func):
# decorated the function
def clocked(*_args):
t0 = time.time()
# 被装饰函数真正返回的结果
# args是clocked的参数
_result = func(*_args)
elapsed = time.time() - t0
name = func.__name__
args = ", ".join(repr(arg) for arg in _args)
result = repr(_result)
# 在fmt引用clocked的局部变量
print(fmt.format(**locals()))
# 从clocked会取代被装饰的函数, 返回被装饰函数的返回值
return _result
return clocked
return decorate


if __name__ == "__main__":

@clock()
def snooze(seconds):
time.sleep(seconds)

for i in range(3):
snooze(.123)

装饰器最好的是通过__call__方法的类实现

7.11 本章小结

阅读清单

杂谈

把函数作为一等对象, 就必须要考虑一个问题: 作为一等对象的函数在某个作用域中定义, 但可能会在其他作用域中调用, 如何计算自由变量呢? python给出的答案是”词法作用域”(根据定义函数的环境计算自由变量)

Part8 ObjectOriented

本章目标如下:

  • variable is mark not box that stores data in python
  • object reference
  • mutable | varibility
  • garbage collection
  • tuple is unmutable data structure but it`s element can change
  • shallow copy and deep copy
  • object mark | value | weak reference | strong reference
  • del

8.1 变量不是盒子

  1. 变量不存储数据, 其是marker, 用来标记数据
  2. 多个变量标记同一个对象, 称为alias
  3. 赋值=, 先执行右边的对象计算, 对象在右边创建或获取, 在此之后创建变量并将变量标记(绑定)到对象上

8.2 标识|相等性|别名

标识: 对象在内存中的地址, 一个对象一旦创建, 在其生命周期内, 其标识不会改变, 返回标识的整数表示:id() 判断标识是否相等: is

别名: 多个变量绑定到同一个对象上, 这些变量统称为这个对象的别名, 别名具有等幂性(is == id)三等

相等性: python有两种相等性 数值相等通过对象定义的__eq__ or __len__定义以及内存地址相等__id__, 两者的运算符分别是== is

1
2
3
4
5
6
7
8
9
a = {1, 2, 3}
b = {3, 1, 2}
c = a

print(b == c, b is c, a is c)
print(id(a), id(c), id(b))
>>
True False True
2478949962920 2478949962920 2478949963144

对象的id在不同的实现中有所不同, Cpython中表示内存地址, 但其他的解释器中可能是别的值, 其本质是保证id唯一且在对象的声明周期内不可更改

8.2.1 is | ==

is: 比较两个对象的标识

==: 比较两个对象的值(存储的数据)

在变量与单例值的比较中使用is如与None的比较(查看变量绑定的值是否是None)

多数情况下, is运算比==快很多, 因为前者不能重载, 两者本质上的魔法方法是__id____eq__ + define by oneself

8.2.2 元组的相对不可变性

relative immutability of tuple

元组和多数python collection一样其内部存储的都是对象的引用, 如果引用的元素本身是可变的, 即使元组不可变其元素依然可变

元组的相对不变性指的是:tuple数据结构的物理内容(保存的引用)不可变[id()], 与引用的对象无关

str bytes array.array等单一类型序列是扁平的, 他们保存的不是引用而是在连续的内存中保存数据本身

元组中不变的是元组的标识id(), 与其内部的对象引用本身无关, 因此有些元组是不可以散列的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t1 = (1, 2, [1, 10])
t2 = (1, 2, [1, 10])

print(id(t1))
print(t1 == t2)

t1[-1].append(100)

print(t1)
print(t1 == t2)
print(id(t1))
>>
2375749450792
True
(1, 2, [1, 10, 100])
False
2375749450792

8.3 默认做潜复制

shallow copy by default

Python operations perform shallow copies by default, no special declaration is required

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a = [1, [2, 3], (4, 5, 6)]
b = list(a)

a.append(7)
a[1].remove(2)
print(f"a: {a}")
print(f"b: {b}")

# list -> mutable type iadd will extend ori obj
b[1] += [4, 5]
# tuple -> immutable type iadd will create a new obj
b[2] += (7, 8)

print(f"a: {a}")
print(f"b: {b}")

>>
a: [1, [3], (4, 5, 6), 7]
b: [1, [3], (4, 5, 6)]
a: [1, [3, 4, 5], (4, 5, 6), 7]
b: [1, [3, 4, 5], (4, 5, 6, 7, 8)]

构造方法或[:]都是浅复制(复制了最外层容器, 副本中的元素是源容器的引用), 如果容器内部的元素都是不可变的, 那么不会出现异常且会节约内存, 如果容器内的元素有可变类型的, 对容器内的元素进行操作会有异常现象


浅复制容易操作, 但对于可变类型来说会出现异常情况

8.3.1 DeepCopy

sometime, we will use deep copy to avoid exception.

1
2
3
4
5
6
7
import copy

# shallow copy
copy.copy(iterable obj)

# deep copy
copy.deepcopy(iterable obj)

一般情况下深复制不是一件简单的事情, 因为有嵌套引用的存在, 但有时深复制可能太深, 对象会引用不该复制的外部资源或单例值, 我们可以是实现__copy__() __deepcopy__() magic method 来自定义copy以及deepcopy的行为

8.4 函数的参数作为引用时

python唯一支持的参数传递方式是<共享传参>, 并且大多数面向对象语言都采用这一模式

共享传参指函数的各个形式参数获得实参中各个引用的副本, 换句话说函数内部的形参是实参的别名, 这种方式可能会修改作为参数传入的可变对象, 但是无法修改那些对象的标识(不能把一个对象替换成另一个对象)

函数对改变任何传入的可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def f(a, b):
a += b
return a


x, y = 1, 1
a, b = [1, 2], [3, 4]
c, d = (1, 2), (3, 4)

f(x, y)
f(a, b)
f(c, d)

print(x, y)
print(a, b)
print(c, d)
>>
1 1
[1, 2, 3, 4] [3, 4]
(1, 2) (3, 4)

8.4.1 不要使用可变类型作为参数的默认值

可选参数设置默认值可以使API在进化的同时保证向后兼容, 但是我们应该避免使用可变参数作为参数的默认值

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
class HanutedBus:
"""this is a haunted bus"""

def __init__(self, passengers=[]):
self.passengers = passengers

def pick(self, name):
self.passengers.append(name)

def drop(self, name):
self.passengers.remove(name)

def __repr__(self):
return f"{self.passengers}"


bus1 = HanutedBus(["Drink", "Star"])
bus1.pick("Fire")
bus1.drop("Drink")
print(bus1)

bus2 = HanutedBus()
bus2.pick("Haunt")
print(bus2)

bus3 = HanutedBus()
print(bus3)
>>
['Star', 'Fire']
['Haunt']
['Haunt']
# bus2 bus3 共用了一个passengers list对象

在实例化对象时, 如果passengers为空, self.passengers变成了passengers参数默认值的别名, 这是因为默认值在定义函数时计算(通常在加载模块中), 因此默认值变成了函数对象的属性

我们可以通过审查HauntedBus.__init__对象, 查看其中的__defaults__属性中的haunted passengers

1
2
3
4
5
print(dir(HanutedBus.__init__))
print(HanutedBus.__init__.__defaults__)
>>
['__annotations__', ..., 'defaults']
(['Haunt'],)

通过验证得出bus2.passengers是一个别名, 它绑定到了HauntedBus.__init__.__defaults__属性的第一个元素上:

1
2
3
4
HauntedBus._-init__ --> return >>function

HanutedBus.__init__.__defaults__[0] is bus2.passengers
>> True

8.4.2 Defense Var Paras

预防可变参数是指: 是否将在函数|类中对参数的处理体现到函数外部

  • 防御可变参数最好的方式是: 类或函数自己维护参数, 通过built-in constructor 构造自己的参数
1
2
3
4
5
6
7
8
class HanutedBus:
"""this is a haunted bus"""

def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)

除非希望这个方法修改传入的参数对象, 否则在类中直接把参数赋值给实例变量之前一定要深思, 因此这样会为参数对象创建别名, 如果不确定就使用<创建副本>的方式取变量

8.5 Del & GC

del: 删除名称, 通过删除名称减少对象引用, 进而会导致删除对象(引用为0)

对象不会自行销毁; 然而无法得到对象时, 可能会被当作垃圾回收

对象销毁情况:

  • 对象引用为0
  • 对象仅存在相互引用(垃圾回收程序会判断两者都无法获取进而销毁)
  • 重新绑定对象导致引用为0
  • 无法得到对象

__del__特殊方法不会销毁实例, 不应该在代码中调用, 但在即将销毁实例时Python解释器会调用__del__给实例释放外部资源的机会

Cpython垃圾回收算法:

引用计数

  • 每个对象都会统计有多少引用指向自己
  • 当引用数为0时, 调用__del__(如果定义了)并销毁对象
  • 释放分配给对象的资源(内存)

分代垃圾回收算法 [Cpy2.0]

  • 检测引用循环中涉及的对象组
  • 如果一组对象之间全是相互引用, 即使在出色的引用方式也会导致组中的对象不可获取

更复杂的收回算法

  • 也有不依赖引用的垃圾回收算法, 这意味者对象引用数量为0时, 可能不会立即调用__del__方法

使用弱引用可以监控对象的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import weakref
a = {1, 2, 3}
b = a

def bye():
print("Gone with the wind...")

ender = weakref.finalize(a, bye)
print(ender.alive)
del a
print(ender.alive)
del b
print(ender.alive)
>>
True
True
Gone with the wind...
False

8.6 weakrefence

弱引用不会增加对象的引用数量, 不会妨碍对象被当作垃圾回收, 往往用在缓存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import weakref

a_set = {1, 2, 3}
# 创建一个弱引用对象
wref = weakref.ref(a_set)

print(wref)
# 调用wref()返回被引用的对象
print(wref())

# 重绑定
a_set = [1, 2]

print(wref)
print(wref())
print(wref() is None)
>>
<weakref at 0x0000026B7900EF98; to 'set' at 0x0000026B791F1E48>
{1, 2, 3}
<weakref at 0x0000026B7900EF98; dead>
None
True
  • wref 重新监测弱引用对象是否被释放|未释放返回对象|否则返回None
  • wref() 获取最近监测的弱引用对象

需要注意的是: weakref.ref类是一个底层接口, 供高级用途使用, 多数程序最好使用weakref集合和finalize

  • WeakKeyDictionary
  • WeakValueDictionary
  • WeakSet
  • finalize(在内部使用弱引用)
  • 不建议手动创建并处理weakref.ref实例

8.6.1 WeakValueDict

此类实现的是可变映射, 里面的value是对象的weakreference, 当被引用的对象被当作垃圾回收后, 对应的键会自动删除, 因此其常常用作缓存cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import weakref
stock = weakref.WeakValueDictionary()
catalog = [cheese("Red"), cheese("Blue"), cheese("Green"), cheese("White")]

for cheese in catalog:
stock[cheese.kind] = cheese

print(list(stock.values()))
print(list(stock.keys()))

del catalog

print(list(stock.values()))

del cheese

print(globals())
print(list(stock.values()))
>>
[-Red, -Blue, -Green, -White]
['Red', 'Blue', 'Green', 'White']
[-White]
{'stock': <WeakValueDictionary at 0x1e8719b5a88>}
[]
  • del catalog后查看stock发现仍有一个white没有deleted
  • 这是因为还有一个临时变量cheese引用了cheese("white")
  • 可以通过global()验证, 临时变量如果是局部变量则会在函数返回时被销毁, 但是在此次例子中, cheesefor loop中的全局变量, 需要手动(显式)删除

WeakValueDictionary相对应的是WeakKeyDictionary, 后者的键是弱引用, 其用途为:

  1. 可以为应用中其他部分拥有的对象附加数据, 这样无需为对象添加属性(这对覆盖属性访问权限的对象尤其有用)

WeakSet

保存元素弱引用的集合类, 元素没有强引用时, 集合会将它删除, 如果一个类需要知道所有实例, 一种好的方案是创建一个weakset类型的类属性, 保存实例的引用, 直到实例被回收, 否则其存在时间和python进程一样长

weakkey|valuedict|set例子参考

weakfinalize

8.6.2 Limitions(WF)

the limitions of weak reference

不是所有python对象都可以作为弱引用的目标, list dict实例就不可, 但是它们的子类可以解决这个问题

具体限制如下(受于Cpython的实现细节, 其他解释器可能有所不同), 这些局限时内部优化导致的结果

  • list -> 其子类可以弱引用
  • dict -> 其子类可以弱引用
  • int -> 不可引用|子类也不可
  • tuple -> 不可引用|子类也不可
1
2
3
4
5
6
7
8
9
10
11
12
class MyList(list):
""""""
a_list = MyList(range(10))
wref_to_a_sub_list = weakref.ref(a_list)
temp = [1, 2, 3]

wref_to_a_list = weakref.ref(temp)

>>
# list不可以直接弱引用
wref_to_a_list = weakref.ref(temp)
TypeError: cannot create weak reference to 'list' object

8.7 Py对不可变类型的实现

  1. Cpython为了优化将不可变类型的copy [:] 等不创建副本而是指向同一个引用
  2. 收到影响的有tuple int str bytes frozenset
1
2
3
4
5
6
7
8
a = "121"
b = "121"

print(a == b)
print(a is b)
>>
True
True

共享字符串字面量是一种优化措施, 称为驻留

这些trick的目的时节省内存提升解释器的速度

8.8 Summary

Python对象三要素

标识(id) 不可变 一般是整数内存地址, 通过id()获取, is比较 通过构造创建副本id()不同
类型(type) 可变但不推荐变化 通过__class__属性指定其他类
值(value) 可变 通过==比较 赋值 | 引用

一般俩个变量具有相同的值== -> True, 有两种情况, 副本或者别名(共同引用), 对于不可变对象没啥区别, 如果涉及到可变变量需要小心

变量保存的是引用

  • 简单的赋值不创建副本, 副本一般通过[:] 构造list().. copy.deepcopy()
    • 副本和引用不同的区别是, 是否开辟了内存创建了一个新对象(id不同)
  • += *= 的增量赋值, 如果左边的变量绑定的是不可变对象, 则会创建一个新对象赋值, 否则是就地修改
  • 对现有的变量赋予新值, 不会修改之前绑定的变量(重新绑定: 现有的变量绑定了其他对象), 需要注意的是, 如果该变量是该对象的唯一绑定那么, 绑定后该对象会被当作垃圾回收
  • 以别名的形式给函数传参, 函数会修改通过参数传入的可变对象, 除非在函数中创建副本或传入不可变对象
  • 使用可变类型作为函数参数的默认值有很大风险, 不推荐使用
  • 在某些情况下需要保存对象的引用, 但不留存对象本身, 可以使用弱引用

Other

== 比较数值

is 比较引用

可变性

1.python是面向对象编程语言, 在定义类的属性时可以是可变的或不可变的

2.可变对象会导致多线程编程难以实现, 因为一个线程修改了数据如果没有及时同步就会损坏数据, 但是过度同步会导致死锁

对象析构和垃圾回收

python没有直接销毁对象的机制, 而是通过引用, 避免销毁时存在强引用

Cpython主要用过引用计数实现垃圾回收, 但是在碰到循环引用时容易泄露内存**(由于疏忽或错误造成程序未能释放已经不再使用的 内存)Cpython2.0后引入了分代垃圾回收**(可以将引用循环中不可获取的对象销毁)

注意

1
open("test.txt", "wt", encoding='utf-8').write("123")

这行代码是安全的, 因为文件对象的引用数量会在write后归零, Python在销毁内存中表示文件的对象前, 会先关闭文件, 但是我们仍然提倡使用显示关闭文件, 即使用with语句, 他能保证文件一定会被关闭, 即使打开文件时抛出了异常也无妨

考虑到(其他环境|语言):

  • 回收程序复杂
  • 销毁对象和关闭文件时间可能会更长
1
2
with open("test.txt", "wt", encoding='utf-8') as fp:
fp.write("123")

参数传递:引用传递

传递方式有两种:

按值传递:函数得到的是参数副本

按引用传递: 函数得到的参数的指针(引用)

python中, 函数得到是参数的副本,但是参数始终是引用, 即参数引用的副本(共享参数)

Part9 Python-Style Obj

Never use two leading underscores, it is annoyingly selfish

本章我们将自定义类并实现内置对象的结构和行为包括:

  • 支持用于生成对象其他表示形式的内置函数repr() bytes()
  • 使用一个类方法实现备选构造方法
  • 扩展内置的format()str.format()
  • 实现只读属性
  • 将对象变为可散列的, 以便在set 或作为dictkey
  • 利用__solt__节省内存
  • 使用@classmethod@staticmethod装饰器
  • python的私有变量和受保护属性的用法约定和局限

9.1 Obj Representation

每种面向对象语言都有一种获取对象的字符串表示形式的标准方式, python提供了str() repr()

对象表示函数 表示类型 内置函数 返回类型
str() 字符串表示 __str__() str (Unicode-str)
repr() 字符串表示 __repr__() str (Unicode-str)
bytes() 字节序列表示 __bytes__() bytes
format() 特殊格式的字符串表示 __format__() str (Unicode-str)
str.format() 特殊格式的字符串表示 __format__() str (Unicode-str)

9.2 Vector Class

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
import math
from array import array


class Vector:
type_code = "d"

def __init__(self, x, y):
self.x = float(x)
self.y = float(y)

# unpacking
def __iter__(self):
return (i for i in (self.x, self.y))

def __repr__(self):
class_name = type(self).__name__
return f"{class_name} ({self.x}, {self.y} )"

def __str__(self):
return str(tuple(self))

def __bytes__(self):
return (bytes([ord(self.type_code)]) +
bytes(array(self.type_code, self)))

def __eq__(self, other):
return tuple(self) == tuple(other)

def __abs__(self):
return math.hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

9.3 Alternative Constructions Method

实现将Vector的字节序列转成Vector实例

1
2
3
4
5
6
7
8
9
10
# 类方法使用classmethod装饰器修饰
@classmethod
# 不传入self(类的实例), 传入cls(类本身)
def frombytes(cls, octets):
# 从第一个字节种获取type_code
type_code = chr(octets[0])
# 使用传入的octets字节序列创建一个memoryview, 在使用typecode转换
memv = memoryview(octets[1:]).cast(type_code)
# 拆包转换后的memoryview得到构造方法所需的一对参数
return cls(*memv)

9.4 classmethod|staticmethod

@classmethod

类方法装饰器|定义备选构造方法, 用它装饰的第一个函数参数必须是cls本身, 而非self类实例, 常用于创建新的类实例

@staticmetho

静态装饰器, 装饰的函数将会与类失去交互, 常用在和类相关联的函数定义中, 但实际效果和定义在类的上下文一样, 意义更在于一种提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Demo:

@classmethod
def test_class(*args):
# 第一个参数返回的始终是cls
return args

@staticmethod
def test_static(*args):
return args


print(Demo().test_static())
print(Demo().test_static("sss"))
print(Demo().test_class())
print(Demo().test_class("sss"))
>>
()
('sss',)
(<class '__main__.Demo'>,)
(<class '__main__.Demo'>, 'sss')

9.5格式化显示

内置的format()str.format()方法本质上调用的是.__format__(format_spec)

格式规范微语言

{0.mass:5.2e}

  • 0.mass: 在代换字段句法种是字段名,
  • : : 分隔符
  • 5.2e: 是格式说明符

格式规范微语言为一些内置类型提供了专用的表示代码, 比如

  • b -> 二进制

  • x -> 十六进制的int

  • f表示float类型

  • % 百分数

1
2
3
4
5
6
7
8
9
print(format(42, "b"))
print(format(42, "x"))
print(format(42, ".2f"))
print(format(2/3, ".2%"))
>>
101010
2a
42.00
66.67%

格式规范微语言是可扩展的, 各个类可以自行决定如何解释format_spec参数, 并且一个类如果没有定义__format__方法, 从Obj继承的方法会返回str(my_obj)


格式规范微语言的本质上将类的部分参数以一定的格式显示

9.6 可散列的类

可散列的类是指类实现了__hash__ __eq__方法, 且类的实例在其生命周期内hash()不变, 有时还要实现属性的只读

1.eq

使用==或其他方式判断两个类实例是否相等

1
2
def __eq__(self, other):
return tuple(self) == tuple(other)

2.hash

最好使用位运算符异或运算, 混合各分量的散列值

1
2
def __hash__(self):
return hash(self.x) ^ hash(self.y)

3.属性只读

init属性时使用self.__x双前导保证属性的私有

在获取属性时, 返回私有属性并添加装饰器@property把读值标记为特性

1
2
3
4
5
6
7
def __init__(self, x, y):
self.__x = x
self.__y = y

@property
def x(self):
return self.__x

@property

python @property 装饰器使一个方法可以像属性一样被使用,而不需要在调用的时候带上()

[example]

4.不变性

我们有时需要实现类中的某些属性具有不可变性, 以便实现__hash__中的id()不变


创建hashable类型, 不一定要实现[@property]特性, 也不一定要保护实例属性, 只需要正确的实现__hash____eq__方法即可, 但是, 实例的散列值绝不应该变化, 因此我们借机实现了只读属性

9.7 私有属性和受保护属性

首先明确两个概念, python虽然可以设置所谓的私有属性通过two leading underscores 但其并不是真正的private attribute而是一种的name mangling<命名改写>, 仅仅是一种保护装置, 通过将属性name mangling_CLS__attr, 保护属性在继承或获取时隐藏, 但是我们依然可以使用instance._cls__attr获取属性或者对属性进行update

python不像Java使用private修饰符创建私有属性, 但是python可以通过self.__attr将属性通过name mangle改写成fake-private-attr, 起到保护作用

这样改写在一些人看来是自私的, 也有人建议通过命名soft-constraint进行约束, 即约定one leading downscore -> self._x, 如果担心命名冲突应该明确使用_Cls_attr进行属性命名

name mangle <名称改写>是一个安全措施, 其保护的是意外改写, 不并不防止故意改写

1
2
3
4
5
6
7
8
9
10
11
12
13
from array import array
import math


class Vector:
type_code = "d"

def __init__(self, x, y):
self.__x = x
self.__y = y

vector = Vector(1, 2)
vector._Vector__x = 3
  • python解释器不会对单个下划线的属性名做特殊处理, 这仅仅是一种约定, 程序员应该遵守约定, 不再类外部访问这种属性, 就像使用全大写字母编写常量
  • 但是在顶层名称使用一个前导下划线的话, 确实会有影响, 对from mymod import *, mymod中前缀为下划线的名称并不会导入, 但是依然可以使用from mymod import _private_func导入
  • python文档中在类中使用self._x single downsoce很常见, 表示此属性为类的私有属性(很少有人称为受保护属性), 尽在内部使用(尽管是一个口头约定)

总结:

私有属性: 使用self.__x, 通过name mangle实现, 为了防止类在inherit时属性丢失以及外部访问, 但是它从本质上是一个取巧装置, 我们依然可以通过_Cls_attr外部访问并修改值

受保护属性: 使用self._x, 完全通过口头约定, 的soft sonstraint

9.8 使用slots类属性节省内存

默认情况下, python在各个实例中名为__dict__的字典里存储实例属性, 这样做的目的是为了使用底层的散列表提升访问速度, 但是字典会大量消耗内存, 如果要处理数百万个属性不多的实例, 通过__slots__类属性, 能节省大量内存, 其本质上是使用tuple存储实例属性而不是dict

继承自超类的__slots__属性没有效果, Python只会使用各个类中定义的__slots__属性

定义__slots__的方法是, 创建一个类属性, 使用__slots__这个名字, 并把它的值设为一个字符串构成的可迭代对象, 元素为每个实例属性, 建议使用元组避免信息发生变化

1
2
3
4
5
6
7
8
class Temp:
# define class`s slots attribute
__slots__ = ("x", "y", "__name")

def __init__(self, x, y, name):
self.x = x
self.y = y
self.__name = name
  • 类中__slots__属性是告诉解释器, 类中的所有实例属性都在此, 这样python会在各个实例中使用类似元组的结构存储实例变量, 从而避免使用消耗内存的__dict__属性, 这样可以节省大量内存, 缓解服务器压力 (56%)
  • __slots__不仅可以降低内存消耗, 而且运行速度也会提升
  • __slots__定义属性后, 实例不再有所列之外的属性, 这是slots的副作用, 其本质目的是为了提升内存使用率, 而不是为了约束内存, 也不推荐这样使用
  • 如果在__slots__中添加__dict__, 那节省的内存会再次被吃掉, 实例会在元组中保存属性, 此外还支持动态创建属性, 这些动态属性会出存在__dict__
  • 如果要使对象支持弱引用weak ref, 必须在__slots__中添加__weakref__
  • 如果套处理数百万个数值对象, 应该使用Numpy数组, 其能够高效使用内存并对数值处理函数进行了高度优化
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
class vector:
__slots__ = ("x", "y")
def __init__(self, x, y, name):
self.x = x
self.y = y
self.name = name

vector1 = vector(1, 2, "Drink")
print(vector1.name)
>>
self.name = name
AttributeError: 'vector' object has no attribute 'name'

# -----------------
...
__slots__ = ("x", "y", "__dict__")
...
>>
Drink

# ------------------
print(vector1.__dict__)
print(vector1.__slots__)
>>
{'name': 'Drink'}
('x', 'y', '__dict__')

slots的问题

  • __slots__: 静态属性

  • 不可滥用, 其存储的使静态属性, __dict__ :存储动态属性

  • 本质上是为了优化内存, 而不是约束属性获取

  • 如果要同时实现对象的弱引用, 需要在其中添加__weakref__

  • 每个子类都要继承__slots__属性, 因为解释器会忽略继承

  • 可以创建 禁止创建动态属性 和 不支持弱引用的类

9.9 OverrideClsAttr

在定义class的属性时, 可以在__init__之外定义class属性而非instance属性, 并且这个属性可以直接通过cls.attr访问, 即是没有初始化实例对象, 也会生成对应的cls.attr

  1. 类属性作为实例属性的默认值, 一旦被实例化, 类属性从用法上将转化为实例属性
  2. 修改类属性必须通过类修改, 实例无法修改类属性
  3. 类属性也可以在子类中实现Override
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
class Temp:
a = 1
b = 2

def __init__(self, x, y):
self.x = x
self.y = y

print(Temp.a)
print(Temp.b)
print(Temp.x)
>>
1
2
AttributeError: type object 'Temp' has no attribute 'x'


# ============
...
a = 1
b = 2
__slots__ = ("x", "y")
...
print(Temp.a)
print(Temp.b)
print(Temp.x)
>>
1
2
<member 'x' of 'Temp' objects>

pythonclassinstance关于attribute的注意事项

  • 在定义cls属性时, 相当于给instance属性做了default value, 当两者属性key冲突时, 解释器取instance属性
  • 在未定义instance属性并slef.attr获取时, 解释器回去取cls属性(default value)
  • instance可以更新自己的cls属性
    • 可以通过在类中添加__slots__声明, 将属性变为静态属性, 即只能通过cls对属性进行更新, instance拿到的是only-read
    • 类属性有个特点: 被实例化后变成<实例属性>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Temp:
a = 1
b = 2
__slots__ = ("x", "y")

def __init__(self, x, y):
self.x = x
self.y = y

def __repr__(self):
return f"a ->{self.a}"

temp = Temp(3, 4)
print(temp.a)

temp.a = 10

temp2 = Temp(3, 4)
print(temp.a)
>>
1
AttributeError: 'Temp' object attribute 'a' is read-only

9.10 Summary

本章主要实现了python style的对象

有关对象的magic method

  • 将对象用str bytes展示:
    • __str__
    • __repr__
    • __format__
    • __bytes__

备选构造方法

通过对方法使用@classmethod装饰器, 将接收cls参数, 并重构cls返回一个类**(cls)**

1
2
3
4
5
6
7
8
9
10
11
12
...
# 类方法使用classmethod装饰器修饰
@classmethod
# 不传入self(类的实例), 传入cls(类本身)
def frombytes(cls, octets):
# 从第一个字节种获取type_code
type_code = chr(octets[0])
# 使用传入的octets字节序列创建一个memoryview, 在使用typecode转换
memv = memoryview(octets[1:]).cast(type_code)
# 拆包转换后的memoryview得到构造方法所需的一对参数
return cls(*memv)
...

可散列对象

  • 实现__hash__ __eq__
  • 要保证对象在其生命周期内hash(obj)不会变化, 通常会选择cls中的部分属性, 将其private_attr并以特性@property变为私有只读属性, 并用异或^ + hash(attr) 实现__hash__

节省内存

  • 使用__slots__节省类的内存使用, 但是其本身也有其他作用
    • 不能隐式继承
    • 声明的是静态属性与__dict__相反(动态属性)
    • 会阻止不再声明内的属性的访问
    • 如果要同时实现对象的弱引用, 需要在其中添加__weakref__

属性的覆盖

  • 类属性是实例属性的前置默认值, 类被实例化后, 类属性在表现形式上为实例属性
  • 类属性无法通过实例修改
  • 类属性在继承或实例化后可以Override
  • 类属性 + __slots__ 可以实现only-read

__index__

作用: 将对象强制转化成整数索引

用途: 在特定的序列切片场景使用, 满足Numpy需求, 或需要新键一种数值类型, 并想把它作为参数传递给__getitem__方法

特性 -> [私有属性一起使用]

@property

def attr()

将类中的方法以属性的方式访问, 如果方法和类同名, 可以避免属性被意外更新(avoid accidental updates)

和私有属性一起使用

python中我们可以大胆定义属性, 在之后使用特性, 避免属性被意外更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test:

def __init__(self, x):
self.__x = x

@property
def x(self):
return self.__x

# @x.setter
# def x(self, new_x):
# self.__x = new_x

test = Test(1)
print(test.x)
test.x = 2
print(test.x)
>> ori code
1
AttributeError: can't set attribute

将@x.setter开打后

1
2
1
2

将属性变为公开属性 self.__x => self.x初始化不成功

1
RecursionError: maximum recursion depth exceeded

私有属性的安全和隐私

严格意义上私有属性没有所谓的安全和隐私

私有属性:

  • self.__x
  • soft constraint
  • 通过instance._Cls__private访问和修改

受保护属性

  • self._x
  • 口头约束
  • 直接访问和修改

Question

Q21 SSLError

-Info

在使用AsyncHTTPClient调用API时, 出现错误

1
2
AsyncHTTPClient
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:852)

-Analysis

1
2
3
4
url = "https://txm.zidouchat.com/wx/get_access_token"
data = {"token": "doododisagreatcompany"}
body = json.dumps(data)
response = await httpclient.fetch(url, method="POST", body=body)

debug排查:

1
2
3
4
5
6
7
print(ssl_options, host)
if ssl_options is not None:
if timeout is not None:
stream = yield gen.with_timeout(timeout, stream.start_tls(False, ssl_options=ssl_options, server_hostname=host))
else:
stream = yield stream.start_tls(False, ssl_options=ssl_options,server_hostname=host)
raise gen.Return(stream)

此网站的ssl证书验证失败

1
<ssl.SSLContext object at 0x7f9f0d393ac8>   txm.zidouchat.com

-Solution

方法一

在使用http链接建立请求时关闭ssl验证

1
2
3
4
5
6
7
8
9
10
11
# 添加ssl跳过验证
import ssl
ssl_options = {"ssl_version": ssl.PROTOCOL_TLSv1}

# 添加ssl跳过验证
if ssl_options is not None:
if timeout is not None:
stream = yield gen.with_timeout(timeout, stream.start_tls(False, ssl_options=ssl_options, server_hostname=host))
else:
stream = yield stream.start_tls(False, ssl_options=ssl_options,
server_hostname=host)

方法二

在实例化时赋予属性 validate_cert=False, 有一定风险仍然使用old_ssl

1
2
3
4
5
6
7
8
import tornado.httpclient

request = tornado.httpclient.HTTPRequest(
url="https://myhostname.com:9001",
method="GET",
validate_cert=False
)
print tornado.httpclient.HTTPClient().fetch(request)

-Review

curl http_address -k

可以跳过ssl验证

1
I'm trying to use tornado's http client to fetch a URL. I've done this before many times, but I'm getting a really odd SSL error this time. The endpoint I'm trying to consume does not have a valid cert, but a -k on a curl call still proves it works.

Q20 Docker Compose

-Info

1
2
# 拉取镜像报错
docker-compose up

-Analysis

1
2
3
4
5
6
7
# 报错信息
ERROR: write /var/lib/docker/tmp/GetImageBlob264931462: no space left on device

# 指宿主机内部没有空间写入docker文件

# 查看内存使用
df -h

-Solution

1
2
# 清除宿主机内不在使用的imgaes
docker rmi $ (docker images -aq)

Q19GIt PUll失败

-Info

1
2
3
4
(venv) edy@bogon TXM % git pull

Your configuration specifies to merge with the ref 'refs/heads/watsons_uat'
from the remote, but no such ref was fetched.

-Analysis

  1. 远程仓库不存在要pull的分支
1
2
3
4
5
6
本地仓库当前分支是a,但是a的远程分支被删除掉了,pull的时候就会报错,这时候使用
git remote prune origin
同步下远程分支,并清除掉已删除的分支,再清除掉当前分支(如果当前分支在工作区的话)就可以了

建议:使用命令行拉取新分支代码前使用:git fetch -p 获取最新分支 然后获取远程分支代码
git fetch -p

-Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 切换到其他分支, 建议master分支
# 同步远程分支, 并清楚已经删除的分支
# 清除被删除的分支
git remote prune origin

# 在获取之前,删除远程上不再存在的任何远程跟踪引用
git fetch -p

# 切换到出现问题的分支
git checkout target_branch

# 关联分支
git branch --set-upstream-to=origin/master <target_branch>

git push --set-upstream origin <local_branch>

Q18mogodb更新失败

-Info

在更新ipdate_one()时出现update error

-Analysis

查看所使用的是否是原生语法, 如果使用http连接更新, 则尝试使用原生语句进行更新

Q17linux连接目标主机报错[ssh]

-Info

问题一

1
通过linux主机,远程其他linux服务器时报错:Unable to negotiate with xx.xxx.xxx.x port 22:no matching cipher found. Their offer: aes128-cbc,aes256-cbc,3des-cbc,des-cbc

问题二

1
远程其他linux服务器时又报错:Unable to negotiate with xx.xx.xx.xx port 22: no matching key exchange method.found.Their.offer:diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1

-Analysis

问题一

1
由于发起远程访问端的主机openssh版本过高(我的这台主机为最新8.1版本),导致不支持对低版本的远程连接主机的ssh解密,故报错。

问题二

1
根据提示,ssh不能远程登录的原因为:没有找到相关的主机密钥类型。

-Soultion

问题一

1
2
3
# 修改发起远程访问的本机SSH客户端配置(/etc/ssh/ssh_config)文件,将默认注释的:
Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc
# 使用即可。

问题二

1
2
# 修改发起远程访问的本机SSH客户端配置(/etc/ssh/ssh_config)文件,在最后加上一行:
KexAlgorithms +diffie-hellman-group1-sha1

Q16Docker连接redis失败

-Info

在docker-container中发现container重启

docker logs -f --tail 100 container_id<container name>

发现是连接redis报错

-Analysis

分析发现是docker连接不到redis

-Solution

1
2
3
4
5
# 查看docker状态
services docker status

# 重启docker
systemctl restart docker

Q15Docker运行脚本

-Info

在需要跑数据时, 可以 [根据场景选择] :

  1. 写script
  2. 写临时API

-Solution

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
35
36
37
38
39
40
41
42
43
44
import asyncio

from config.config import Table
from model.base_model import BaseModel

phone = ['16620132242', '16676748179']


async def export_excel():
friend_count = []
for sim in phone:
print(f'---------sim:{sim}')
bot_username = await BaseModel.fetch_one(
Table.BotInfo,
"*",
BaseModel.where_dict({"sim": sim})
)

if not bot_username:
friend_count.append(0)
continue

friend_count_info = await BaseModel.count_(
Table.Friend,
BaseModel.where_dict({"bot_username": bot_username.username, "is_friend": 1})
)

friend_count.append(friend_count_info)
return friend_count


async def main():
task = asyncio.Task(export_excel())
done, pending = await asyncio.wait({task})
if task in done:
print(task)


if __name__ == "__main__":
# 需要初始化BaseModel的model_dict()
BaseModel.init_model_dict()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

-Review

使用到的命令:

1
2
3
4
5
6
7
8
# copy file from linux-server to another linux-server
scp source_file root@remote_ip:/target_directory/

# create file
vim target_file.py

# copy file from linux-server to linux-server-container
docker cp source_file container_id[name]:/target_directory/

需要注意的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 需要初始化BaseModel的model_dict()
BaseModel.init_model_dict()

# 导出excel
from openpyxl import Workbook


people_count_list = [4190, 1415]
book = Workbook()
sheet = book.active

for index, value in enumerate(people_count_list):
sheet[f"A{index+1}"] = value
print(value)

book.save("/Users/edy/Desktop/sample.xlsx")

Q14导出大数据Excel

-Info

在导出群-机器人数据时, 涉及到群数据量很大导出excel失败

-Solution

使用page pagesize进行分页导出

Q13字典获取key

获取dict中key所对应的vaule, 使用. get [“key”]有什么区别?

Q12unhashed type dict

-Info

1
ERROR: unhashed type dict

-Analysis

字典不能存储在集合

Q11生成器对象不可下标

-Infolocal

1
TypeError: ‘generator’ object is not subscriptable 

-Analysis

通常是list.append(generator)

-Solution

1
2
3
4
5
6
7
8
9
10
a = []
b = []
a.append([for item in range(1, 5)])
b.extend(for item in range(1, 5))

[[1, 2, 3, 4]]
[1, 2, 3, 4]

# 列表生成器
[for item in range(1, 5)]

Q10导入包含同名文件冲突

-Info

1
2
3
4
import resolves to its containing file

file> component
import time

-Analysis

component目录下有time文件,且import 了 time(python) 与python自带的第三方time库冲突

-Solution

Q9Mongodb保存|插入字段失败

  • 1.查看Table定义==>fiedls = fiedls.IntFields() ==>需要加()

Q8虚拟环境配置

  • 拉取项目后如何配置虚拟环境?
  • 如何激活虚拟环境, 并每次进入pycharm的terminal都保证是venv?
  1. venv的目录和interpreter的目录要配置相同

![image-20211018164551110](/Users/edy/Library/Application Support/typora-user-images/image-20211018164551110.png)

![image-20211018164714977](/Users/edy/Library/Application Support/typora-user-images/image-20211018164714977.png)

  1. terminal的配置如下

![image-20211018164922890](/Users/edy/Library/Application Support/typora-user-images/image-20211018164922890.png)

Q7Nginx Timeout 504

-Info

在前端导出excel时, 导出失败并返回GateWay TimeOut 504

-Analysis

  • 重新导出查看log
1
2
url: /channel/get_excel?token=ab5c40fa9da40d74f5a2ba17d84dcf63
GET /channel/get_excel?token=ab5c40fa9da40d74f5a2ba17d84dcf63 (183.81.181.194) 67000.83ms
  • 说明导出正常, 其中导出时间大于60s
  • nginx做反向代理,默认请求是有一个60秒的超时,如果http请求超过了60秒,再返回,连接就会被nginx中断,前端就会得到504的错误:gateway time-out。

-Solution

修改nginx的配置, 将超时设置延长至300s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docker ps 

docker exec -it nginx_container_id bash

whereis nginx.conf

cd /etc/config

vim nginx.conf

{
...
keepalive_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
...
}

使用挂载修复

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 进入服务器
ssh root@xxx

# 编辑volumne
vim docker-compose.yaml

# 添加挂载
nginx-proxy:
image: registry.cn-beijing.aliyuncs.com/doododmain/nginx-proxy:20mbody
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- /certs:/etc/nginx/certs
- /var/run/docker.sock:/tmp/docker.sock:ro
- /root/nginx/nginx.conf:/etx/nginx/nginx.conf

# 在外部创建对应的挂载文件[宿主机]
vim /root/nginx/nginx.conf

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 300;

proxy_connect_timeout 300;

proxy_send_timeout 300;

proxy_read_timeout 300;

send_timeout 300;

#gzip on;

include /etc/nginx/conf.d/*.conf;
}
daemon off;

-Review

  • 如果程序正常, log也没有报错, 很可能是配置的原因

Q6mongodb插入数据报错

-Info

在mongodb中插入数据报错{'code': -1, 'msg': 'exception'}

-Analysis

经过排查不是以下原因:

  • 插入字段类型与定义不符
  • 缺少必要字段

-Solution

在mongodb表中定义字段如下:

1
2
3
4
5
6
7
# 群发任务
class AssistantBatchSend(BaseModel):
__table__ = 'assistant_batch_send'
client_id = fields.IntField() # 创建人
task_id = fields.IntField() # 任务标识
message_list = fields.JsonField() # 发送内容
bot_username_list = fields.JsonField() # 机器人id

在插入的数据中

1
2
3
task_identification = int(str(self.user_info.client_id) + str(int(time.time()*1000000))) # 任务标识
immediate_info = CM(Table.AssistantBatchSend)
immediate_info.task_id = task_identification

其中task_id定义为intfiled长度最大

为:

![image-20211011134300296](/Users/edy/Library/Application Support/typora-user-images/image-20211011134300296.png)

但是传入的长度为22, 应该把此字段定义为StringField

Q5SSH连接VCM断开

-Question

报错信息

info1

client_loop: send disconnect: Broken pipe

info2

yum-config-manager client_loop: send disconnect: Broken pipe

-Analysis

info1

终端连接服务器无响应且没用自动间隔连接配置VCM的问题

-Solution

info1

修改/etc/ssh/sshd_config文件, 修改ClientAliveInterval 120

1
2
3
4
5
6
# 通过每 300 秒(每 5 分钟)进行响应确认来保持连接。 我认为这个部分的数字可以是60或120。
>/etc/ssh/sshd_config
#PermitUserEnvironment no
#Compression delayed
ClientAliveInterval 60
#ClientAliveCountMax 3

重新启动ssh服务

1
systemctl restart sshd

或者在客户端进行更改/etc/ssh/ssh_config

1
2
3
# 添加参数
TCPKeepAlive yes
ServerAliveInterval 60

info2

配置/etc/ssh/ssh_config文件,增加以下内容即可:

1
2
3
4
5
6
7
Host *
IPQoS=throughput
# 断开时重试连接的次数
ServerAliveCountMax 5

# 每隔5秒自动发送一个空的请求以保持连接
ServerAliveInterval 5

重新启动ssh服务

1
systemctl restart sshd

-Review

1.在服务端进行修改

在服务器端, 可以让服务器发送“心跳”信号测试提醒客户端进行保持连接

通过修改 sshd 的配置文件,能够让 SSH Server 发送“心跳”信号来维持持续连接,下面是设置的内容

打开服务器 /etc/ssh/sshd_config,我在最后增加一行

1
2
ClientAliveInterval 60
ClientAliveCountMax 1

这样,SSH Server 每 60 秒就会自动发送一个信号给 Client,而等待 Client 回应,(注意:是服务器发心跳信号,不是客户端,这个有别于一些 FTP Client 发送的 KeepAlives 信号),如果客户端没有回应,会记录下来直到记录数超过 ClientAliveCountMax 的值时,才会断开连接。

2.在客户端进行修改

只要在/etc/ssh/ssh_config文件里加两个参数

1
2
TCPKeepAlive yes
ServerAliveInterval 60

前一个参数是说要保持连接,后一个参数表示每过1分钟发一个数据包到服务器表示“我还活着”

如果你没有root权限,修改或者创建~/.ssh/ssh_config也是可以的

Q4vim安装失败

-Question

  1. 在使用vim时发现报错bash: vim: command not found

-Analysis

使用cat /proc/version查看系统版本

![image-20210928202753365](/Users/edy/Library/Application Support/typora-user-images/image-20210928202753365.png)

查看系统镜像中是否有vim, 发现red hat 5才有vim

-Solution

  • 使用apt install vim 安装vim
  • 发现Unable to locate package vim

![image-20210928203059115](/Users/edy/Library/Application Support/typora-user-images/image-20210928203059115.png)

  • 需要输入:apt-get update(作用是:同步 /etc/apt/sources.list 和 /etc/apt/sources.list.d 中列出的源的索引,这样才能获取到最新的软件包)
  • 之后再输入apt-get install vim安装(非root用户登录,root apt-get intall vim

-Review

docker常用命令

1
2
3
4
5
6
7
8
9
10
11
# 修改了image内的文件后重启image
docker restart image_id

# 启动container中的所有image
docker start $(docker ps -a | awk '{ print $1}' | tail -n +2)

# 停止container中的所有image
docker stop $(docker ps -a | awk '{ print $1}' | tail -n +2)

# 删除container中的所有image
docker rm $(docker ps -a | awk '{ print $1}' | tail -n +2)

Q3镜像部署失败

-Question

  1. 部署对应的tag的时候出现error

![image-20210928155639667](/Users/edy/Library/Application Support/typora-user-images/image-20210928155639667.png)

  1. 有正在运行的container, 无法部署

-Analysis

  • 在部署的过程中存在正在运行的container
  • 需要将所有的正在运行的container停下来或者删除所有正在运行的container中的image

-Solution

1
2
3
4
5
6
7
8
9
10
11
# 启动所有image
docker start $(docker ps -a | awk '{ print $1}' | tail -n +2)

# 停止运行所有image
docker stop $(docker ps -a | awk '{ print $1}' | tail -n +2)

# 删除container中的所有image
docker rm $(docker ps -a | awk '{ print $1}' | tail -n +2)

# 重新部署
sh deploy-debug.sh 1.0.0.65

-Review

  • 使用docker ps -acontainer中的所有image包括正在运行和未运行的
  • docker rmi -f 'docker iamges' 删除的已经存在但未运行的image
  • docker rm image_id删除正在运行的image

Q2镜像重启

-Question

  1. Docker ps 查看镜像状态显示restart

![image-20210928095953991](/Users/edy/Library/Application Support/typora-user-images/image-20210928095953991.png)

  1. RabbitMQ只有生产者没有消费者

-Analysis

  1. 部分服务器没起来

-Solution

  1. docker logs -f image_name 查看对应的image日志
  2. 找到报错信息

![image-20210928095931103](/Users/edy/Library/Application Support/typora-user-images/image-20210928095931103.png)

cerely can't find module name 'component.task.member_change_task.component'

cerely配置错误

-Review

  • docker logs -f
  • command r 搜索已经使用的order

Q1磁盘溢出

-Question

  1. mongodb连接失败报错
    • failed to read 4 bytes from socket within 300000 milliseconds
  2. 紫豆超管后台登陆失败
    • login 无响应
  3. 机器人正常但不可用
    • 机器人上报heartbeat给服务器但服务器不callback

-Analysis

  1. 登陆紫豆服务器ssh root@ip
    • 报错显示disk overflower
  2. df -h查看disk存储情况

image-20210926170428181

-Solution

  1. 删除不用的container|容器修剪

    • PRO环境具有一定的风险
    • docker system prune docker container prune
    1
    2
    3
    4
    5
    6
    7
    8
    docker container prune
    WARNING! This will remove all stopped containers.
    Are you sure you want to continue? [y/N] y
    Deleted Containers:
    4a7f7eebae0f63178aff7eb0aa39cd3f0627a203ab2df258c1a00b456cf20063
    f98f9c2aa1eaf727e4ec9c0283bc7d4aa4762fbdba7f26191f26c97f64090360

    Total reclaimed space: 212 B
  2. 删除镜像[优先选择]

    • docker rmi -f `docker images`
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - docker image需`

      - **删除已经存在的镜像「不包括正在运行的」**

      ### -Review

      **docker rmi :** 删除本地一个或多个镜像。

      ```docker
      docker rmi [OPTIONS] IMAGE [IMAGE...]

OPTIONS说明:

  • **-f :**强制删除;
  • **–no-prune :**不移除该镜像的过程镜像,默认移除;

docker prune 命令

prune 命令用来删除不再使用的 docker 对象。

删除所有未被 tag 标记和未被容器使用的镜像:

1
2
3
$ docker image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y

删除所有未被容器使用的镜像:

1
$ docker image prune -a

删除所有停止运行的容器:

1
$ docker container prune

删除所有未被挂载的卷:

1
$ docker volume prune

删除所有网络:

1
$ docker network prune

删除 docker 所有资源:

1
$ docker system prune

docker system prune 命令:

1
2
3
4
5
This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- all dangling build cache

删除停止的容器、删除所有未被容器使用的网络、删除所有none的镜像。

My-Project

服务器

详细对比

preview

轻量应用服务器

单机类应用

  • 个人网站
  • 博客
  • 云端测试等轻量级应用
    • 技术小白
    • 不想搭建云服务器环境
    • 没有云平台内网需求
    • 本身自带运行环境
  • 特点
    • 轻运维、开箱即用

云服务器CVM

专业级应用

  • 集群
  • 高可用应用
  • 特点
    • 高并发网站
    • 视频编解码
    • 大型游戏
    • 复杂分布式集群应用

终端连接CVM

  1. CVM生成hash密钥
1
2
# 在云服务器配置中生成密钥rsa
# 需要注意生成的后只能绑定已经关机的实例
  1. 修改rsa证书权限
1
2
3
4
# 将在CVM生成的pem证书文件移动到指定目录下

# 在次目录下运行
chomd 400 <target_rsa.pem>
  1. item远程链接服务器

配置信息如下

Profiles - order
name -
title job
command command ssh -i order
working directory home directory
  • ssh orderssh [-i login_name] [-p port] [user@]hostname
1
ssh -i ~/.ssh/drink_rsa.pem root@110.40.240.39
  • ssh -i :ssh命令, 指定用户登录
    • [-i identity_file]
    • <必须指定>
  • ~/.ssh/drink_rsa.pem : cvm生成的rsa证书
  • root : cvm主机用户
  • 110.40.240.39 : cvm公网ip地址

ssh操作

ssh命令用于远程登陆上Linux主机

ssh默认port: 22

不指定用户:服务器

ssh 192.168.0.11网络

指定用户:

ssh -l root 192.168.0.11ssh

ssh root@192.168.0.11tcp
若是修改过ssh登陆端口的能够:工具

ssh -p 12333 192.168.0.11spa

ssh -l root -p 12333 216.230.230.114操作系统

ssh -p 12333 root@216.230.230.114

1
2
3
4
5
6
7
8
9
10
ssh [-b bind_address] [-c cipher_spec]
[-D [bind_address:]port] [-E log_file]
[-e escape_char] [-F configfile]
[-I pkcs11] [-i identity_file]
[-J [user@]host[:port]] [-L address]
[-l login_name] [-m mac_spec]
[-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address]
[-S ctl_path] [-W host:port]
[-wlocal_tun[:remote_tun]][user@]hostname [command]

默认配置文件和SSH端口

/etc/ssh/sshd_config:OpenSSH服务器配置文件;

/etc/ssh/ssh_config:OpenSSH客户端配置文件;

~/.ssh/:用户SSH配置目录;

~/.ssh/authorized_keys:用户公钥(RSA或DSA);

/etc/nologin:若是存在这个文件,sshd会拒绝除root用户外的其它用户登陆;

/etc/hosts.allow和/etc/hosts.deny:定义tcp-wrapper执行的访问控制列表;

  1. mac终端连接(需要CVM密码)
  • 打开mac终端
  • 输入chmod 400 <CVM-ssh-的绝对路径>
  • 输入ssh root@CVM.ip
  • 输入CVM密码

  • 打开mac终端
  • 输入chmod 400 <CVM-ssh-的绝对路径>
  • 输入ssh -i ~/.ssh/id_rsa root@CVM.ip
  • (无需密码即可)

  • 打开mac终端
  • 输入chmod 400 <CVM-ssh-的绝对路径>
  • 输入ssh -l root -p 22 CVM.ip
  • (VCM密码)

0.Linux基础

查看系统内核

uname -r

查看系统版本

cat /etc/os-release

用户与主机名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# [root@localhost]
root: 当前登陆的用户
localhost: 当前的主机名

# 修改主机名
1.临时修改
sudo hostname <new-hostname>

2.永久修改 命令修改|配置修改
> sudo hostnamectl set-hostname <newhostname>
> vim手动修改/etc/hostname文件

# 查看当前主机名
hostnamectl

# 修改之后重启生效
reboot

退出|关机|重启linux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
退出:
exit

关机命令:
1、halt 立刻关机
2、poweroff 立刻关机
3、shutdown -h now 立刻关机(root用户使用)
4、shutdown -h 10 10分钟后自动关机 如果是通过shutdown命令设置关机的话,可以用shutdown -c命令取消重启
5、init0 停机或者关机

重启命令:
1、reboot
2、shutdown -r now 立刻重启(root用户使用)
3、shutdown -r 10 过10分钟自动重启(root用户使用)
4、init6 重启

init一共分为7个级别,这7个级别的所代表的含义如下
0:停机或者关机(千万不能将initdefault设置为0)
1:单用户模式,只root用户进行维护
2:多用户模式,不能使用NFS(Net File System)
3:完全多用户模式(标准的运行级别)
4:安全模式
5:图形化(即图形界面)
6:重启(千万不要把initdefault设置为6)

yum命令

yum( Yellow dog Updater, Modified)是一个在 Fedora 和 RedHat 以及 SUSE 中的 Shell 前端软件包管理器。

基于 RPM 包管理,能够从指定的服务器自动下载 RPM 包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软件包,无须繁琐地一次次下载、安装。

SSH登陆

  • 从CVM中获取rsa_pub.pem
  • rsa_pub.pem保存在client的./ssh目录下
  • 从client./ssh中获取rsa_pub
  • rsa_pub中的内容放在CVM的/root/.ssh/authorized_keys
1
2
3
4
5
# client 
ssh root@ip

# ori
ssh -i ~/.ssh/rsa_CVM.pub root@CVM.ip

SSH

/root/.ssh/authorized_keys

远程主机将用户的公钥,保存在此

  • 存放访问的client的pub_rsa

/etc/ssh/sshd_config

/etc/ssh/ssh_config

  • ssh_config是针对客户端的配置文件

  • sshd_config是针对服务器的配置文件

ssh服务配置文件

文件修改后使用:

service sshd restart 保存

1.安装docker

1⃣️卸载旧版本docker

1
2
3
4
5
6
7
8
$  sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

2⃣️安装yum

1
$ sudo yum install -y yum-utils

3⃣️设置镜像仓库

1
2
3
sudo yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

4⃣️更新yum索引和cache

1
yum makecache fast

5⃣️安装docker

1
yum install docker-ce docker-ce-cli container.io

6⃣️启动docker

1
systemctl start docker

7⃣️查看docker配置|版本

1
docker version

8⃣️启动hello world

1
docker run hell-world

9⃣️查看docker的images

1
docker images

阿里云镜像加速

我的学习

阅读书籍

框架思维

  • 数据密集型应用

数据结构

底层源码

目前学习

网络基础原理

source: wait

  • 网络基础原理

Docker基础

source:b站|狂神

  • Docker基本概念
  • Docker基本语法
  • Docker进阶
  • Docker实践操作
  • Docker复盘

Nginx基础

source:b站|狂神

  • nginx基本概念
  • nginx基本语法
  • nginx进阶
  • nginx实践操作
  • nginx复盘

计划学习

  • Nginx
  • 网络基础
  • Celery
  • RabbitMQ
  • Docker
  • Mysql
  • ElasticSearch
  • Hadoop

已经学习

Mongodb

source:b站|黑马

  • mongodb基本概念
  • mongodb基本语法
  • mongodb进阶
  • mongodb实践操作
  • mongodb复盘

Redis

source:b站|狂神

  • redis基本概念
  • redis基本语法
  • redis进阶
  • redis实践操作
  • redis复盘

数据库索引原理

数据库索引原理

推荐阅读

MySQL索引——入门进阶必备

MySQL中B+树索引的应用场景大全


MongoDB索引原理


关系型数据库和非关系型数据库之间的区别

非关系型数据库(NOSQL)和关系型数据库(SQL)区别详解


什么是事务?事务的四大特性是什么?

B+树索引是B+树在数据库中的一种实现,是最常见也是数据库中使用最为频繁的一种索引。B+树中的B代表平衡(balance),而不是二叉(binary),因为B+树是从最早的平衡二叉树演化而来的。在讲B+树之前必须先了解二叉查找树、平衡二叉树(AVLTree)和平衡多路查找树(B-Tree),B+树即由这些树逐步优化而来。

二叉查找树

二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。
如下图所示就是一棵二叉查找树,

索引

对该二叉树的节点进行查找发现深度为1的节点的查找次数为1,深度为2的查找次数为2,深度为n的节点的查找次数为n,因此其平均查找次数为 (1+2+2+3+3+3) / 6 = 2.3次

二叉查找树可以任意地构造,同样是2,3,5,6,7,8这六个数字,也可以按照下图的方式来构造:

索引

但是这棵二叉树的查询效率就低了。因此若想二叉树的查询效率尽可能高,需要这棵二叉树是平衡的,从而引出新的定义——平衡二叉树,或称AVL树。

平衡二叉树(AVL Tree)

平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。下面的两张图片,左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1;

索引

如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。它们的示意图如下:

索引

这四种失去平衡的姿态都有各自的定义:

  • LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子(Left Child)的左孩子(Left Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。

  • RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子(Right Child)的右孩子(Right Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。

  • LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子(Left Child)的右孩子(Right Child)还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。

  • RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子(Right Child)的左孩子(Left Child)还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。

AVL树失去平衡之后,可以通过旋转使其恢复平衡。下面分别介绍四种失去平衡的情况下对应的旋转方法。

LL的旋转。LL失去平衡的情况下,可以通过一次旋转让AVL树恢复平衡。步骤如下:

  1. 将根节点的左孩子作为新根节点。
  2. 将新根节点的右孩子作为原根节点的左孩子。
  3. 将原根节点作为新根节点的右孩子。

LL旋转示意图如下:

索引

RR的旋转:RR失去平衡的情况下,旋转方法与LL旋转对称,步骤如下:

  1. 将根节点的右孩子作为新根节点。
  2. 将新根节点的左孩子作为原根节点的右孩子。
  3. 将原根节点作为新根节点的左孩子。

RR旋转示意图如下:

索引

LR的旋转:LR失去平衡的情况下,需要进行两次旋转,步骤如下:

  1. 围绕根节点的左孩子进行RR旋转。
  2. 围绕根节点进行LL旋转。

LR的旋转示意图如下:

索引

RL的旋转:RL失去平衡的情况下也需要进行两次旋转,旋转方法与LR旋转对称,步骤如下:

  1. 围绕根节点的右孩子进行LL旋转。
  2. 围绕根节点进行RR旋转。

RL的旋转示意图如下:

索引

平衡多路查找树(B-Tree)

B-Tree是为磁盘等外存储设备设计的一种平衡查找树。因此在讲B-Tree之前先了解下磁盘的相关知识。

系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。

InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB,可通过参数innodb_page_size将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小:

mysql> show variables like 'innodb_page_size';

而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB。InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。

B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。

一棵m阶的B-Tree有如下特性:

  1. 每个节点最多有m个孩子。
  2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
  3. 若根节点不是叶子节点,则至少有2个孩子
  4. 所有叶子节点都在同一层,且不包含其它关键字信息
  5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
  6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
  7. ki(i=1,…n)为关键字,且关键字升序排序。
  8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)

B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:

索引

每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。

模拟查找关键字29的过程:

  1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
  2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
  3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
  4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
  5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
  6. 在磁盘块8中的关键字列表中找到关键字29。

分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。

B+Tree

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同:

  1. 非叶子节点只存储键值信息。
  2. 所有叶子节点之间都有一个链指针。
  3. 数据记录都存放在叶子节点中。

将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:

索引

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为10^3^)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在24层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要13次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

Mongodb

Mongodb

推荐阅读

mongodb时间戳

_id

打开 MongoDB 数据库中的任何一个文档,你会注意到文档中有一个 _id 字段,实际上,ObjectId 或 _id 是每个 MongoDB 文档中都存在的字段。

bjectId 的结构

这些是 _id 的一些主要特征的摘要:

  • _id 是集合中文档的主键,用于区分文档(记录)。
  • _id自动编入索引。指定 { _id: } 的查找将 _id 索引作为其指南。
  • 默认情况下,_id 字段的类型为 ObjectID,是 MongoDB 的 BSON 类型之一。如果需要,用户还可以将 _id 覆盖为 ObjectID 以外的其他内容。

ObjectID 长度为 12 字节,由几个 2-4 字节的链组成。每个链代表并指定文档身份的具体内容。以下的值构成了完整的 12 字节组合:

一个字节两个字符

  • 一个 4 字节的值,表示自 Unix 纪元以来的秒数–>8位16进制
  • 一个 3 字节的机器标识符
  • 一个 2 字节的进程 ID
  • 一个 3 字节的计数器,以随机值开始

img

通常,你不必担心要如何生成 ObjectID。如果文档尚未分配 _id 值,MongoDB 将自动生成一个 _id 值。

创建新的 ObjectId

如果你想自己生成新的 ObjectId,可以使用以下代码:

1
newObjectId = ObjectId()

你也可以直接在 Navicat 编辑器中输入它。

这将会生成一个唯一的 _id,例如:

1
ObjectId("5349b4ddd2781d08c09890f3")

或者,你亦可以提供一个 12 字节的 ID:

1
myObjectId = ObjectId("5349b4ddd2781d08c09890f4")

文档的创建时间戳

由于 _id ObjectId 默认存储了 4 字节的时间戳,因此在大多数情况下,你不需要存储任何文档的创建时间。你可以使用 getTimestamp 方法获取文档的创建时间:

1
2
3
ObjectId("5349b4ddd2781d08c09890f4").getTimestamp()
>>
ObjectId("612b264173e5b4a6bda0abdb")

这将以 ISO 日期格式返回此文档的创建时间

1
ISODate("2019-09-12T30:39:17Z")

将 ObjectId 转换为字符串(String)

在某些情况下,你可能需要得到字符串格式的 ObjectId 值。若要转换 ObjectId 为字符串,请使用以下代码:

1
newObjectId.str

上面的代码将返回 Guid 的字符串格式的 ObjectId:

1
5349b4ddd2781d08c09890f3

文档排序

由于每个 ObjectId 都包含一个时间戳,因此你可以用 _id 字段将文档按创建时间排序。但请务必注意,此排序方法并不代表排序是严格或精确的,因为 ID 的其他组件也会影响,导致次序会反映其他变量,而不仅仅是创建时间。

更改 ObjectId

_id 字段基本上是不可变的。在创建文档之后,根据定义,它已被分配了一个无法更改的 _id。话虽如此,在插入新文档时是可以覆盖 _id 的。覆盖文档的 _id 字段可能有其用处,但是当这样做时,你有责任确保每个文档的 _id 值都是唯一的。

English-Study-5

Day7

  • detach |蒂塔迟~| n v 分离 分开 派遣
  • STDIN 标准输入
  • interactive |艾特.rua^棵.忒.夫.| adj web n 交互的 相互配合的
  • isolation |艾涩.雷神.| n adj web 隔离 分离 分析 绝缘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-i, --interactive                    
Keep STDIN open even if not attached

--ip string
IPv4 address (e.g., 172.30.100.104)

--ip6 string
IPv6 address (e.g., 2001:db8::33)

--ipc string
IPC mode to use

--isolation string
Container isolation technology

--kernel-memory bytes
Kernel memory limit

Day6

  • digest |得艾.杰.斯特| n 摘要 汇编 v 消化 理解 领会 web 忍受
  • suffix |撒.费.刻丝-| n 后缀
  • discard|滴.斯.嘎得.| n 抛弃 垫出的牌 v 抛弃 解雇 web 丢弃

Day5

  • dimension |呆.门.身.| n 纬度

Day4

  • stock |丝.大.渴| n 股票 库存 储备 v 库存 放鱼 adj 老一套 常备的 web 存库 紫罗兰
  • purchase |泼.企^鹅.丝| n v 购买 买卖

Day3

  • async |A.醒颗| n 异步
  • commodity |渴.贸得.忒| n 商品 产品 大宗商品
  • distribution |低丝泼.biu.什恩| n 分布 分配 分发 web 配送 发行 配销
  • refund |瑞.放得饿| n v 退款 返还款 偿还金额

Day2

  • scalability |斯.盖楼.biu乐^特A| n 可扩展性
  • cipher |撒.佛沃.| n 密码 密码索引 v 计算 web 暗号 密文 加密算法

Day1

  • fuzzy |佛爱.Z(en)·| adj 模糊的, 有绒毛的, 失帧的
  • essence|艾森.s~| n 本质的 实质 香精 精华 要素
  • snapshot|斯.奈普,少特er| n 简介 快照
  • resource|瑞扫丝.| n 资源
  • quantity|框内A^特A.| n 数量 大量 订购数量
  • retrieve|瑞.吹无^v| n 恢复 v 取回, 挽回 检索