2022年 11月 9日

第二章 Python基础语法的补充拓展

第二章 基础语法的补充拓展

2.1 关于变量的机制

2.1.1 变量的创建和修改

Python创建一个变量时,我们假设是name,赋值为zm,变量的值(zm)就会存储在计算机的内存中,而该变量的变量名(name就会指向变量数据(zm)存放的内存地址,我们在代码中可以通过id(name)来查看变量name的内存地址。

现在我们修改name的值,修改成zhuming。此时,zhuming放到内存的一个新的地方,然后name指向的内存地址改变成zhuming的内存地址。那么就意味着一开始的zm就已经断了联系了,Python解释器有自动垃圾回收机制,在一定时间内就会刷新内存,将没有引用关系的变量值(zm)清除掉,恢复成可用的存储位置。

2.1.2 变量的指向关系

现在,创建变量name并赋值为zm,再创建一个新的变量name1,赋值为变量name,此时的对应关系改怎么解释。

name1赋值为name时,name1name就同时指向name的内存地址(name1name都指向zm的内存地址,此时name1name无任何关系);

我们这时候再修改name的值为zhuming,现在输出namename1,我们可以看到name输出为zhumingname1输出为zm

2.2 身份运算和None

python中有很多数据类型,查看一个数据类型的方法有type()

name = 'zm'
age = 1

print(type(age))	# <class 'int'>
print(type(name))	# <class 'str'>
  • 1
  • 2
  • 3
  • 4
  • 5

2.2.1 身份运算

需要用到isis not运算符,可以理解为是和不是。

name = 'zm'
print(type(name) is str)	# True
print(type(name) is int)	# False
print(type(name) is not int)	# True
print(type(name) is not str)	# False
  • 1
  • 2
  • 3
  • 4
  • 5

2.2.2 None

None表示什么都没有,为空。

在程序中的用处:很多时候创建出来的变量是给用户输入的数据来赋值的,但是我们如果仅仅创建变量,不赋值就会报错。我们就会用None来赋值,表示现在值为空,后面会赋值的意思。

2.3 三元运算

a = 10
b = 5

# 一般代码
if a > 15:
    c = a
else:
    c = b

# 三元运算 表示如果a > 15,d=a;否则d=b
d = a if a > 15 else b
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

三元运算的公式可以看为变量名 = 值1 if 条件A else 值2

2.4 细说数据类型-列表

前面说了列表的基本知识(1.3.4 列表),可以回顾一下。下面就做出一些补充。

2.4.1 列表的增

  1. append(value):追加,需要一个参数,将value插入在末尾
  2. insert(index, value):插入,需要两个参数,将value插入到index的位置,原来此位置以及后面的元素都往后挪1
  3. extend(new_list):合并,需要一个列表参数,将new_list与调用此方法的

2.4.2 列表的删

  1. del:Python自带的删除方法,不需调用。del后面跟删除的元素就好。eg:del names[1]
  2. pop([index]):删除,不加参数就默认删除列表的最后一个元素;加参数就会删除索引为index的元素。如果传进的index值越界就会报错。eg:names.pop() names.pop(3)
  3. clear():清空,不用参数,直接将列表清空,变成空列表(可以print()空列表看看,clear()返回的不是None)。eg:names.clear() # names为[] 即为空列表
  4. remove(value):从左到右删除第一个匹配到的value。删掉不存在的值也会报错。

学过下面列表的查方法之一的index()之后,我们可以搭配起来实现,通过元素值来匹配删除指定元素。

names = ['zm', 'wzw', 'yz', 'zk']
names.pop(names.index('wzw'))		# 删除列表中的wzw
  • 1
  • 2

2.4.3 列表的改

和前面一样,还是以索引的方式来修改值。eg:names[0] = 'zm'

2.4.4 列表的查

  1. index(value):从左到右查看第一个value的下标,若列表中无value的值,会报错的。eg:names.index('zm')
  2. count(value):统计value数量,返回的是int型数据。

2.4.5 切片

切片的正反

names = ['zm', 'wzw', 'yz', 'zk']
print(names[1:2])	# 输出['wzw'],顾头不顾尾(包括1不包括2)
print(names[2:])	# 输出['yz', 'zk']
print(names[:2])	# 输出['zm', 'wzw']
print(names[-2:])	# 输出['yz', 'zk']
print(names[:-3])	# 输出['zm']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

关于上面的负数,是切片的逆序切法,但是切片还是从左到右(没变),只是索引变成从右往左,所以还是顾头不顾尾。这个东西…多试试就会懂了,上升至理论层次可能会比较抽象,难以理解。eg:第五行代码切片包括-2的元素,第六行代码切片不包括-3的元素。

步长

切片完整的格式是[begin: end: ​step],这里的step就是步长。

关于步长,默认是1(eg:[begin: end]),可以来试试修改步长。eg:names[::2] # 列表全部元素隔一个输出一个

切片也可以啥都不写,比如names[::]name[:],他们都等价于names,就是啥都没切,原样输出了。

有趣的是,步长可以为负值,这时候整个列表就会反转输出。

names = ['zm', 'wzw', 'yz', 'zk']
print(names[::])	# ['zm', 'wzw', 'yz', 'zk']
print(names[::-1])	# ['zk', 'yz', 'wzw', 'zm']
  • 1
  • 2
  • 3

那步长为负时,beginend参数也是负值的话,那是不是也符合顾头不顾尾呢?有兴趣可以自己试试,这个点不是很重要。

2.4.6 列表的循环与排序

  • 排序:sort(),根据字符的十进制码进行排序。大体的优先级可以模糊理解为:字符<英文<汉字,可能会有一些特殊的个例。
  • 循环:通常我们使用循环来遍历打印列表的元素(大多使用for循环,while循环看个人,习惯while的话就可以使用while)。
names = [1, 2, 3, 4, 5]

for i in names:
    print(i)
  • 1
  • 2
  • 3
  • 4

2.4.7 嵌套

嵌套适用于列表,也包括后面的元组、字典,除了字符串,其他的基本数据类型都支持嵌套语句。所谓嵌套,就是在一个数据类型里面放入一个数据类型,不限于单一的数据类型。

names = ['zm', ['zm', 'wzw', 'yz'], {'411': 'zm', '409': 'yz'}]
print(names)
  • 1
  • 2

2.4.8 enumerate()的使用

有时我们既需要取出列表的值,又需要列表的索引,我们就可以使用enumerate()

# -*- encoding: utf-8 -*-
lis = ['b', 'a']
for i, k in enumerate(lis):
    print(i, k)
  • 1
  • 2
  • 3
  • 4

不过,enumerate()可以使用index()来代替,只是麻烦一点而已,尽量记住enumerate()

2.5 细说数据类型-元组

取值和创建都与列表类似,只是把[]改成了()。与列表不同的是,元组创建后,只能查,不能增删改,可以看做只读列表。元组只有count()index()两种方法,方法的功能也与前面列表的一样。

names = ('zm', 'wzw')
print(type(names))	# <class 'tuple'>
  • 1
  • 2

元组的循环遍历、包含(if ... in tuple:)等运算都和列表一样。

不过,元组中的元素设置为可变的数据类型,就可以改变元组中的元素。

names = ('yz', ['zm', 'wzw'])
print(names)	# ('yz', ['zm', 'wzw'])
names[1][0] = 'zhuming'
print(names)	# ('yz', ['zhuming', 'wzw'])
  • 1
  • 2
  • 3
  • 4

比如我们元组中嵌套列表,我们可以修改列表里面的元素。元组只是存每个元素的内存地址,而作为元组的元素的列表里面的元素存在内存里另外的空间,所以是可变的。

2.6 细说数据类型-字符串

2.6.1 切片

我们也可以对字符串使用切片的所有功能,或者说我们可以把字符串当做一个列表,每一个字符是列表的每个元素。

name = 'zhuming'
print(name[::-1])
print(name[::2])
print(name[::2])
  • 1
  • 2
  • 3
  • 4

但是,值得注意的是,字符串不可被修改,修改的话都会创建一个新值,我们可以通过查看修改前和修改后的内存地址来观察。

name = 'zm'
print(id(name))
name = 'wzw'
print(id(name))		# 与第一次输出的地址不一样
  • 1
  • 2
  • 3
  • 4

2.6.3 转义字符

Python中有许多有代表特殊含义的字符,被称为转义字符。比如常见的有\n\t等,分别代表了回车、缩进等特殊含义。

如果不想让转义字符生效,就可以在前面加一个\,或者在字符串前加一个r,表示原生,不触发转义字符。

print('zm\nwzw\tyz')
# 输出:
# zm
# wzw    yz

print('zm\\nwzw\\tyz')	# 输出:zm\nwzw\tyz
print(r'zm\nwzw\tyz')	# 输出:zm\nwzw\tyz
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.6.4 字符串的常用操作

  • capitalize():大写字符串的首字母。eg:name = 'jack chen' name.capitalize()就是'Jack chen',注意而不是'Jack Chen'

  • casefold():大写全部变小写,与lower()函数的区别在于,casefold()适用于大多数字符。eg:name = 'Jack Chen' name.casefold()就是'jack chen'

  • center(width, fillchar):定义宽度,然后字符中间显示,两侧填充fillchar的字符。

    name = 'System Of Information'
    print(name.center(50, '#'))
    # 输出:
    # ##############System Of Information###############
    
    • 1
    • 2
    • 3
    • 4
  • count(char, start, end):计数字符串内出现char字符的次数,后面两个参数可填可不填,表示切片的开始和结束,在切出来的一段字符串内对char进行计数。

    the_string = 'ssaadd sad'
    print(the_string.count('s'))	# 3
    print(the_string.count('a', 0, 3))	# 1(顾头不顾尾)
    
    • 1
    • 2
    • 3
  • endswith(str):判断字符串是以什么结尾。

    email = '2511343050@qq.com'
    if email.endswith('@qq.com'):
        print('邮箱格式正确')
    elif email.endswith('@163.com'):
        print('邮箱格式正确')
    else:
        print('邮箱格式错误')
    # 输出:
    # 邮箱格式正确
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • find(char, start, end):这里的三个参数和count的参数一样的含义,找到了就返回char的下标。注意的是find只能找从左到右第一个的char

    name = 'qwq owo xwx'
    
    
    • 1
    • 2
  • format():格式化输出。

    title = ' the system '
    choice_1 = ' [1] select '
    choice_0 = '  [0] exit  '
    # %
    welcome = """
    ----%s----
    ----%s----
    ----%s----
    ----%s----
    """ % (title, choice_1, choice_0, title)
    print(welcome)
    
    # format 1
    welcome = """
    ----{0}----
    ----{1}----
    ----{2}----
    ----{0}----
    """.format(title, choice_1, choice_0)
    print(welcome)
    
    # format 2
    welcome = f"""
    ----{title}----
    ----{choice_1}----
    ----{choice_0}----
    ----{title}----
    """
    print(welcome)
    
    • 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
  • index(char, start, end):和count以及find类似的功能,只是返回的是下标

  • isdigit():判断字符串是否是一个整数(可以理解为int()强制转换为整型),返回TrueFalse

  • islower():判断字符串是否全部是小写。

  • isspace():判断字符串是否有空格,多个空格也可以。

  • isupper():判断字符串是否全部是大写。

  • join(iterable):字符串的拼接,必须全是字符串,不能有其他任何类型。

    names = ['zm', 'wzw', 'yz']
    print(''.join(names))	# zmwzwyz
    print('|'.join(names))	# zm|wzw|yz
    print(', '.join(names))	# zm, wzw, yz
    
    • 1
    • 2
    • 3
    • 4
  • ljust(width, fillchar):与center()类似,从左开始数

  • lower():大写变小写,只支持ASCI Ⅱ编码。

  • strip(chars):将字符串两边的其他字符清除,不写具体chars的值就默认包括空格和转义字符这些。该函数还衍生出另外两个函数,分别是lstrip()rstrip()lstrip()功能是将左边的其他字符清除,rstrip()功能是将左边的其他字符清除。

  • replace(old, new, count=None):将原字符串中的old片段换成newcount表示换多少个(顺序为从左到右),默认是全部old都换。

    score = 'My score is 580!580!580!'
    print(score.replace("580", "630"))	# 修改成My score is 630!630!630!
    print(score.replace("580", "630", 1))	# 修改成My score is 630!580!580!
    print(score.replace("580", "630", 2))	# 修改成My score is 630!630!580!
    
    • 1
    • 2
    • 3
    • 4
  • split(sep, maxsplit=-1):字符串通过sep分割成列表的元素,以列表的形式返回。如果什么参数都不写,字符串直接变成只有一个元素的列表。maxsplit设置的是从左到右分割几次,默认无数次。该函数还衍生出一个函数,是rsplit(),其功能就是把从左往右变成从右往左。

    n = ['zm', 'yz', 'wzw']
    name = ",".join(n)	# 'zm,yz,wzw'
    names = name.split(',')	# ['zm', 'yz', 'wzw']
    
    • 1
    • 2
    • 3
  • startswith(prefix, start=None, end=None):和endswith()一样,一个判断字符串以什么开头,一个以什么结尾。startendendswith()的参数功能一模一样。

  • swapcase():大小写交换,小写变大写,大写变小写,不过特殊字符不会变。

  • upper():字符串统一变成大写。

  • zfill(width):规定长度,不够长度用0来补,一般不常用,网络编程时可能会用到。

总结:

  • find()index()count()

  • replace()upper()lower()swapcase()casefold()strip()split()

  • 格式化format()ljust()rjust()join()

  • 判断isdigit()startswith()endswith()

2.7 细说数据类型-字典

2.7.1 字典的优点及创建

方便嵌套操作海量数据,而且字典是以键值对形式存储,可以通过查找键来获得值,而字典的键可以是整型、字符串、元组等不可变数据类型(且每个键要唯一存在),而不是像列表那样只有单一的下标查询。

字典的创建:

# {key1: value1, key2: value2, ...}
new_dict = {
	"name": 'zm',
    "password": '123',
}
print(new_dict)
print(new_dict['name'])	# 取键name对应的值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

字典没有顺序(无序),查询速度不受字典大小的影响(HASH),非常快。字典的键不可变、必须唯一,键对应的值可以有多个,可以被修改、可以不唯一。

几种创建方法

# 1
person = {"name": "zm", "age": 20}	# {'name': 'zm', 'age': 20}
# 2
person = dict(name='zm', age=20)	# {'name': 'zm', 'age': 20}
# 3
{}.fromkeys([1, 2, 3, 4, 5, 6, 7, 8], 100)	# {1: 100, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.7.2 字典的增删改查

字典的增

new_dict = {
	"name": 'zm',
    "password": '123',
}
# 增 1 (如果job已经存在就变成了修改job的值)
new_dict['job'] = 'teacher'
# 增 2 (如果address已经存在,setdefault就只会返回原address的值,不进行任何修改或添加操作)
new_dict.setdefault('address', '地球')
print(new_dict)		# 多了job和address两个键值对
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

字典的删

new_dict = {
	"name": 'zm',
    "password": '123',
}
new_dict.pop('name')	# 删除键为name的键值对
new_dict.popitem()	# 随机删除一个键值对
del new_dict['name']	# 删除键为name的键值对
new_dict.clear()	# 清空字典
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

del在删除数据类型的所有方法里,尽量不要用…

字典的改

dic = {1: 1, 2: 2, 3: 3}
dic2 = {1: 2, 2: 4, 4: 8}
# 修改3的键值对
dic[3] = 5	# {1: 1, 2: 2, 3: 5}
# 将dic2的键值对加到dic里面,如果键有重复的情况,dic2中的键值对会覆盖dic的原键值对
dic.update(dic2)	# {1: 1, 2: 4, 4: 8, 3: 5}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

字典的查

  1. dic['key']:返回字典key对应的值,不存在则会报错;
  2. dic.get(key, default=None):如果有就返回key对应的value,找不到就返回default的值;
  3. 'key' in dic:存在返回True,不存在返回False
  4. dic.keys():返回包括dic的所有键的列表;
  5. dic.values():返回包括dic的所有值的列表;
  6. dic.items():返回包括dic所有键值对(元组的形式:(key, value))的列表。

循环

for i in dic:
    print(i)	# 打印的只是key

# 推荐方法
for i in dic:
    print(i, dic[i])

# 以下效率比较低(用到keys(),items(),values()的都比较慢)
for i in dic.items():
    print(i)
    
for k, v in dic.items():
    print(k, v)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.8 细说数据类型-集合

2.8.1 集合的作用

和列表一样,存一堆数据,里面的元素不可变,不能存列表、字典等可变数据类型放在集合内,只能存储元组、数字、字符串等不可变数据;集合天生去重,在集合不存在重复元素;集合无序,不能通过索引来查找值。

集合最重要的两个功能:去重关系运算(与、并、异或等)

2.8.2 集合的增删改查

# 去重
names = ['zm', 'wzw', 'yz', 'zk', 'zm', 'zk']
print(set(names))	# {'wzw', 'zm', 'zk', 'yz'}
print(type(set(names)))	# <class 'set'>

# 增
set_name = set(names)
set_name.add('py')	# {'zm', 'zk', 'yz', 'wzw', 'py'}

# 删
set_name.discard('py')	# 删除py,{'zm', 'zk', 'yz', 'wzw'}
set_name.discard('p2y')	# 删除不存在的值时,就不进行任何操作

set_name.pop()	# 随机删除并返回删除后的集合

set_name.remove('zm')	# 删除zm
set_name.remove('zm2')	# 删除不存在的值时,就会报错

# 查
# 只能in运算
if 'wzw' in set_name:
    print(True)

# 改
# 不能修改!
# 切片也是不能用的
  • 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

2.8.3 集合的关系运算

names_181 = ['zm', 'wzw', 'yz', 'zk', 'kyj']
names_182 = ['zm', 'yz', 'py', 'zzw', 'ghd']

print(names_181 & names_182)	# 交集
print(names_181 | names_182)	# 并集
print(names_181 - names_182)	# 差集,only in names_181
print(names_182 - names_181)	# 差集,only in names_182
print(names_181 ^ names_182)	# 对称差集,等于 两个集合的并集 - 两个集合的交集
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

集合的关系一般只有三个:包含、相交、不相交。Python中分别用下面的方法判断:

  • names_181.isdisjoint(names_182):判断两个集合是否相交,返回True or False
  • names_181.issubset(names_182):判断name_181是否被包含于name_182中,返回True or False
  • names_181.issuperset(names_182):判断name_181是否包含name_182,返回True or False

集合还有其他的方法,这些就不一一赘述了,用到就百度一下就好了。

2.9 关于二进制和十六进制

2.9.1 二进制

理解数学的十进制到二进制的转换就好了,二进制逢2进一

计算机为什么用二进制?是因为计算机的晶体管只能有高电平和低电平两个状态,先人把低电平和高电平用0和1表示,通过这种方法,用二进制来抽象表示人类生活中的种种信息数据。

Python中将十进制转化成二进制的方法是bin(),十进制的值只能从0-255,更高的表示方法需要更深学习bin()方法的具体表示,我们就可以不用看了,了解二进制的转化即可。

2.9.2 十六进制

10进制用0-9表示所有数字,2进制用0, 1表示所有数字,而16进制用0-9以及A-F,15个符号表示所有数字,逢16进一。A-F分别代表10-15

16进制只是一种表示方法,计算机的底层还是使用2进制的。计算机中16进制多用来表示HTML/CSS的颜色表的色号(有RGB等,16进制也是一种常用表示方法)、mac地址、字符编码等都用16进制来表示。

为了16进制不与其他进制不混淆,16进制一般开头为0x,后面的字符才是可以转化的东西,比如hex(16)输出是0x10,数学上的计算方法就是:0* 16*0+1* 16*1=16,数学方法就不再多介绍了,可以自行百度了解,自己演算的时候很常用。

Python中将十进制转换成十六进制的方法是hex(),可以自己多试试。

2.10 字符编码

2.10.1 文字的显示原理

为了表示字符,我们必须实现zhuming -> 十进制,使他们关联起来。

将一个一个字符与十进制数字对应起来,最常见的有ASCII码(基于拉丁字母和特殊字符的一套字符编码表)。然而ASCII码一开始只有127个,后来又补充了一些(128后面的字符对应关系称作扩展ASCII码),但是没有中文。Python中可以通过ord()来查看字符对应的十进制数。

但是每个字符转化的二进制长度都不一样,二进制怎么断句呢?因为ASCI Ⅱ码小于255,所以就规定每8位(bit)代表一个字节(B,又称bytes)。1024B=1KB1024KB=1MB1024MB=1GB1024GB=1TB -> PB EB ZB等等。

因为ASCII表没有中文,我们就自己研发了一张表,又因为汉字一个bit存不下,我们就用了两个bit来存储。后来研发成功,名为GB2312,总共可有存储6w个汉字,当时只有6763个汉字(日常生活中常用的)

随着电脑在中国的普及,越来越多的字需要被计算机表示,后面升级成了GBK编码,表示了2w多个字符(包括汉字、少数民族语言、日语、韩语等),21世纪又进行了升级,表示了2w8k多个字符。

随着发展,问题又来了,人们生活中不光单纯使用中文,英文使用的频率和中文的频率差不多,如果用GBK来表示纯英文的文档比用ASCII一倍的内存。那么如何兼容呢?

因为ASCII高位的字符(大于127的特殊字符)比较少用,那么两个连续的高位字符就更少出现了。我们就增加一个判断,如果有两个连续的高位字符,我们就使用GBK编码,其他情况就用ASCII编码,较好的解决了兼容性问题。

2.10.2 编码种类

每个国家可能都会有自己的编码,比如美国是ASCII,中国是GBK,日本是shift_JIS等等等等,每个的字符对应关系都不一样,那不同国家的软件自己拿来就会产生乱码。

最后,出现了Unicode编码,2-4字节,俗称万国码,支持所有的语言(13w6k多个),可以与各种语言进行转换(将已经出来的软件不用推倒重做)

但是Unicode用了2-4个字节,比ASCII的一个字节多了很多,在当时的传输和硬件存储水平下,很不适应。

最后针对传输和存储,基于Unicode做出了一个新编码,叫UTF-8(使用1、2、3、4字节表示所有字符,优先使用少的字节)。

下面两个编码因为没有解决传输和存储的问题,所以不常使用,只做解。

UTF-16:使用2、4字节表示所有字符

UTF-32:使用4个字节表示所有字符

这里也简单提一下python2和python3的区别。

python2用的是ASCII码,python3用的是utf-8,所以python2不能显示中文,所以python2在程序的开头一般都要手动声明# -*- encoding: utf-8 -*-(官网推荐写法),必须在代码的顶头(第一行)写。

我们python3呢建议也写上,防止python3偶尔因为编码文件导致自己的项目代码运行报错,最后画个好几天解决就很伤了。

# -*- encoding: utf-8 -*-

2.11 字典密码——HASH

2.11.1 HASH

HASH(又称哈希)是把任意长度的输入(又叫做预映射)通过散列算法变换成固定长度的输出,该输出值就是散列值。这种转换是一种压缩映射,这样我们可以通过更小的值来表示很大的值所表示的东西。

HASH是一种算法,很复杂的运算,它的输入可以是字符创或者数字或者任何文件,经过哈希算法处理后,变成一个固定长度的输出,该输出就是哈希值。哈希算法有一个很大的特点,就是不能从结果推算出输出是什么,所以成为不可逆的算法

哈希多应用于密码学以及区块链等方面。

Python中有hash()的方法来将字符串或数字变成哈希值,每次重启Python之后,同样的字符或数字hash()的哈希值都不是一致的。注意,hash()的参数只能hash不可变的值。

HASH的特性

  1. 刚才也说了,不可逆是HASH的一个重要特性。生活中也有许多不可逆的情况,比如煮熟的鸡蛋变不回生鸡蛋。
  2. 计算极快。20G高清电影和5K的文件

HASH的用途

  1. 密码。常用的加密算法是md5md5就是基于HASH做的,使你的密码经过基于HASH的算法处理,使你的密码与一个哈希值一一对应,然后存放进数据库中。如果想登陆,就只能知道原来的密码是什么,然后输入,进行算法的处理,将生成的值与数据库内存储的哈希值比较,如果一致才可以登陆成功。
  2. 文件完整性校验。常用的也是md5算法。一个文件对应一个哈希值,在传输过程中可能会遇到拦截等恶意攻击,当接收文件之后,再进行md5处理,如果两次得出的哈希值一致,那就说明文件完整,没有残缺或植入了木马等恶意程序。
  3. 数字签名。数字签名是为了防止假情报,效果类似于对暗号,A说天王盖地虎,B回应宝塔镇河妖,那A和B就确定了是自己人,才可以进行通信。数字签名就是在互联网上的暗号应用。A说的天王盖地虎在互联网上称为私钥,将发送的原文进行hash,生成一段hash值(就是摘要信息),然后把原文和摘要信息一同发给B。他回应的宝塔镇河妖被称为公钥,将你的摘要信息解密,得到hash值。B再对原文件进行hash,也得到一个hash值,将这个hash与a发来的hash值进行比较,如果一致就说明这个原文是A本人发送的,而不是别人发送的。
  4. 区块链。电子钱包,加密钱包等应用。
  5. 还有很多其他的应用就不一一介绍了。

2.11.2 应用HASH的数据结构

Python中有两个数据类型是基于hash来做的,一个是dict,一个是set

字典快的原因

dict有三个显著特点:

  • key唯一
  • key不可变
  • 查询速度极快,且不受dict大小的影响的特点。

第三个特性就是hash带来的。下面就进行简单的原理解释(真实的原理不是这样,只是做类似的解说)。

dict的每个key都会经过hash生成一段固定长度的hash值,然后这些hash值经过大小排序放入一个列表A中(hash值都是一串数字),需要查询哪个字典的值就通过二分法查找列表A,所以只需要31次查询就可以找20亿的数据,计算机查询一次也仅仅只需要几毫秒。

不过,字典的原理还不单单只有这些,这些只是简单地类似原理,真实的字典原理比这还要复杂。

集合为何去重

集合每add进一个值,他都会经过hash运算出一个hash值,然后存放在一个列表A中,如果后面add的数据的hash值在列表A中,那么这个数据就不会再存进集合里了。

2.11.3 判断数据结构是否可变

将这个数据进行hash如果可以哈希就是不可变的数据,反之则是可变的数据类型。

2.12 文件操作

2.12.1 基础操作

日常文件操作的步骤:

  1. 找到文件,打开
  2. 读、修改
  3. 保存、关闭

Python中的文件操作:

  1. open(filename):根据路径和文件名打开文件
  2. f.read()/f.write():读写操作
  3. f.close():文件关闭(默认保存)

需要注意的是文件操作的模式只能以一种方式(后面再介绍复杂的多种方式混合的文件操作模式)。python有rwa三种基本方法,分别代表只读模式写入模式追加模式三种基本方式。(注意,如果w的写入模式下会覆盖原文件的旧数据,想在原文件的旧数据后面写新数据的话就要用到a的追加模式才可以)

因为是文件的操作,所以存在路径、文件名等差异,每个人都不一样。我们这里就统一规定一下。

以下文件操作的代码都是对名为test.txt,路径是所在目录是当前python文件下的路径的文件进行的操作。

# -*- encoding: utf-8 -*-

# 文件的读
f = open(file='test.txt', mode='r')
# 读一行
print(f.readline())
# 读剩下的数据
print(f.read())
f.close()

# 不需要写close()的with语句
with open(file='test.txt', mode='r') as f:
	# 读一行
    print(f.readline())
    # 读剩下的数据
    print(f.read())
print('with代码块执行完之后,with会关闭所有代码块中用到的功能进程,其中就包括f.close()')

# 文件的追加写入
f = open(file='test.txt', mode='a')
# 读一行
f.write('\nzk\t24\t18845516854')
f.close()


# 文件的覆盖写入
f = open(file='test.txt', mode='w')
# 读一行
f.write('zk\t24\t18845516854')
f.close()
  • 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

循环输出

# -*- encoding: utf-8 -*-
f = open(file='test.txt', mode='r')

print('循环输出:')
for line in f:
    print(line.strip())     # strip是去掉每一行末尾的\n
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
# -*- encoding: utf-8 -*-
f = open(file='test.txt', mode='r')
print('输出age大于20的信息')
for line in f:
    line_info = line.strip().split(', ')
    if int(line_info[1]) >= 20:
        print(line.strip())

f.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
# -*- encoding: utf-8 -*-
f = open(file='test.txt', mode='r')
print("全部输出:")
f.readlines()

f.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.12.2 file类的常用功能

  • seek(num):光标的定位,移动到num的位置,**num是以字节来算而不是字符!**中文占三个字节,所以seek()定位的时候可能会定位到一个字符的几个字节之间,导致可能会抛出编码错误(UnicodeDecodeError

    # -*- encoding: utf-8 -*-
    with open('test.txt', 'r') as f:
        f.seek(2)	# 从zm后面开始输出
        print(f.readlines())
    
    • 1
    • 2
    • 3
    • 4
  • flush():强制刷入硬盘。因为对硬盘的数据进行频繁操作很慢,所以文件操作有个缓存机制write的数据先写到缓存的内存里,缓存满了才会刷到硬盘里。比如我们一直write方法写入,但是如果你不close()它就不会立即刷到硬盘。如果写入的数据很重要,怕运行过程中断电等因素(计算机关闭时只有硬盘的数据不会被清空)影响到关键数据的写入,就可以用flush()强制刷入硬盘中。

  • readable():判断文件是否可读。

  • seekable():判断文件是否可以进行seek(),比如Linux所有的东西都是文件,所以需要判断这个函数判断,不常用。

  • tell():返回当前光标的位置。

  • truncate():按指定长度截断文件。指定长度就从文件头开始到指定长度为止;不指定就从当前位置截到文件末尾。**长度也是用字节来算,不是按字符来算!**针对网站访问量大的日志的分析可能会用到。

  • writable():判断文件是否可写。

  • writelines():传入一个列表数据,然后一个元素一行的写入文件。

2.12.3 混合模式下处理文件

文件模式肯定不止前面的基本三种,打开文件还有w+r+a+三种混合模式,分别是三种基本文件模式的组合,比如

  • w+就是创建一个新文件,写一段内容可以再把内容读出,可以理解为wr的组合,不常用;
  • r+就是能读能写,可以理解为ra的组合,文件写入是从末尾开始写;读的话,是从开头开始读,不用重新seek(),不用为较为常用;
  • a+就是文件一打开,文件写入是从末尾开始写,读也是从文件末尾开始读。

这三种模式很少使用,r+a+偶尔会用到。

混合模式修改文件

我们要修改文件的话就需要用到r+的混合模式。比如我们要在test.txt的第一行中插入一个'dd'

# -*- encoding: utf-8 -*-
with open('test.txt', 'r+', encoding='utf-8') as f:
    f.seek(2)
    f.write('dd')
  • 1
  • 2
  • 3
  • 4

插入过后打开test.txt文件,我们就可以看到,插入的'dd'插入是插入成功了,但是dd的第二个d会把seek(2)后面的一个字节(别忘了seek()是以字节为单位的)覆盖掉… 这是我们不想看到的,下面我们就来阐述这个问题的原因以及解决方法。

在文件中间修改数据的话,就会覆盖后面的数据,这个和数据在硬盘上的数据存储方法有关系。但是像wordvim这些工具都可以修改文件,那他们到底是怎么实现的呢?

我们就可以先把硬盘的文件数据加载到内存,再对内存的文件数据进行修改,然后再重新刷到硬盘里。如果你打开很大的word,你的电脑可能会花费较多的时间加载,就是因为在将文件数据读入内存。

2.12.4 其他模式

二进制打开文件的三种模式:rbwbab

2.12.5 不占内存的修改文件

如果像上面讲的那样,我们将数据从硬盘全部读入到内存,然后再对内存的文件数据进行修改的话,如果文件很大的话,那就会对内存造成很大影响。

那么我们可以读取一行数据到内存然后对这行数据进行修改的操作,然后刷入硬盘,周而复始,就完成了对整个文件数据的修改。这种方法被称作边读边写的方法。

将文件中的zm都改为qq

# -*- encoding: utf-8 -*-
import os

old_file = 'test.txt'
new_file = 'test_new.txt'
f = open(old_file, "r")
f_new = open(new_file, "w")
old_str = 'zm'
new_str = "qq"

for line in f:
    if "zm" in line:
        line = line.replace(old_str, new_str)
        f_new.write(line)

f.close()
f_new.close()

os.replace(new_file, old_file)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.13 关于脚本的说明

Python原来就是一种脚本语言,脚本的数据获取是通过sys模块的argv()实现的。

前面没讲的是,在Python中导入库或者模块,需要用到import语句,并在开头几行写,上面的import os就是一个例子。

下面我们给出不占内存修改文件的脚本写法,主要看5到7行的脚本获取输入数据。

import os
import sys

# 脚本获取传入的数据 需要在前面写,中间不能用其它语句
old_str = sys.argv[1]
new_str = sys.argv[2]
file = sys.argv[3]

new_file = f'new_{file}'

f = open(file, 'r', encoding='utf-8')
new_f = open(new_file, 'w', encoding='utf-8')

for line in f.readlines():
    if old_str in line:
        new_line = line.replace(old_str, new_str)
    else:
        new_line = line
    new_f.write(new_line)

f.close()
new_f.close()
os.replace(new_file, file)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

回到目录

链接:0基础学习Python笔记目录整理