变量,输入输出语句

Java,C++,Python中的数组和变量存储位置相同吗

Java

Java 的数组和变量在存储上是有区别的。在 Java 中,变量是一种基本的数据结构,用于存储单个值。而数组则是一种复合数据类型,用于存储一系列相同类型的值。

在 Java 中,变量存储在栈(stack)中,而数组则存储在堆(heap)中。栈是一种线性数据结构,用于存储基本数据类型的值和对象的引用。栈的大小是固定的,由系统自动分配和释放,而且存储在栈中的数据生命周期较短。当一个方法被调用时,系统为该方法创建一个栈帧(stack frame),用于存储方法的参数、局部变量和方法的返回值。当方法执行完毕时,栈帧会被销毁。

堆是一种动态数据结构,用于存储对象和数组。堆的大小是可变的,由 JVM 进行管理,存储在堆中的数据生命周期较长。当创建一个数组时,Java 在堆中分配一块连续的内存区域,用于存储数组的元素。数组的大小和元素类型在创建时就已经确定,而且数组中的元素可以是基本数据类型或对象类型。

因此,Java 的数组和变量存储位置不同,变量存储在栈中,而数组存储在堆中。这也是 Java 中数组和变量使用和赋值时的一些注意事项。例如,数组的元素可以通过下标访问和修改,而变量只能直接使用其值进行操作。

C++

C++ 中的变量和数组在存储位置上是不同的。变量通常被存储在栈(stack)上,而数组通常被存储在堆(heap)上或全局数据区(data segment)。

栈是一种先进后出(Last-In-First-Out,LIFO)的数据结构,主要用于存储函数调用时的临时变量和函数参数。栈中的变量会随着函数的调用和返回而动态地分配和释放内存,因此它们的生命周期比较短,只在函数的执行期间存在。在 C++ 中,局部变量和函数参数通常被存储在栈上。

堆是一种动态内存池,用于存储动态分配的内存。在 C++ 中,通过 new 和 delete 运算符可以在堆上动态地分配和释放内存。数组在堆上分配内存的方法如下:

1
int* arr = new int[10];

在这里,arr 指向堆中分配的一个大小为 10 的整型数组。

全局数据区是用于存储全局变量、静态变量和常量的一段内存区域。在程序运行期间,这段区域的内存会一直存在,直到程序结束。在 C++ 中,静态数组和全局数组通常被存储在全局数据区。

因此,C++ 中的变量和数组在存储位置上是不同的,变量通常存储在栈上,而数组通常存储在堆上或全局数据区。这也意味着,在 C++ 中使用变量和数组时需要注意它们的生命周期和存储位置,以避免出现一些常见的问题,如空指针、野指针等。

Python

在Python中,变量和数组存储位置是不同的。

Python中的变量可以指向任何类型的数据,因此其存储方式与C/C++/Java等语言不同。在Python中,变量实际上是一个指针,指向该变量所代表的对象在内存中的地址。因此,Python中的变量存储的是地址,而不是实际的值。

而Python中的数组通常是使用列表(List)来实现的,列表是一种可变的有序集合。Python中的列表实际上是一个数组,存储方式与C/C++/Java等语言的数组类似,但是列表中的元素可以是任何类型的数据。列表中的元素存储在内存中的连续空间中,可以通过索引来访问和修改。

因此,Python中的变量和数组的存储位置是不同的。

python,C++,java在格式化输出上的区别

Python、C++ 和 Java 都支持格式化输出,但它们的实现方法略有不同。

  1. Python 格式化输出

Python 中格式化输出可以使用字符串的 format() 方法或 % 操作符。下面是两种不同的写法:

使用 format() 方法:

1
2
3
name = "Tom"
age = 20
print("My name is {}, and I'm {} years old".format(name, age))

使用 % 操作符:

1
2
3
name = "Tom"
age = 20
print("My name is %s, and I'm %d years old" % (name, age))
  1. C++ 格式化输出

C++ 中格式化输出可以使用 printf() 函数或 std::cout 对象。下面是两种不同的写法:

使用 printf() 函数:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
char name[] = "Tom";
int age = 20;
printf("My name is %s, and I'm %d years old\n", name, age);
return 0;
}

使用 std::cout 对象:

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
int main()
{
char name[] = "Tom";
int age = 20;
cout << "My name is " << name << ", and I'm " << age << " years old" << endl;
return 0;
}
  1. Java 格式化输出

Java 中格式化输出可以使用 System.out.printf() 方法。下面是一个例子:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String name = "Tom";
int age = 20;
System.out.printf("My name is %s, and I'm %d years old\n", name, age);
}
}

总的来说,这三种编程语言在格式化输出方面都有各自的特点和使用方式,需要根据具体的应用场景选择适合的方式。

PS:python字符串的format方法

1. 概念:

  • str.format() 方法通过字符串中的大括号{} 来识别替换字段 replacement field,从而完成字符串的格式化。
  • 替换字段 由字段名 field name 和转换字段 conversion field 以及格式说明符 format specifier 组成,即一般形式为 {字段名!转换字段:格式说明符}。
  • 字段名分为简单字段名 simple field name 和复合字段名 compound field name。而转换字段和格式说明符都是可选的。

2. 字段名

form的完整格式是{字段名!转换字符:格式说明符}。其中字段名师必须的,而且可以分为简单字段名和复合字段名。

2.1 简单字段名

简单字段名由三中写法:

  • 省略字段名:{}
  • 使用非负十进制整数{0}
  • 变量名{name}
2.1.1 省略字段名

大括号内省略字段名,传递位置参数

  • 替换字段形式: {}
  • 注意:大括号个数可以少于位置参数的个数,反之不然。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 省略字段名传递位置参数
print('我叫{},今年{}岁。'.format('小明', 18))
"""
我叫小明,今年18岁。
"""

# 大括号个数可以少于位置参数的个数
print('我爱吃{}和{}。'.format('香蕉', '苹果', '大鸭梨'))
"""
我爱吃香蕉和苹果。
"""

# 大括号个数多于位置参数的个数则会报错
# print('我还吃{}和{}。'.format('西红柿'))
"""
IndexError: tuple index out of range
"""
2.1.2 数字字段名

可以通过数字形式的简单字段名传递位置参数。

  • 数字必须是大于等于 0 的整数。
  • 带数字的替换字段可以重复使用。
  • 数字形式的简单字段名相当于把 format 中的所有位置参数整体当作一个元组,通过字段名中的数字进行取值。即 {0} 等价于 tuple[0],所以大括号内的数字不能越界。
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
# 通过数字形式的简单字段名传递位置参数
print('身高{0},家住{1}。'.format(1.8, '铜锣湾'))
"""
身高1.8,家住铜锣湾
"""

# 数字形式的简单字段名可以重复使用。
print('我爱{0}。\n她今年{1}。\n我也爱{0}。'.format('阿香', 17))
"""
我爱阿香。
她今年17。
我也爱阿香。
"""

# 体会把所有位置参数整体当成元组来取值
print('阿香爱吃{1}、{3}和{0}。'.format(
'榴莲', '臭豆腐', '皮蛋', '鲱鱼罐头', '螺狮粉'))
"""
阿香爱吃臭豆腐、鲱鱼罐头和榴莲。
"""

# 尝试一下越界错误
# print('{1}'.format('错误用法'))
"""
IndexError: tuple index out of range
"""
2.1.3 变量字段名

使用变量名形式的简单字段名传递关键字参数

  • 关键字参数的位置可以随意调换。
1
2
3
4
5
6
7
8
9
10
11
# 使用变量名形式的简单字段名传递关键字参数
print('我大哥是{name},今年{age}岁。'.format(name='阿飞', age=20))
"""
我大哥是阿飞,今年20岁。
"""

# 关键字参数的顺序可以随意调换
print('我大哥是{name},今年{age}岁。'.format(age=20, name='阿飞'))
"""
我大哥是阿飞,今年20岁。
"""
2.1.4 简单字段名的混合使用
  • 混合使用数字形式和变量名形式的字段名,可以同时传递位置参数和关键字参数。
  • 关键字参数必须位于位置参数之后。
  • 混合使用时可以省略数字。
  • 省略字段名 {} 不能和数字形式的字段名 {非负整数} 同时使用。
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
# 混合使用数字形式和变量名形式的字段名
# 可以同时传递位置参数和关键字参数
print('这是一个关于{0}、{1}和{girl}的故事。'.format(
'小明', '阿飞', girl='阿香'))
"""
这是一个关于小明、阿飞和阿香的故事。
"""

# 但是关键字参数必须位于位置参数之后
# print('这是一个关于{0}、{1}和{girl}的故事。'.format(
# '小明', girl='阿香' , '阿飞'))
"""
SyntaxError: positional argument follows keyword argument
"""

# 数字也可以省略
print('这是一个关于{}、{}和{girl}的故事。'.format(
'小明', '阿飞', girl='阿香'))

# 但是省略字段名不能和数字形式的字段名同时出现
# print('这是一个关于{}、{1}和{girl}的故事。'.format(
# '小明', '阿飞', girl='阿香'))
"""
ValueError: cannot switch from automatic field numbering to manual field specification
"""
2.1.5 使用元组和字典传参

str.format() 方法还可以使用 *元组**字典 的形式传参,两者可以混合使用。 位置参数、关键字参数、*元组**字典 也可以同时使用,但是要注意,位置参数要在关键字参数前面,*元组 要在 **字典 前面。

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
# 使用元组传参
infos = '钢铁侠', 66, '小辣椒'
print('我是{},身价{}亿。'.format(*infos))
"""
我是钢铁侠,身家66亿。
"""
print('我是{2},身价{1}亿。'.format(*infos))
"""
我是小辣椒,身家66亿。
"""

# 使用字典传参
venom = {'name': '毒液', 'weakness': '火'}
print('我是{name},我怕{weakness}。'.format(**venom))
"""
我是毒液,我怕火。
"""

# 同时使用元组和字典传参
hulk = '绿巨人', '拳头'
captain = {'name': '美国队长', 'weapon': '盾'}
print('我是{}, 我怕{weapon}。'.format(*hulk, **captain))
print('我是{name}, 我怕{1}。'.format(*hulk, **captain))

"""
我是绿巨人, 我怕盾。
我是美国队长, 我怕拳头。
"""

# 同时使用位置参数、元组、关键字参数、字典传参
# 注意:
# 位置参数要在关键字参数前面
# *元组要在**字典前面
tup = '鹰眼',
dic = {'weapon': '箭'}
text = '我是{1},我怕{weakness}。我是{0},我用{weapon}。'
text = text.format(
*tup, '黑寡妇', weakness='男人', **dic)
print(text)
"""
我是黑寡妇,我怕男人。我是鹰眼,我用箭。
"""
2.2 复合字段名
  • 同时使用了数字和变量名两种形式的字段名就是复合字段名

  • 复合字段名

    支持两种操作符:

    • . 点号
    • [] 中括号
2.2.1 使用. 点号

传递位置参数

  • 替换字段形式:{数字.属性名}
  • 只有一个替换字段的时候可以省略数字
1
2
3
4
5
6
7
8
class Person(object):
def __init__(self,name,age,gender):
self.name = name
self.age = age
self.gender = gender
p = Person('zhangsan',18,'female')
print('姓名是{0.name},年龄是{0.age},性别是{0.gender}'.format(p))
print('姓名是{.name}'.format(p)) # 只有一个替换字段时,可以省略数字
2.2.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
# 中括号用法:用列表传递位置参数
infos = ['阿星', 9527]
food = ['霸王花', '爆米花']
print('我叫{0[0]},警号{0[1]},爱吃{1[0]}。'.format(
infos, food))
"""
我叫阿星,警号9527,爱吃霸王花。
"""

# 中括号用法:用元组传递位置参数
food = ('僵尸', '脑子')
print('我叫{0[0]},年龄{1},爱吃{0[1]}。'.format(
food, 66))
"""
我叫僵尸,年龄66,爱吃脑子。
"""

# 中括号用法:用字典传递位置参数
dic = dict(name='阿星', pid=9527)
print('我是{[name]}!'.format(
dic))
# 多个替换字段,不能省略数字
print('我是{0[name]},警号{0[pid]}。'.format(
dic))
"""
我是阿星!
我是阿星,警号9527。
"""

3. 转换字段

转换字段 conversion field 的取值有三种,前面要加 !

  • s:传递参数之前先对参数调用 str()
  • r:传递参数之前先对参数调用 repr()
  • a:传递参数之前先对参数调用 ascii()

ascii() 函数类似 repr() 函数,返回一个可以表示对象的字符串。 但是对于非 ASCII 字符,使用 \x\u 或者 \U 转义。

1
2
3
4
5
6
7
8
9
# 转换字段
print('I am {!s}!'.format('Bruce Lee 李小龙'))
print('I am {!r}!'.format('Bruce Lee 李小龙'))
print('I am {!a}!'.format('Bruce Lee 李小龙'))
"""
I am Bruce Lee 李小龙!
I am 'Bruce Lee 李小龙'!
I am 'Bruce Lee \u674e\u5c0f\u9f99'!
"""

4. 格式说明符

格式说明符使用过于复杂,且实际使用场景不多,暂不讨论。

进制转换、数据类型详解、类型转换、运算符

python,C++,java在数据类型和类型转换上的区别

Python、C++、Java 都是常用的编程语言,它们在数据类型和类型转换方面有一些不同点,具体如下:

  1. 数据类型

Python、C++ 和 Java 支持的数据类型略有不同。下面是它们支持的一些常见数据类型:

  • Python: int、float、bool、str、list、tuple、dict、set 等
  • C++: int、float、bool、char、double、long、short、unsigned 等
  • Java: int、float、double、char、boolean、byte、short、long 等
  1. 类型转换

Python、C++ 和 Java 都支持类型转换,但它们的实现方法略有不同。

  • Python:可以使用 int()、float()、bool()、str() 等函数进行类型转换。此外,Python 也支持自动类型转换。
  • C++:可以使用强制类型转换和隐式类型转换。强制类型转换需要使用括号加上转换后的类型来完成,例如 (int)3.14 将 3.14 转换为整数。隐式类型转换会自动将一种类型转换为另一种类型,例如将 int 转换为 float。
  • Java:可以使用强制类型转换和自动类型转换。强制类型转换需要使用括号加上转换后的类型来完成,例如 (int)3.14 将 3.14 转换为整数。自动类型转换会自动将一种类型转换为另一种类型,例如将 int 转换为 double。

总的来说,这三种编程语言在数据类型和类型转换方面都有各自的特点和使用方式,需要根据具体的应用场景选择适合的编程语言和数据类型。

python,java,C++位运算的区别

位运算是一种基于二进制的运算,可以对二进制数的每一位进行操作。Python、Java和C++都支持位运算,下面是它们的区别:

Python: Python支持位运算符,包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移位(<<)、右移位(>>),但是Python中没有无符号位移运算符。Python的整数类型是可以自动扩展的,不需要考虑溢出问题。

Java: Java同样支持位运算符,包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移位(<<)、右移位(>>)和无符号右移位(>>>)。Java中的整数类型有固定的位数,超出范围会出现溢出问题。

C++: C++同样支持位运算符,包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移位(<<)、右移位(>>)和无符号右移位(>>>)。与Java不同的是,C++中的整数类型也有固定的位数,超出范围也会出现溢出问题。C++也支持位域,可以将一个变量的几个位用来存储不同的信息。

PS:Python讲解进制抓换,位运算,补充位运算

流程控制语句

C++,java,python的for循环的区别

虽然C++、Java和Python的for循环语法有些差异,但其本质都是实现循环执行一段代码,直到达到指定的循环次数或者满足某些条件后退出循环。

下面是这三种语言中for循环的主要差异:

C++的for循环

C++的for循环通常用于实现计数循环,其语法如下:

1
2
3
for (初始化表达式; 布尔表达式; 更新表达式) {
// 代码块
}

其中,初始化表达式是一条语句,用于初始化一个计数器变量;布尔表达式是一个返回布尔值的表达式,用于测试计数器变量是否达到指定的值;更新表达式用于更新计数器变量的值。

C++遍历容器的几种方法

C++ 中遍历容器(如数组、vector、map 等)的方法有多种,下面介绍其中的几种:

  1. 使用 for 循环

使用 for 循环可以遍历数组和 vector 等容器,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec = {1, 2, 3, 4, 5};
for (int i = 0; i < vec.size(); i++)
{
cout << vec[i] << " ";
}
return 0;
}
  1. 使用 C++11 新特性的 for-each 循环

C++11 引入了 for-each 循环,可以方便地遍历容器中的元素,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec = {1, 2, 3, 4, 5};
for (int x : vec)
{
cout << x << " ";
}
return 0;
}
  1. 使用迭代器

使用迭代器可以遍历所有类型的容器,包括 vector、list、set、map 等,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec = {1, 2, 3, 4, 5};
for (vector<int>::iterator it = vec.begin(); it != vec.end(); it++)
{
cout << *it << " ";
}
return 0;
}
  1. 使用 auto 关键字

使用 auto 关键字可以让编译器自动推导容器类型和迭代器类型,使代码更简洁,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); it++)
{
cout << *it << " ";
}
return 0;
}

以上是常见的遍历容器的方法,根据具体的应用场景选择适合的方法即可。

Java的for循环

Java的for循环也常用于计数循环,其语法如下:

1
2
3
for (初始化表达式; 布尔表达式; 更新表达式) {
// 代码块
}

Java的for循环与C++的for循环语法基本相同,只是Java的for循环还支持对数组和集合进行迭代操作。

1
2
3
for (数据类型 变量名: 数组名/集合名) {
// 代码块
}

Python的for循环

Python的for循环主要用于迭代操作,其语法如下:

1
2
for 变量 in 序列:
// 代码块

其中,序列可以是列表、元组、字符串等可迭代对象。在每一次迭代过程中,循环变量会被赋值为序列中的下一个元素。

需要注意的是,Python中的for循环不支持类似于C++和Java中的计数循环,但可以通过range函数实现类似的功能。例如:

1
2
for i in range(10):
// 代码块

这段代码会执行10次循环,每次循环i的值会依次为0~9。

Python的for循环可以迭代(遍历)以下对象:

  1. 序列(sequence):例如字符串(string)、列表(list)、元组(tuple)等。
  2. 集合(set):例如集合(set)和冻结集合(frozenset)等。
  3. 映射(mapping):例如字典(dictionary)等。
  4. 迭代器(iterator):实现了__next__()和__iter__()方法的对象,例如生成器(generator)和文件对象(file object)等。

需要注意的是,虽然Python的for循环可以迭代大多数容器类型(container),但并不是所有容器类型都是可迭代的(iterable)。例如数字(integer)和布尔(boolean)等非容器类型就不是可迭代的。

此外,我们还可以使用range()函数生成一个由整数组成的序列,并用for循环遍历这个序列。例如:

1
2
for i in range(5):
print(i)

这个代码片段将输出从0到4的整数。

字符串

C++,java,python的字符串常见操作对比

字符串是计算机程序中非常常见的数据类型之一。以下是一些常见的字符串操作:

  • 获取字符串长度:获取字符串中字符的个数,可以使用 lengthsize 方法。
  • 字符串连接:将两个字符串拼接起来,可以使用 +concat 方法。
  • 字符串切片:截取字符串中的一部分,可以使用下标、substring 方法或者正则表达式。
  • 分割字符串:将一个字符串分割成多个子串,可以使用 split 方法或者正则表达式。
  • 替换字符串:将字符串中的一个子串替换成另一个字符串,可以使用 replace 方法或者正则表达式。
  • 查找字符串:在一个字符串中查找指定的子串,可以使用 find 方法或者正则表达式。
  • 转换字符串:将一个字符串转换成另一种形式,比如将字符串转换成整数或者浮点数,可以使用 stoistof 或者 parseFloatparseInt 等方法。
  • 字符串比较:比较两个字符串的大小,可以使用 <><=>===!= 操作符或者 compareTo 方法。

以下是 C++,Java 和 Python 中一些常见的字符串操作及其对比:

字符串长度

在 C++ 中,可以使用 size() 或者 length() 函数来获取字符串的长度。在 Java 中,可以使用 length() 方法来获取字符串的长度。在 Python 中,可以使用 len() 函数来获取字符串的长度。

1
2
str = "Hello, world!"
print(len(str)) # 输出 13
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "Hello, world!";
cout << str.size() << endl; // 输出 13
return 0;
}

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
String str = "Hello, world!";
System.out.println(str.length()); // 输出 13
}
}

字符串比较

在 C++ 中,可以使用 == 或者 != 运算符来比较字符串是否相等。在 Java 中,可以使用 equals() 方法来比较字符串是否相等。在 Python 中,可以使用 == 或者 != 运算符来比较字符串是否相等。

1
2
3
4
5
6
str1 = "Hello, world!"
str2 = "Hello, Python!"
if str1 == str2:
print("相等")
else:
print("不相等") # 输出 "不相等"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

using namespace std;

int main() {
string str1 = "Hello, world!";
string str2 = "Hello, C++!";
if (str1 == str2) {
cout << "相等" << endl;
} else {
cout << "不相等" << endl; // 输出 "不相等"
}C++
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
String str1 = "Hello, world!";
String str2 = "Hello, Java!";
if (str1.equals(str2)) {
System.out.println("相等");
} else {
System.out.println("不相等"); // 输出 "不相等"
}
}
}

字符串查找

在 C++ 中,可以使用 find 函数来查找字符串中是否包含某个子串。在 Java 中,可以使用 indexOf 方法来查找字符串中是否包含某个子串。在 Python 中,可以使用 in 关键字来查找字符串中是否包含某个子串。

1
2
3
4
5
6
str = "Hello, world!"
if "world" in str:
print("包含")
else:
print("不包含") # 输出 "包含"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "Hello, world!";
if (str.find("world") != string::npos) {
cout << "包含" << endl;
} else {
cout << "不包含" << endl; // 输出 "包含"
}
return 0;
}
1

字符串切片

在 C++,Java 和 Python 中,字符串切片操作用于从一个字符串中获取子字符串。但是,这些语言中的切片操作略有不同。

C++

在 C++ 中,可以使用 substr 函数来获取字符串的子串,其语法如下:

1
string substr (size_t pos, size_t len) const;

其中,pos 参数指定要从哪个位置开始获取子串,len 参数指定要获取的子串的长度。如果省略 len 参数,则会返回从 pos 开始到字符串末尾的所有字符。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "Hello, world!";
string sub = str.substr(7, 5);
cout << sub << endl; // 输出 "world"
return 0;
}

Java

在 Java 中,可以使用 substring 方法来获取字符串的子串,其语法如下:

1
String substring(int beginIndex, int endIndex);

其中,beginIndex 参数指定要从哪个位置开始获取子串(包括该位置的字符),endIndex 参数指定要获取的子串的结束位置(不包括该位置的字符)。如果省略 endIndex 参数,则会返回从 beginIndex 开始到字符串末尾的所有字符。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "Hello, world!";
String sub = str.substring(7, 12);
System.out.println(sub); // 输出 "world"
}
}

Python

在 Python 中,可以使用切片操作符 [] 来获取字符串的子串,其语法如下:

1
string[start:end:step]

其中,start 参数指定要从哪个位置开始获取子串(包括该位置的字符),end 参数指定要获取的子串的结束位置(不包括该位置的字符),step 参数指定要跳过的字符数(默认为 1。为正数则从左向右输出,为负数则从右向左输出)。如果省略 start 参数,则会从字符串的开头开始获取子串;如果省略 end 参数,则会一直获取到字符串的末尾。

1
2
3
str = "Hello, world!"
sub = str[7:12]
print(sub) # 输出 "world"
1
2
3
4
5
6
7
8
9
s = "ABCDEFG"
s[1:4]#BCD
s[-3:]#EFG
s[:]#ABCDEF

s[::-1]#GFEDCBA
s[::-2]#GECA 倒着取且步长为2
s[0:6:-2]#无法取到任何值,注意方向,从0向左取
s[6:0:-2]#GECA

字符串连接

在 C++、Java 和 Python 中,字符串连接操作是非常常见的字符串操作之一。下面是在这三种语言中实现字符串连接的一些方法的对比:

使用 + 运算符

在 C++、Java 和 Python 中,都可以使用 + 运算符将两个字符串拼接起来。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

using namespace std;

int main() {
string str1 = "Hello";
string str2 = "world!";
string newStr = str1 + ", " + str2 + "!";
cout << newStr << endl; // 输出 "Hello, world!"
return 0;
}
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "world!";
String newStr = str1 + ", " + str2 + "!";
System.out.println(newStr); // 输出 "Hello, world!"
}
}
1
2
3
4
str1 = "Hello"
str2 = "world!"
newStr = str1 + ", " + str2 + "!"
print(newStr) # 输出 "Hello, world!"

使用 concat 方法

在 Java 中,还可以使用 concat 方法将两个字符串拼接起来。

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "world!";
String newStr = str1.concat(", ").concat(str2).concat("!");
System.out.println(newStr); // 输出 "Hello, world!"
}
}

使用 join 方法

在 Python 中,可以使用 join 方法将多个字符串拼接成一个字符串。

1
2
3
strList = ["Hello", "world!"]
newStr = ", ".join(strList) + "!"
print(newStr) # 输出 "Hello, world!"

总的来说,在 C++、Java 和 Python 中,都可以使用类似于 + 运算符的方式来实现字符串连接,但是在 Java 和 Python 中还提供了其他的方法来实现字符串连接。

字符串分割

在 C++、Java 和 Python 中,字符串分割操作也是比较常见的字符串操作之一。下面是在这三种语言中实现字符串分割的一些方法的对比:

使用 split 方法

在 Java 和 Python 中,都提供了 split 方法,可以将一个字符串按照指定的分隔符分割成多个子串。

在 Java 中,可以使用 split 方法将一个字符串按照指定的正则表达式分割成多个子串,返回一个字符串数组。

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String str = "apple,banana,orange";
String[] strArr = str.split(",");
for (String s : strArr) {
System.out.println(s);
}
}
}

在 Python 中,可以使用 split 方法将一个字符串按照指定的分隔符分割成多个子串,返回一个字符串列表。

1
2
3
4
str = "apple,banana,orange"
strList = str.split(",")
for s in strList:
print(s)

使用 istringstream(C++)

在 C++ 中,可以使用 istringstream 类将一个字符串按照指定的分隔符分割成多个子串,返回一个字符串流对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

using namespace std;

int main() {
string str = "apple,banana,orange";
vector<string> strVec;
istringstream iss(str);
string s;
while (getline(iss, s, ',')) {
strVec.push_back(s);
}
for (string s : strVec) {
cout << s << endl;
}
return 0;
}

总的来说,在 Java 和 Python 中都提供了比较方便的 split 方法,可以快速地将一个字符串按照指定的分隔符分割成多个子串,而在 C++ 中需要使用 istringstream 类来实现类似的功能。

字符串替换

在 C++、Java 和 Python 中,字符串替换操作也是常见的字符串操作之一。下面是在这三种语言中实现字符串替换的一些方法的对比:

使用 replace 方法

在 Java 和 Python 中,都提供了 replace 方法,可以将一个字符串中指定的子串替换为另一个字符串。

在 Java 中,可以使用 replace 方法将一个字符串中指定的子串替换为另一个字符串。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "hello, world!";
String newStr = str.replace("world", "Java");
System.out.println(newStr);
}
}

在 Python 中,可以使用 replace 方法将一个字符串中指定的子串替换为另一个字符串。

1
2
3
str = "hello, world!"
newStr = str.replace("world", "Python")
print(newStr)

使用算法库(C++)

在 C++ 中,可以使用 algorithm 库中的 replace 函数将一个字符串中指定的子串替换为另一个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

int main() {
string str = "hello, world!";
replace(str.begin(), str.end(), ',', ':');
cout << str << endl;
return 0;
}

使用正则表达式(C++、Java、Python)

在 C++、Java 和 Python 中,都支持正则表达式操作,可以使用正则表达式来实现字符串替换操作。

在 C++ 中,可以使用 regex 库来实现正则表达式操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <regex>
#include <string>

using namespace std;

int main() {
string str = "hello, world!";
regex reg(",");
string newStr = regex_replace(str, reg, ":");
cout << newStr << endl;
return 0;
}

在 Java 中,可以使用 replaceAll 方法来实现正则表达式操作。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "hello, world!";
String newStr = str.replaceAll(",", ":");
System.out.println(newStr);
}
}

在 Python 中,可以使用 re 模块来实现正则表达式操作。

1
2
3
4
5
import re

str = "hello, world!"
newStr = re.sub(",", ":", str)
print(newStr)

总的来说,在 Java 和 Python 中都提供了比较方便的 replace 方法,可以快速地将一个字符串中指定的子串替换为另一个字符串,而在 C++ 中需要使用 algorithm 库中的 replace 函数或者 regex 库来实现类似的功能。同时,正则表达式在三种语言中都可以用来实现字符串替换操作。

转换字符串

在 C++、Java 和 Python 中,字符串与其他数据类型之间的转换是非常常见的操作。下面是在这三种语言中实现字符串转换的一些方法的对比:

字符串转整数

在 C++ 中,可以使用 stoi 函数将一个字符串转换为整数。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "123";
int num = stoi(str);
cout << num << endl;
return 0;
}

在 Java 中,可以使用 Integer.parseInt 方法将一个字符串转换为整数。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "123";
int num = Integer.parseInt(str);
System.out.println(num);
}
}

在 Python 中,可以使用 int 函数将一个字符串转换为整数。

1
2
3
str = "123"
num = int(str)
print(num)

整数转字符串

在 C++ 中,可以使用 to_string 函数将一个整数转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
int num = 123;
string str = to_string(num);
cout << str << endl;
return 0;
}

在 Java 中,可以使用 String.valueOf 方法将一个整数转换为字符串。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
int num = 123;
String str = String.valueOf(num);
System.out.println(str);
}
}

在 Python 中,可以使用 str 函数将一个整数转换为字符串。

1
2
3
num = 123
str = str(num)
print(str)

字符串转浮点数

在 C++ 中,可以使用 stof 函数将一个字符串转换为浮点数。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "3.14";
float num = stof(str);
cout << num << endl;
return 0;
}

在 Java 中,可以使用 Float.parseFloat 方法将一个字符串转换为浮点数。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "3.14";
float num = Float.parseFloat(str);
System.out.println(num);
}
}

在 Python 中,可以使用 float 函数将一个字符串转换为浮点数。

1
2
3
str = "3.14"
num = float(str)
print(num)

浮点数转字符串

在 C++ 中,可以使用 to_string 函数将一个浮点数转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
float num = 3.14;
string str = to_string(num);
cout << str << endl;
return 0;
}

在 Java 中,可以使用 String.valueOf 方法将一个浮点数转换为字符串。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
float num = 3.14f;
String str = String.valueOf(num);
System.out.println(str);
}
}

在 Python 中,可以使用 str 函数将一个浮点数转换为字符串。

1
2
3
num = 3.14
str = str(num)
print(str)

字符串转布尔值

在 C++ 中,可以使用 stoi 函数将一个字符串转换为布尔值。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
string str = "true";
bool value = stoi(str);
cout << value << endl;
return 0;
}

在 Java 中,可以使用 Boolean.parseBoolean 方法将一个字符串转换为布尔值。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String str = "true";
boolean value = Boolean.parseBoolean(str);
System.out.println(value);
}
}

在 Python 中,可以使用 bool 函数将一个字符串转换为布尔值。

1
2
3
str = "True"
value = bool(str)
print(value)

布尔值转字符串

在 C++ 中,可以使用 to_string 函数将一个布尔值转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main() {
bool value = true;
string str = to_string(value);
cout << str << endl;
return 0;
}

在 Java 中,可以使用 String.valueOf 方法将一个布尔值转换为字符串。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
boolean value = true;
String str = String.valueOf(value);
System.out.println(str);
}
}

在 Python 中,可以使用 str 函数将一个布尔值转换为字符串。

1
2
3
value = True
str = str(value)
print(str)

总的来说,在这些语言中,字符串与其他数据类型之间的转换非常容易,并且提供了方便的内置函数和方法来完成这些操作。

PS:Python中字符串的常见操作

获取长度:len

查找内容:find,index,rfind,rindex

判断:startswith,endswith,isdigit,isalnum,isspace

计算出现次数:count

替换内容:replace

切割字符串:split,rsplit,splitlines,partition,rpartition

修改大小写:capitalize,title,upper,lower

空格处理:ljust,rjust,center,lstrip,rstrip,strip

字符串拼接:join

注意:Python中字符串是不可变的,所有的字符串相关方法,都不会改变原来的字符串,都是返回一个结果,在这个新的的返回值里,保留了运行后的结果。

len

len函数可以获取字符串的长度。

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(len(mystr)) # 17 获取字符串的长度

查找

查找相关的方法,使用方式大致相同,但是略有区别。

find

查找指定内容在字符串中是否存在,如果存在就返回该内容在字符串中第一次出现的开始位置索引值,如果不存在,则返回-1.

语法格式:

1
S.find(sub[, start[, end]]) -> int

示例:

1
2
3
4
5
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.find('好风光')) # 10 '好风光'第一次出现时,'好'所在的位置
print(mystr.find('你好')) # -1 '你好'不存在,返回 -1
print(mystr.find('风', 12)) # 15 从下标12开始查找'风',找到风所在的位置试15
print(mystr.find('风光',1,10)) # -1 从下标1开始到12查找"风光",未找到,返回 -1

rfind

类似于 find()函数,不过是从右边开始查找。

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.rfind('好')) # 14

index

跟find()方法一样,只不过,find方法未找到时,返回-1,而str未找到时,会报一个异常。

语法格式:

1
S.index(sub[, start[, end]]) -> int

rindex

类似于 index(),不过是从右边开始。

判断

python提供了非常丰富的方法,可以用来对一个字符串进行判断。

startswith

判断字符串是否以指定内容开始。 语法格式:

1
S.startswith(prefix[, start[, end]]) -> bool

示例:

1
2
3
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.startswith('今')) # True
print(mystr.startswith('今日')) # False

endswith

判断字符串是否以指定内容结束。

1
2
3
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.endswith('好风光')) #True
print(mystr.endswith('好日子')) #False

isalpha

判断字符串是否是纯字母。

1
2
3
4
mystr = 'hello'
print(mystr.isalpha()) # True
mystr = 'hello world'
print(mystr.isalpha()) # False 因为中间有空格

isdigit

判断一个字符串是否是纯数字,只要出现非0~9的数字,结果就是False.

1
2
3
4
5
6
mystr = '1234'
print(mystr.isdigit()) # True
mystr = '123.4'
print(mystr.isdigit()) # False
mystr = '-1234'
print(mystr.isdigit()) # False

isalnum

判断是否由数字和字母组成。只要出现了非数字和字母,就返回False.

1
2
3
4
5
6
7
8
mystr = 'abcd'
print(mystr.isalnum()) # True
mystr = '1234'
print(mystr.isalnum()) # True
mystr = 'abcd1234'
print(mystr.isalnum()) # True
mystr = 'abcd1234_'
print(mystr.isalnum()) # False

isspace

如果 mystr 中只包含空格,则返回 True,否则返回 False.

1
2
3
4
5
6
mystr = ''
print(mystr.isspace()) # False mystr是一个空字符串
mystr = ' '
print(mystr.isspace()) # True 只有空格
mystr = ' d'
print(mystr.isspace()) # False 除了空格外还有其他内容

计数

count

返回 str在start和end之间 在 mystr里面出现的次数。

语法格式:

1
S.count(sub, start, end) -> int

示例:

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.count('好')) # 3. '好'字出现三次

替换

替换字符串中指定的内容,如果指定次数count,则替换不会超过count次。

1
2
3
4
5
6
7
mystr = '今天天气好晴朗,处处好风光呀好风光'
newstr = mystr.replace('好', '坏')
print(mystr) # 今天天气好晴朗,处处好风光呀好风光 原字符串未改变!
print(newstr) # 今天天气坏晴朗,处处坏风光呀坏风光 得到的新字符串里,'好'被修改成了'坏'

newstr = mystr.replace('好','坏',2) # 指定了替换的次数
print(newstr) # 今天天气坏晴朗,处处坏风光呀好风光 只有两处的'好'被替换成了'坏'

c++的替换是直接改变原来的字符串,而python和java的替换是返回新的字符串,区分。

内容分隔

内容分隔主要涉及到split,splitlines,partition和rpartition四个方法。

split

以指定字符串为分隔符切片,如果 maxsplit有指定值,则仅分隔 maxsplit+1 个子字符串。返回的结果是一个列表。

1
2
3
4
5
6
7
8
9
mystr = '今天天气好晴朗,处处好风光呀好风光'
result = mystr.split() # 没有指定分隔符,默认使用空格,换行等空白字符进行分隔
print(result) #['今天天气好晴朗,处处好风光呀好风光'] 没有空白字符,所以,字符串未被分隔

result = mystr.split('好') # 以 '好' 为分隔符
print(result) # ['今天天气', '晴朗,处处','风光呀,'风光']

result = mystr.split("好",2) # 以 '好' 为分隔符,最多切割成3份
print(result) # ['今天天气', '晴朗,处处', '风光呀好风光']

rsplit

用法和split基本一致,只不过是从右往左分隔。

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.rsplit('好',1)) #['今天天气好晴朗,处处好风光呀', '风光']

splitlines

按照行分隔,返回一个包含各行作为元素的列表。

1
2
3
4
5
6
7
8
mystr = 'hello \nworld'
print(mystr.splitlines())
s = '''
今天天气好晴朗!
处处好风光呀好风光!
'''
result = s.splitlines()
print(result)#['', '今天天气好晴朗!', '处处好风光呀好风光!']

partition

把mystr以str分割成三部分,str前,str和str后,三部分组成一个元组

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.partition('好')) # ('今天天气', '好', '晴朗,处处好风光呀好风光')

rpartition

类似于 partition()函数,不过是从右边开始.

1
2
mystr = '今天天气好晴朗,处处好风光呀好风光'
print(mystr.rpartition('好')) # ('今天天气好晴朗,处处好风光呀', '好', '风光')

PS:partitionrpartition只会将字符串分成三部分

修改大小写

修改大小写的功能只对英文有效,主要包括,首字母大写capitalize,每个单词的首字母大写title,全小写lower,全大写upper.

capitalize

第一个单词的首字母大写。

1
2
mystr = 'hello world'
print(mystr.capitalize()) # Hello world

title

每个单词的首字母大写。

1
2
mystr = 'hello world'
print(mystr.title()) # Hello World

lower

所有都变成小写。

1
2
mystr = 'hElLo WorLD'
print(mystr.lower()) # hello world

upper

所有都变成大写。

1
2
mystr = 'hello world'
print(mystr.upper()) #HELLO WORLD

空格处理

引入:

1
2
username = input("请输入用户名:")#请输入用户名:admin    '\n'
print(len(username))#9

如果输入了admin加上四个空格时空格也会被读取到,如果写入数据里后,之后再登录时不直观,易出错,不希望字符串参与到数据库中,所以一般需要一些去除空格的操作。

Python为我们提供了各种操作字符串里表格的方法。

ljust

返回指定长度的字符串,并在右侧使用空白字符补全(左对齐)。

1
2
str = 'hello'
print(str.ljust(10)) # hello 在右边补了五个空格

rjust

返回指定长度的字符串,并在左侧使用空白字符补全(右对齐)。

1
2
str = 'hello'
print(str.rjust(10)) # hello在左边补了五个空格

center

返回指定长度的字符串,并在两端使用空白字符补全(居中对齐)

1
2
str = 'hello'
print(str.center(10)) # hello 两端加空格,让内容居中

上面三个类似于C/C++ 的\t,但是比较灵活

lstrip

删除 mystr 左边的空白字符。

1
2
mystr = '    he   llo      '
print(str.lstrip()) #he llo 只去掉了左边的空格,中间和右边的空格被保留

rstrip

删除 mystr 右边的空白字符。

1
2
mystr = '    he   llo      '
print(str.rstrip()) # he llo右边的空格被删除

strip

删除两断的空白字符。

1
2
str = '    he   llo      '
print(str.strip()) #he llo

字符串拼接

把参数进行遍历,取出参数里的每一项,然后再在后面加上mystr

语法格式:

1
S.join(iterable)

join的作用常用在拼接列表的字符串

示例:

1
2
3
mystr = 'a'
print(mystr.join('hxmdq')) #haxamadaq 把hxmd一个个取出,并在后面添加字符a. 最后的 q 保留,没有加 a
print(mystr.join(['hi','hello','good'])) #hiahelloagood

作用:可以把列表或者元组快速的转变成为字符串,并且以指定的字符分隔。

1
2
3
txt = '_'
print(txt.join(['hi','hello','good'])) #hi_hello_good
print(txt.join(('good','hi','hello'))) #good_hi_hello

字符串运算符

  1. 字符串和字符串之间能够使用加法运算符,作用是将两个字符串拼接成为一个字符串。例如:'hello' + 'world'的结果是 'helloworld'
  2. 字符串和数字之间可以做乘法运算,结果是将指定的字符串重复多次。例如:'hello'*2的结果是hellohello
  3. 字符串和字符串之间,如果使用比较运算符进行计算,会获取字符对应的编码,然后进行比较。
  4. 除上述几种运算符以外,字符串默认不支持其他运算符。

示例

1
2
3
4
5
6
7
8
#查找:find,rfind
#例子:截取指定字符串
path = "https://www.bilibili.com/video/BV1R7411F7JV/?p=32&spm_id_from=pageDriver&vd_source=547f6e585f4f9c01d3cdb8fe3dcd38e9"
name = path[path.rfind('=') + 1:] #path.rfind('=')返回的是下标
print(name)#547f6e585f4f9c01d3cdb8fe3dcd38e9

path = "https://www.bilibili.com/video/BV1R7411F7JV/?p=32&spm_id_from=pageDriver&vd_source=547f6e585f4f9c01d3cdb8fe3dcd38e9"
print(path.rfind('bilibili'))#12
  1. find和index的区别是find没找到会返回-1,index没找到会报错。
  2. 如果查找的是多个字母,则返回子母首地址的下标
1
2
3
4
5
6
7
8
#判断:startswith,endswith,isdigit,isalnum,isspace
#返回值都是bool类型
path = "https://www.bilibili.com/video/BV1R7411F7JV/?p=32&spm_id_from=pageDriver&vd_source=547f6e585f4f9c01d3cdb8fe3dcd38e9"
result = path.startswith('http')#返回的是bool类型
print(result)#True

s = "a1234"
print(s.isdigit)
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
#练习
'''
模拟文件上传,键盘输入文件名abc.jpg,判断文件名abc是否大于6位以上,扩展名是否是:jpg,gif,png格式,如果不是则提示上传失败,如果名字不满足条件,而扩展名满足条件则随机生成一个6位数子组成的文件名,打印成功上传xxxx.png
'''
import random

file = input()
if file.endswith('jpg') or file.endswith('gif') or file.endswith('png'):
i = file.find('.')
name = file[:i]
if len(name)<6:
n = random.randint(100000,999999)
file = str(n) + file[i:] #T:所有的字符串操作都不会改变原来的字符串
print("成功上传%s"%file)
else:
print("文件格式错误")

'''
模拟随机产生验证码
'''
filename = ""
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456"
for i in range(4):
index = random.randint(0,len(s)-1)
filename += s[index]
print(filename)

字符集

计算机只能处理数字(其实就是数字0和数字1),如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),0 - 255被用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码。

ASCII码表使用7位二进制表示一个字符,它的区间范围时0~127,一共只能表示128个字符,仅能支持英语。随着计算机科学的发展,西欧语言、希腊语、泰语、阿拉伯语、希伯来语等语言的字符也被添加到码表中,形成了一个新的码表ISO8859-1(又被称为Latin1)码表。ISO8859-1使用8位二进制表示一个字符串,完全兼容ASCII码表。

Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

字符和编码相互转换

使用chr和ord方法,可以实现字符和编码之间的相互转换。

1
2
print(ord('a'))  # 使用ord方法,可以获取一个字符对应的编码
print(chr(100)) # 使用chr方法,可以获取一个编码对应的字符

编码规则

使用Unicode为每种语言的每个字符都设定了唯一的二进制编码,但是它还是存在一定的问题,不够完美。

例如,汉字 “你” 转换成为一个字符结果是0x4f60,转换成为二进制就是 01001111 01100000,此时就有两个问题:

  1. 1001111 01100000 到底是一个汉字 “你” ,还是两个 Latin1 字符?
  2. 如果Unicode进行了规定,每个字符都使用n个八位来表示,对于Latin1字符来说,又会浪费很多存储空间。

为了解决这个问题,就出现了一些编码规则,按照一定的编码规则对Unicode数字进行计算,得出新的编码。在中国常用的字符编码有 GBK,Big5utf8这三种编码规则。

使用字符串的encode方法,可以将字符串按照指定的编码格式转换称为二进制;使用decode方法,可以将一个二进制数据按照指定的编码格式转换成为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
s1 = '你'.encode('utf8')  # 将字符 你 按照utf8格式编码称为二进制
print(type(s1)) # <class 'bytes'>
print(s1) # b'\xe4\xbd\xa0'

s2 = s1.decode('utf8') # 将二进制按照utf8格式解码称为字符串
print(s2)

s3 = '你'.encode('gbk') # 将字符 你 按照gbk格式转换称为二进制
print(s3) # b'\xc4\xe3'

s4 = s3.decode('gbk') # 将二进制按照gbk格式解码称为字符
print(s4)

思考:文字产生乱码的原因以及解决方案。

列表

列表的基本使用

一、列表的格式

定义列的格式:[元素1, 元素2, 元素3, ..., 元素n]

变量tmp的类型为列表

1
tmp = ['xiaoWang',180, 65.0]

列表中的元素可以是不同类型的

二、使用下标获取列表元素

1
2
3
4
namesList = ['xiaoWang','xiaoZhang','xiaoHua']
print(namesList[0])
print(namesList[1])
print(namesList[2])

结果:

1
2
3
xiaoWang
xiaoZhang
xiaoHua

列表的增删改查

我们对于可变数据(例如,列表,数据库等)的操作,一般包含增、删、改、查四个方面。

一、添加元素

添加元素有一下几个方法:

  • append 在末尾添加元素
  • insert 在指定位置插入元素
  • extend 合并两个列表

append

append会把新元素添加到列表末尾

1
2
3
4
5
6
7
8
9
10
11
#定义变量A,默认有3个元素
A = ['xiaoWang','xiaoZhang','xiaoHua']

print("-----添加之前,列表A的数据-----A=%s" % A)

#提示、并添加元素
temp = input('请输入要添加的学生姓名:')
A.append(temp)

print("-----添加之后,列表A的数据-----A=%s" % A)

insert

insert(index, object) 在指定位置index前插入元素object

1
2
3
4
strs = ['a','b','m','s']
strs.insert(3,'h')
print(strs) # ['a', 'b', 'm', 'h', 's']

extend

通过extend可以将另一个集合中的元素逐一添加到列表中

1
2
3
4
5
6
a = ['a','b','c']
b = ['d','e','f']
a.extend(b)
print(a) # ['a', 'b', 'c', 'd', 'e', 'f'] 将 b 添加到 a 里
print(b) # ['d','e','f'] b的内容不变

PS:python中的+号可以用在那些方面

  1. 数字:n = 1 + 2
  2. 字符串:s = ‘aa’ + ‘bb’
  3. 列表:list = [1,2,3] + [‘a’,’b’,’c’] = [1,2,3,’a’,’b’,’c’]

二、修改元素

我们是通过指定下标来访问列表元素,因此修改元素的时候,为指定的列表下标赋值即可。

1
2
3
4
5
6
7
8
9
10
#定义变量A,默认有3个元素
A = ['xiaoWang','xiaoZhang','xiaoHua']

print("-----修改之前,列表A的数据-----A=%s" % A)

#修改元素
A[1] = 'xiaoLu'

print("-----修改之后,列表A的数据-----A=%s" % A)

三、查找元素

所谓的查找,就是看看指定的元素是否存在,以及查看元素所在的位置,主要包含一下几个方法:

  • in 和 not in
  • index 和 count

in, not in

python中查找的常用方法为:

  • in(存在),如果存在那么结果为true,否则为false
  • not in(不存在),如果不存在那么结果为true,否则false
1
2
3
4
5
6
7
8
9
10
11
#待查找的列表
nameList = ['xiaoWang','xiaoZhang','xiaoHua']

#获取用户要查找的名字
findName = input('请输入要查找的姓名:')

#查找是否存在
if findName in nameList:
print('在列表中找到了相同的名字')
else:

结果1:(找到)

结果2:(没有找到)

说明:

in的方法只要会用了,那么not in也是同样的用法,只不过not in判断的是不存在

index, count

index用来查找元素所在的位置,如果未找到则会报错;count用来计算某个元素出现的次数。它们的使用和字符串里的使用效果一致。

1
2
3
4
5
6
7
8
9
10
11
>>> a = ['a', 'b', 'c', 'a', 'b']
>>> a.index('a', 1, 3) # 注意是左闭右开区间
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'a' is not in list
>>> a.index('a', 1, 4)
3
>>> a.count('b')
2
>>> a.count('d')
0

四、删除元素

类比现实生活中,如果某位同学调班了,那么就应该把这个条走后的学生的姓名删除掉;在开发中经常会用到删除这种功能。

列表元素的常用删除方法有:

  • del:根据下标进行删除
  • pop:删除最后一个元素:从后往前删除
  • remove:根据元素的值进行删除

del

1
2
3
4
movieName = ['加勒比海盗','骇客帝国','第一滴血','指环王','霍比特人','速度与激情']
print('------删除之前------movieName=%s' % movieName)
del movieName[2]
print('------删除之后------movieName=%s' % movieName)

pop

1
2
3
4
5
6
7
movieName = ['加勒比海盗','骇客帝国','第一滴血','指环王','霍比特人','速度与激情']
print('------删除之前------movieName=%s' % movieName)
movieName.pop()
print('------删除之后------movieName=%s' % movieName)

movieName.pop(-1)#['加勒比海盗','骇客帝国','第一滴血','指环王','霍比特人']
movieName.pop(8)#报错

remove

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
movieName = ['加勒比海盗','骇客帝国','第一滴血','指环王','霍比特人','速度与激情']
print('------删除之前------movieName=%s' % movieName)
movieName.remove('指环王')
print('------删除之后------movieName=%s' % movieName)

list1 = ['火腿','酸奶','酸奶','辣条','薯条','面包','薯条','酸奶','酸奶']
for i in list1:
if i == '酸奶':
list1.remove(i)
print(list1)#['火腿', '辣条', '薯条', '面包', '薯条', '酸奶', '酸奶']
#结果发现如果有连续的元素时会出现漏删操作,因为删除后列表元素减一,但是for循环的下标不变,继续进行,所以跳过了下一个元素。
#优化:
n = 0
while n < len(list1):
if list1[n] == '酸奶':
list1.remove('酸奶')
else:
n+=1

#错误写法
#for i in range(len(list1)):
# if list1[i] == '酸奶':
# list1.remove('酸奶')
# i -= 1
# print(list1)

#正确写法1
result_li = []
for i in list1:
if i != elem:
result_li.append(i)
list1 = result_li
print(li)

#正确写法2
for i in list1[::-1]:
if i == '酸奶':
list1.remove(i)#remove从左往右寻找删除
print(list1)
  1. 如果参数是不存在的一个字符串会报错。所以remove使用前可以先用in或者not in判断。
  2. 如果列表中中有多个同名的元素,remove只会删除第一个。如果想都删掉则需要循环删除。

五、排序(sort, reverse)

sort方法是将list按特定顺序重新排列,默认为由小到大,参数reverse=True可改为倒序,由大到小。

reverse方法是将list逆置。

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = [1, 4, 2, 3]
>>> a
[1, 4, 2, 3]
>>> a.reverse() # 逆置,不排序
>>> a
[3, 2, 4, 1]
>>> a.sort() # 默认从小到大排序
>>> a
[1, 2, 3, 4]
>>> a.sort(reverse=True) # 从大到小排序
>>> a
[4, 3, 2, 1]

练习

请删除列表 words = ['hello','',','good','hi','','yes','','no'] 里所有的空字符串。

列表的遍历

1. 使用while循环

为了更有效率的输出列表的每个数据,可以使用循环来完成

1
2
3
4
5
6
7
namesList = ['xiaoWang','xiaoZhang','xiaoHua']
length = len(namesList) # 获取列表长度
i = 0
while i<length:
print(namesList[i])
i+=1
Copy

结果:

1
2
3
4
xiaoWang
xiaoZhang
xiaoHua
Copy

2. 使用for循环

while 循环是一种基本的遍历列表数据的方式,但是最常用也是最简单的方式是使用 for 循环

1
2
3
4
namesList = ['xiaoWang','xiaoZhang','xiaoHua']
for name in namesList:
print(name)
Copy

结果:

1
2
3
4
xiaoWang
xiaoZhang
xiaoHua
Copy

3. 交换2个变量的值

1
2
3
4
5
6
7
8
9
10
11
12
# 使用中间变量
a = 4
b = 5
c = 0

c = a
a = b
b = c

print(a)
print(b)
Copy

练习

  1. 手动实现冒泡排序(难)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    nums = [5, 1, 7, 6, 8, 2, 4, 3]

    for j in range(0, len(nums) - 1):
    for i in range(0, len(nums) - 1 - j):
    if nums[i] > nums[i + 1]:
    a = nums[i]
    nums[i] = nums[i+1]
    nums[i+1] = a

    print(nums)
    Copy
  2. 有一个列表names,保存了一组姓名names=['zhangsan','lisi','chris','jerry','henry'],再让用户输入一个姓名,如果这个姓名在列表里存在,提示用户姓名已存在;如果这个姓名在列表里不存在,就将这个姓名添加到列表里。

列表的嵌套

列表嵌套

类似while循环的嵌套,列表也是支持嵌套的

一个列表中的元素又是一个列表,那么这就是列表的嵌套

此处重点掌握怎么操作被嵌套的列表

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> schoolNames = [
... [1, 2, 3],
... [11, 22, 33],
... [111, 222, 333]
... ]
>>> schoolNames[1][2] # 获取数字 33
33
>>> schoolNames[1][2] = 'abc' # 把 33 修改为 'abc'
>>> schoolNames
[[1, 2, 3], [11, 22, 'abc'], [111, 222, 333]]
>>> schoolNames[1][2][2] # 获取 'abc' 里的字符c
'c'
Copy

也就是说,操作嵌套列表,只要把要操作元素的下标当作变量名来使用即可。

应用

一个学校,有3个办公室,现在有8位老师等待工位的分配,请编写程序,完成随机的分配

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

# 定义一个列表用来保存3个办公室
offices = [[],[],[]]

# 定义一个列表用来存储8位老师的名字
names = ['A','B','C','D','E','F','G','H']

i = 0
for name in names:
index = random.randint(0,2)
offices[index].append(name)

i = 1
for tempNames in offices:
print('办公室%d的人数为:%d'%(i,len(tempNames)))
i+=1
for name in tempNames:
print("%s"%name,end='')
print("\n")
print("-"*20)
Copy

运行结果如下:

image

列表推导式

所谓的列表推导式,就是指的轻量级循环创建列表

基本方式

image

1
list1 = [i*2 for i in range(1,10,2)]

在循环的过程中使用if

image

image

2个for循环

image

3个for循环

image

列表的复制

查看以下代码,说出打印的结果。

1
2
3
4
5
6
7
8
9
10
11
12
a = 12
b = a
b = 13
print(b)
print(a)

nums1 = [1, 5, 8, 9, 10, 12]
nums2 = nums1
nums2[0] = 100
print(nums2)
print(nums1)
Copy

思考:

  1. 为什么修改了 nums2里的数据,nums1的数据也会改变?

Python中的赋值运算都是引用(即内存地址)的传递。对于可变类型来说,修改原数据的值,会改变赋值对象的值。

  1. 怎样nums1和nums2变成两个相互独立不受影响的列表?

使用列表的 copy 方法,或者 copy 模块就可以赋值一个列表。

列表的copy方法

使用列表的copy方法,可以直接将原来的列表进行复制,变成一个新的列表,这种复制方式是浅复制。

1
2
3
4
5
6
7
8
9
nums1 = [1, 5, 8, 9, 10, 12]
nums2 = nums1.copy() # 调用列表的copy方法,可以复制出一个新的列表

nums2[0] = 100

# 修改新列表里的数据,不会影响到原有列表里的数据
print(nums2)
print(nums1)
Copy

copy模块的使用

除了使用列表的copy方法以外,Python还提供了copy模块来复制一个对象。copy模块提供了浅复制和深复制两种方式,它们的使用方式相同,但是执行的效果有一定的差异。

浅拷贝

浅拷贝是对于一个对象的顶层拷贝,通俗的理解是:拷贝了引用,并没有拷贝内容。

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

words1 = ['hello', 'good', ['yes', 'ok'], 'bad']

# 浅拷贝只会拷贝最外层的对象,里面的数据不会拷贝,而是直接指向
words2 = copy.copy(words1)

words2[0] = '你好'
words2[2][0] = 'no'

print(words1) # ['hello', 'good', ['no', 'ok'], 'bad']
# wrods2 里的 yes 被修改成了 no
print(words2) # ['你好', 'good', ['no', 'ok'], 'bad']
Copy

深拷贝

深拷贝是对于一个对象所有层次的递归拷贝。

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

words1 = ['hello', 'good', ['yes', 'ok'], 'bad']

# 深拷贝会将对象里的所有数据都进行拷贝
words2 = copy.deepcopy(words1)

words2[0] = '你好'
words2[2][0] = 'no'

print(words1) # ['hello', 'good', ['yes', 'ok'], 'bad']
print(words2) # ['你好', 'good', ['no', 'ok'], 'bad']
Copy

切片

列表和字符串一样,也支持切片,切片其实就是一种浅拷贝。

1
2
3
4
5
6
words1 = ['hello', 'good', ['yes', 'ok'], 'bad']
words2 = words1[:]
words2[0] = '你好'
words2[2][0] = 'no'
print(words1) # ['hello', 'good', ['no', 'ok'], 'bad']
print(words2) # ['你好', 'good', ['no', 'ok'], 'bad']

元组、字典、集合

元组

Python的元组与列表类似,不同之处在于元组的元素不能修改。元组使用小括号,列表使用方括号。

1
2
3
4
>>> aTuple = ('et',77,99.9)
>>> aTuple
('et',77,99.9)
Copy

一、访问元组

image

二、修改元组

image

说明: python中不允许修改元组的数据,包括不能删除其中的元素。

三、count, index

index和count与字符串和列表中的用法相同

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = ('a', 'b', 'c', 'a', 'b')
>>> a.index('a', 1, 3) # 注意是左闭右开区间
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: tuple.index(x): x not in tuple
>>> a.index('a', 1, 4)
3
>>> a.count('b')
2
>>> a.count('d')
0
Copy

四、定义只有一个数据的元组

定义只有一个元素的元组,需要在唯一的元素后写一个逗号

1
2
3
4
5
6
7
8
9
10
11
>>> a = (11)
>>> a
11
>>> type(a)
int
>>> a = (11,) # 只有一个元素的元组,必须要在元素后写一个逗号
>>> a
(11,)
>>> type(a)
tuple
Copy

五、交换两个变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第1种方式,使用中间变量
a = 4
b = 5
c = 0

c = a
a = b
b = c
print(a)
print(b)

# 第2种方式,直接交换。
a, b = 4, 5
a, b = b, a

print(a)
print(b)

字典

列表的缺点

当存储的数据要动态添加、删除的时候,我们一般使用列表,但是列表有时会遇到一些麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
# 定义一个列表保存,姓名、性别、职业
nameList = ['xiaoZhang', '男', '木匠'];

# 当修改职业的时候,需要记忆元素的下标
nameList[2] = '铁匠'

# 如果列表的顺序发生了变化,添加年龄
nameList = ['xiaoWang', 18, '男', '铁匠']

# 此时就需要记忆新的下标,才能完成名字的修改
nameList[3] = 'xiaoxiaoWang'
Copy

有没有方法,既能存储多个数据,还能在访问元素的很方便就能够定位到需要的那个元素呢?

答:

字典

字典的使用

定义字典的格式:**{键1:值1, 键2:值2, 键3:值3, …, 键n:值n}**

变量info为字典类型:

1
2
info = {'name':'班长', 'id':100, 'sex':'f', 'address':'地球亚洲中国上海'}
info['name'] # 字典使用键来获取对应的值

Python中的字典是一种可变的数据结构,可以用于存储键值对。下面是一些常用的字典操作:

1.创建字典

使用大括号{}或者dict()函数创建一个空的字典:

1
2
my_dict = {}
my_dict = dict()

使用大括号{}创建包含键值对的字典:

1
my_dict = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}

2.添加或修改键值对

通过指定键来添加或修改对应的值:

1
2
my_dict['key4'] = 'value4'
my_dict['key1'] = 'new_value1'

PS:setdefault()函数

字典的setdefault()函数是一种用于向字典中添加键值对的方法。如果字典中已经存在指定的键,则setdefault()函数会返回该键的值,否则会将键值对添加到字典中。

setdefault()函数的语法如下:

1
dict.setdefault(key, default_value)

其中,key是要查找或添加的键,default_value是可选参数,表示如果指定的键不存在时要添加的默认值。如果没有指定default_value,则默认为None。

setdefault()函数的作用如下:

  • 如果字典中已经存在指定的键,则返回该键对应的值。
  • 如果字典中不存在指定的键,则向字典中添加一个新的键值对,并将默认值作为该键的值返回。

例如,以下代码演示了如何使用setdefault()函数:

1
2
3
4
5
6
7
>>> d = {'a': 1, 'b': 2}
>>> d.setdefault('a', 3)
1
>>> d.setdefault('c', 3)
3
>>> print(d)
{'a': 1, 'b': 2, 'c': 3}

在上面的示例中,字典d已经存在键’a’,因此第一个setdefault()调用返回1。而字典d中不存在键’c’,因此第二个setdefault()调用将添加一个新的键值对{‘c’: 3}到字典中,并返回默认值3。最终,字典d中包含三个键值对。

3.删除键值对

使用del关键字删除指定的键值对:

1
del my_dict['key3']

使用pop()方法删除指定的键值对,并返回对应的值:

1
value = my_dict.pop('key2')

使用popitem()方法从后往前删除一个键值对,并返回对应的键和值(返回的是一个元组):

1
key, value = my_dict.popitem()

4.访问字典中的值

通过指定键来访问对应的值:

1
value = my_dict['key1']

使用get()方法访问指定键对应的值,如果键不存在则返回指定的默认值:

1
value = my_dict.get('key2', 'default_value')

使用keys()方法返回字典中所有的

1
keys = my_dict.keys()#dict_keys(['name', 'id', 'sex', 'address'])

使用values()方法返回字典中所有的

1
values = my_dict.values()#dict_values(['班长', 100, 'f', '地球亚洲中国上海'])

使用items()方法返回字典中所有的键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
items = my_dict.items()#dict_items([('name', '班长'), ('id', 100), ('sex', 'f'), ('address', '地球亚洲中国上海')])

for i in items:
print(i)
'''
('name', '班长')
('id', 100)
('sex', 'f')
('address', '地球亚洲中国上海')
'''

for k,v in items:
print(k,v)
'''
name 班长
id 100
sex f
address 地球亚洲中国上海
'''

5.其他操作

1.使用len()函数获取字典中键值对的数量:

1
length = len(my_dict)

2.使用in关键字检查指定的键是否存在于字典中:

1
2
if 'key1' in my_dict:
print('Key exists in the dictionary')

3.使用clear()方法清空字典中所有的键值对:

1
my_dict.clear()

4.使用update方法合并字典

它用于将一个字典中的键值对添加到另一个字典中。

update() 方法的语法如下:

1
dict.update([other])

其中,other 是要添加到当前字典中的字典或键值对序列。如果 other 是字典,则将其键值对添加到当前字典中。如果 other 是键值对序列,则将其作为一个列表添加到当前字典中。如果键已经存在,则将其对应的值更新为新的值。

例如,假设我们有两个字典 dict1dict2

1
2
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}

现在我们想将 dict2 中的键值对添加到 dict1 中,可以使用 update() 方法:

1
2
dict1.update(dict2)
print(dict1)

输出结果为:

1
{'a': 1, 'b': 3, 'c': 4}

注意,update() 方法会修改原字典 dict1,并返回 None。如果要创建一个新的字典而不修改原字典,则可以使用以下语句:

1
2
new_dict = dict1.copy()
new_dict.update(dict2)

5.fromkeys方法

fromkeys()是一个dict类方法,用于创建一个新的字典,其中包含指定键列表的键和所有值为默认值None(或指定的默认值)。fromkeys()方法的语法如下:

1
dict.fromkeys(seq[, value])

其中,seq参数是指定的键列表,可以是一个可迭代对象,例如一个列表、元组或集合。value参数是可选的,它是指定的默认值。如果省略了value参数,则默认值为None

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
keys = ['a', 'b', 'c']

my_dict = dict.fromkeys(keys)
print(my_dict)
# Output: {'a': None, 'b': None, 'c': None}

my_dict = dict.fromkeys(keys,10)
print(my_dict)
# Output: {'a': 10, 'b': 10, 'c': 10}

my_dict = dict.fromkeys(keys,[10,20])
print(my_dict)
# Output: {'a': [10,20], 'b': [10,20], 'c': [10,20]}

在上面的示例中,我们创建了一个包含三个键的字典,并使用fromkeys()方法将它们初始化为None

说明:

  • 字典和列表一样,也能够存储多个数据
  • 列表中找某个元素时,是根据下标进行的;字典中找某个元素时,是根据’名字’(就是冒号:前面的那个值,例如上面代码中的’name’、’id’、’sex’)
  • 字典的每个元素由2部分组成,键:值。例如 ‘name’:’班长’ ,’name’为键,’班长’为值
  • 键可以使用数字、布尔值、布尔值、元组等不可变数据类型,但是一般习惯使用字符串
  • 每个字典里的key都是唯一的,如果出现了多个key,后面的value会覆盖前一个key对应的value.

在习惯上:

  • 列表更适合保存多个商品、多个姓名、多个时间,这样的相似数据;
  • 字典更适合保存一个商品的不同信息、一个人的不同信息,这样的不同数据。

字典的遍历

for循环遍历

您可以使用for循环遍历字典中的所有键值对。以下是一个示例代码,它使用for循环遍历字典,并打印出每个键值对:

1
2
3
4
my_dict = {"apple": 3, "banana": 5, "orange": 2}

for key, value in my_dict.items():
print(key, value)

在上面的代码中,my_dict.items()方法返回一个包含字典中所有键值对的列表,然后for循环迭代该列表,并将每个键值对的键和值分别赋值给keyvalue变量。然后,print函数用于打印出每个键值对。

集合

set的使用

集合(set)是一个无序的不重复元素序列,可以使用大括号 { } 或者 set() 函数创建集合。

注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。

创建格式:

1
2
3
parame = {value01,value02,...}
#或者
set(value)

添加元素

语法格式如下:

1
s.add(x)

将元素 x 添加到集合 s 中,如果元素已存在,则不进行任何操作。

1
2
3
4
>>>thisset = set(("Google", "Runoob", "Taobao"))
>>> thisset.add("Facebook")
>>> print(thisset)
{'Taobao', 'Facebook', 'Google', 'Runoob'}

还有一个方法,也可以添加元素,且参数可以是列表,元组,字典等,语法格式如下:

1
s.update( x )

x 可以有多个,用逗号分开。

1
2
3
4
5
6
7
>>>thisset = set(("Google", "Runoob", "Taobao"))
>>> thisset.update({1,3})
>>> print(thisset)
{1, 3, 'Google', 'Taobao', 'Runoob'}
>>> thisset.update([1,4],[5,6])
>>> print(thisset)
{1, 3, 4, 5, 6, 'Google', 'Taobao', 'Runoob'}

移除元素

语法格式如下:

1
s.remove( x )

将元素 x 从集合 s 中移除,如果元素不存在,则会发生错误。

1
2
3
4
5
6
7
8
9
>>>thisset = set(("Google", "Runoob", "Taobao"))
>>> thisset.remove("Taobao")
>>> print(thisset)
{'Google', 'Runoob'}
>>> thisset.remove("Facebook") # 不存在会发生错误
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'Facebook'
>>>

此外还有一个方法也是移除集合中的元素,且如果元素不存在,不会发生错误。格式如下所示:

1
2
3
4
5
6
s.discard( x )
Copy
>>>thisset = set(("Google", "Runoob", "Taobao"))
>>> thisset.discard("Facebook") # 不存在不会发生错误
>>> print(thisset)
{'Taobao', 'Google', 'Runoob'}

我们也可以设置随机删除集合中的一个元素,语法格式如下:

1
2
3
4
5
6
7
s.pop()
Copy
thisset = set(("Google", "Runoob", "Taobao", "Facebook"))
x = thisset.pop()

print(x)
print(thisset)

set常见方法列表

方法 描述
add() 为集合添加元素
clear() 移除集合中的所有元素
copy() 拷贝一个集合
pop() 随机移除元素
remove() 移除指定元素
union 返回两个集合的并集
update() 给集合添加元素
difference() 返回多个集合的差集
difference_update() 移除集合中的元素,该元素在指定的集合也存在。
discard() 删除集合中指定的元素
intersection() 返回集合的交集
intersection_update() 删除集合中的元素,该元素在指定的集合中不存在。
isdisjoint() 判断两个集合是否包含相同的元素,如果没有返回 True,否则返回 False。
issubset() 判断指定集合是否为该方法参数集合的子集。
issuperset() 判断该方法的参数集合是否为指定集合的子集
symmetric_difference() 返回两个集合中不重复的元素集合。
symmetric_difference_update() 移除当前集合中在另外一个指定集合相同的元素,并将另外一个指定集合中不同的元素插入到当前集合中。

练习

有一个无序且元素数据重复的列表nums, nums=[5,8,7,6,4,1,3,5,1,8,4],要求对这个列表里的元素去重,并进行降序排序。

1
2
3
4
5
6
7
# 方法一:调用列表的sort方法
nums2 = list(set(nums))
nums2.sort(reverse=True)
print(nums2)

# 方法二:使用sorted内置函数
print(sorted(list(set(nums)),reverse=True))

转换相关

执行字符串

使用Python内置的eval函数,可以执行字符串里的Python代码。使用这种方式,可以将字符串转换成为其他类型的数据。

1
2
3
4
5
6
7
8
9
10
11
x = '1+1'
print(eval(x)) # 2
print(type(eval(x))) # <class 'int'>

y = '{"name":"zhangsan","age":18}'
print(eval(y))
print(type(eval(y))) # <class 'dict'>

print(eval('1 > 2')) # False

eval('input("请输入您的姓名:")')

转换成为字符串

JSON(JavaScriptObjectNotation, JS对象简谱)是一种轻量级的数据交换格式,它基于 ECMAScript 的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。JSON本质是一个字符串

JSON的功能强大,使用场景也非常的广,目前我们只介绍如何使用Python的内置JSON模块,实现字典、列表或者元组与字符串之间的相互转换。

使用json的dumps方法,可以将字典、列表或者元组转换成为字符串。

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

person = {'name': 'zhangsan', 'age': 18}
x = json.dumps(person)
print(x) # {"name": "zhangsan", "age": 18}
print(type(x)) # <class 'str'>

nums = [1, 9, 0, 4, 7]
y = json.dumps(nums)
print(y) # [1, 9, 0, 4, 7]
print(type(y)) # <class 'str'>

words = ('hello','good','yes')
z = json.dumps(words)
print(z) # ["hello", "good", "yes"]
print(type(z)) # <class 'str'>

使用json的loads方法,可以将格式正确的字符串转换成为字典、列表。

1
2
3
4
5
6
7
8
9
x = '{"name": "zhangsan", "age": 18}'
person = json.loads(x)
print(person) # {'name': 'zhangsan', 'age': 18}
print(type(person)) # <class 'dict'>

y = '[1, 9, 0, 4, 7]'
nums = json.loads(y)
print(nums) # [1, 9, 0, 4, 7]
print(type(nums)) # <class 'list'>

通用方法

字符串、列表、元组、字典和集合,它们有很多相同点,都是由多个元素组合成的一个可迭代对象,它们都有一些可以共同使用的方法。

算数运算符

在Python里,常见的算数运算符,有一些可以使用于可迭代对象,它们执行的结果也稍有区别。

运算符 Python 表达式 结果 描述 支持的数据类型
+ [1, 2] + [3, 4] [1, 2, 3, 4] 合并 字符串、列表、元组
- {1,2,3,4} - {2,3} {1,4} 集合求差集 集合
* [‘Hi!’] * 4 [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] 复制 字符串、列表、元组
in 3 in (1, 2, 3) True 元素是否存在 字符串、列表、元组、字典
not in 4 not in (1, 2, 3) True 元素是否不存在 字符串、列表、元组、字典

+

加法运算符可以用于字符串、列表和元组,用来拼接多个可迭代对象,不能用于字典和集合(思考:为什么字典和集合不能使用)。

1
2
3
4
5
6
>>> "hello " + "world"
'hello world'
>>> [1, 2] + [3, 4]
[1, 2, 3, 4]
>>> ('a', 'b') + ('c', 'd')
('a', 'b', 'c', 'd')

-

减法只能用于集合里,用来求两个集合的差集。

1
2
>>> {1, 6, 9, 10, 12, 3} - {4, 8, 2, 1, 3}
{9, 10, 12, 6}

*

加法运算符可以用于字符串、列表和元组,用来将可迭代对象重复多次,同样不能用于字典和集合。

1
2
3
4
5
6
>>> 'ab' * 4
'ababab'
>>> [1, 2] * 4
[1, 2, 1, 2, 1, 2, 1, 2]
>>> ('a', 'b') * 4
('a', 'b', 'a', 'b', 'a', 'b', 'a', 'b')

in

in和not in成员运算符可以用于所有的可迭代对象。但是需要注意的是,in 和 not in 在对字典进行判断时,是查看指定的key是否存在,而不是value.

1
2
3
4
5
6
7
8
>>> 'llo' in 'hello world'
True
>>> 3 in [1, 2]
False
>>> 4 in (1, 2, 3, 4)
True
>>> "name" in {"name":"chris", "age":18}
True

遍历

通过for … in … 我们可以遍历字符串、列表、元组、字典、集合等可迭代对象。

字符串遍历

1
2
3
4
5
>>> a_str = "hello world"
>>> for char in a_str:
... print(char,end=' ')
...
h e l l o w o r l d

列表遍历

1
2
3
4
5
>>> a_list = [1, 2, 3, 4, 5]
>>> for num in a_list:
... print(num,end=' ')
...
1 2 3 4 5

元组遍历

1
2
3
4
>>> a_turple = (1, 2, 3, 4, 5)
>>> for num in a_turple:
... print(num,end=" ")
1 2 3 4 5

带下标的遍历

可迭代对象都可以使用 enumerate 内置类进行包装成一个 enumerate 对象。对enumerate进行遍历,可以同时得到一个可迭代对象的下标和元素。

1
2
3
4
5
nums = [12, 9, 8, 5, 4, 7, 3, 6]

# 将列表 nums 包装成 enumerate 对象
for i, num in enumerate(nums): # i表示元素下标,num表示列表里的元素
print('第%d个元素是%d' % (i, num))

垃圾回收机制

java,python的垃圾回收机制对比

Python使用引用计数来跟踪对象的引用,当一个对象的引用计数变为0时,Python会立即释放该对象的内存。但是,引用计数并不能解决循环引用的问题,Python还使用垃圾收集器(Garbage Collector)来处理循环引用。Python的垃圾收集器使用标记-清除算法(Mark and Sweep Algorithm)来找到和清除无法访问的循环引用对象。

C++没有内置的垃圾回收机制,程序员需要手动管理内存,例如使用new和delete操作符来分配和释放内存。虽然C++有一些第三方库来帮助程序员管理内存,但是这些库的性能和效率并不如手动管理内存。

Java也有垃圾回收机制,但与Python不同的是,Java使用的垃圾回收机制是自动的。Java垃圾回收器在应用程序运行时自动回收无用的对象,从而避免了内存泄漏和野指针问题。Java的垃圾回收器使用标记-清除算法和复制算法(Copying Algorithm)来处理垃圾对象。

Python的垃圾回收机制对比

Python的垃圾回收机制主要使用两种算法:引用计数算法和标记-清除算法。

1.引用计数算法

引用计数算法是Python最基本的垃圾回收算法。在Python中,每个对象都有一个引用计数器,记录有多少个变量引用了这个对象。当引用计数器为0时,说明没有任何变量指向该对象,Python就会回收该对象的内存。

举例来说:

1
2
3
a = [1, 2, 3]
b = a #a给b赋值是把a的引用地址给了b,python的变量类似与指针
c = b

在这个例子中,a、b、c都指向同一个列表对象[1, 2, 3],它的引用计数为3。如果我们执行以下代码:

1
2
3
del a
del b
del c

当没有变量引用这个对象时,那么这个列表对象的引用计数就变为了0,Python就会回收该对象的内存。

2.标记-清除算法

引用计数算法只能处理没有循环引用的对象,当对象之间存在循环引用时,引用计数算法就无法正常工作了。这时Python就需要使用标记-清除算法来处理垃圾对象。

标记-清除算法分为两个阶段:标记阶段和清除阶段。

在标记阶段,Python会从根对象开始,遍历所有可达对象,并给它们打上标记。根对象可以是Python程序中所有全局变量、局部变量、以及当前线程的调用栈中的变量。

在清除阶段,Python会遍历所有未标记的对象,将它们回收掉。这些未标记的对象就是垃圾对象,可以被回收掉。

举例来说:

1
2
3
4
5
6
7
8
9
10
11
class A:
pass

class B:
pass

a = A()
b = B()

a.b = b
b.a = a

在这个例子中,对象a和b之间存在循环引用,无法使用引用计数算法回收它们的内存。此时Python会使用标记-清除算法来处理它们的内存回收。在标记阶段,Python会从根对象开始,遍历所有可达对象并打上标记,a和b都会被打上标记。在清除阶段,Python会回收未被标记的对象,这里没有未被标记的对象,所以a和b都会被保留下来,直到没有任何变量引用它们为止。

函数

  • 如果在开发程序时,需要某块代码多次执行。为了提高编写的效率以及更好的维护代码,需要把具有独立功能的代码块组织为一个小模块,这就是函数。

函数定义和调用

一、定义函数

定义函数的格式如下:

1
2
3
def 函数名():
代码

示例:

1
2
3
4
5
6
7
# 定义一个函数,能够完成打印信息的功能
def printInfo():
print('------------------------------------')
print(' 人生苦短,我用Python')
print('------------------------------------')

print(printInfo)#打印函数名返回的是函数地址

二、调用函数

定义了函数之后,就相当于有了一个具有某些功能的代码,想要让这些代码能够执行,需要调用它

调用函数很简单的,通过 函数名() 即可完成调用

1
2
# 定义完函数后,函数是不会自动执行的,需要调用它才可以
printInfo()

三、注意:

  • 函数定义好以后,函数体里的代码并不会执行,如果想要执行函数体里的内容,需要手动的调用函数。
  • 每次调用函数时,函数都会从头开始执行,当这个函数中的代码执行完毕后,意味着调用结束了。
  • 当然了如果函数中执行到了return也会结束函数。

四、练一练

要求:定义一个函数,能够计算两个数字之和,并且调用这个函数让它执行

  • 使用def定义函数
  • 编写完函数之后,通过 函数名() 进行调用

函数参数

python函数传递参数与C++,java函数传递参数的区别

在Python、C++和Java中,函数传递参数的基本概念是相似的,都是将值或者引用传递给函数。但是,这些编程语言在实现参数传递时有一些区别:

1.参数传递方式

  • Python:默认情况下,Python 采用传递对象引用的方式传递参数,即实参传递的是对象的引用,函数形参接收的是该引用的副本。在函数内部修改形参,也会影响到实参。
  • C++:C++中有两种参数传递方式,分别是传值调用和引用调用。传值调用是将实参的值拷贝一份传递给函数,函数内部对形参的修改不会影响到实参;而引用调用则是将实参的地址传递给函数,函数内部对形参的修改会影响到实参。
  • Java:Java采用传值调用的方式传递参数。与C++的传值调用类似,Java中也是将实参的值拷贝一份传递给函数,函数内部对形参的修改不会影响到实参。

2.参数类型

  • Python:Python的参数类型是动态的,不需要在函数定义时声明参数类型。
  • C++:C++的参数类型是静态的,需要在函数定义时声明参数类型。
  • Java:Java的参数类型也是静态的,需要在函数定义时声明参数类型。

3.默认参数

  • Python:Python支持在函数定义时为参数设置默认值,调用函数时可以不传递该参数。如果调用函数时传递了该参数,则使用传递的值覆盖默认值。
  • C++:C++也支持默认参数,但是必须在函数声明或定义时设置默认值,调用函数时可以不传递该参数。
  • Java:Java不支持默认参数。

4.参数数量

  • Python:Python支持可变数量的参数,包括可变长参数列表和关键字参数列表。
  • C++:C++不支持可变数量的参数,必须在函数定义时确定参数数量和类型。
  • Java:Java也不支持可变数量的参数,必须在函数定义时确定参数数量和类型。

总的来说,Python、C++和Java在函数参数传递方面有一些区别,但基本概念是相似的。在选择使用哪种语言时,需要根据具体情况来决定。

定义、调用带有参数的函数

定义一个add2num(a, b)函数,来计算任意两个数字之和:

1
2
3
4
5
def add2num(a, b):
c = a+b
print c

add2num(11, 22) # 调用带有参数的函数时,需要在小括号中,传递数据

注意点:

  • 在定义函数的时候,小括号里写等待赋值的变量名
  • 在调用函数的时候,小括号里写真正要进行运算的数据

用函数时参数的顺序

1
2
3
4
5
6
7
8
9
10
11
>>> def test(a,b):
... print(a,b)
...
>>> test(1,2) # 位置参数
1 2
>>> test(b=1,a=2) # 关键字参数
2 1
>>>
>>> test(b=1,2) # 关键字参数写在位置参数之前会导致出错
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

函数参数是列表

1
2
3
4
5
6
7
8
9
10
11
12
library = ["a","b","c","d","e","f"]
#形参
def add_book(bookname):
library.append(bookname)
print("引入成功")

def show_book(books):
for book in books:
print(book)
#调用
add_book('g')
show_book(library)

局部变量,全局变量

局部变量

  • 局部变量,就是在函数内部定义的变量
  • 其作用范围是这个函数内部,即只能在这个函数中使用,在函数的外部是不能使用的
  • 因为其作用范围只是在自己的函数内部,所以不同的函数可以定义相同名字的局部变量(打个比方,把你、我是当做成函数,把局部变量理解为每个人手里的手机,你可有个iPhone8,我当然也可以有个iPhone8了, 互不相关)
  • 局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储
  • 当函数调用时,局部变量被创建,当函数调用完成后这个变量就不能够使用了

全局变量

如果一个变量,既能在一个函数中使用,也能在其他的函数中使用,这样的变量就是全局变量

打个比方:有2个兄弟 各自都有手机,各自有自己的小秘密在手机里,不让另外一方使用(可以理解为局部变量);但是家里的电话是2个兄弟都可以随便使用的(可以理解为全局变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义全局变量
a = 100

def test1():
print(a) # 虽然没有定义变量a但是依然可以获取其数据

def test2():
print(a) # 虽然没有定义变量a但是依然可以获取其数据

# 调用函数
test1()
test2()
Copy

运行结果:

1
2
100
100

总结1:

  • 在函数外边定义的变量叫做全局变量
  • 全局变量能够在所有的函数中进行访问

全局变量和局部变量名字相同问题

看如下代码:

image

总结2:

  • 当函数内出现局部变量和全局变量相同名字时,函数内部中的 变量名 = 数据 此时理解为定义了一个局部变量,而不是修改全局变量的值

修改全局变量

函数中进行使用时可否进行修改呢?

代码如下:

image

总结3:

  • 如果在函数中出现global 全局变量的名字 那么这个函数中即使出现和全局变量名相同的变量名 = 数据 也理解为对全局变量进行修改,而不是定义局部变量
  • 如果在一个函数中需要对多个全局变量进行修改,那么可以一次性全部声明,也可以分开声明。
1
2
3
4
5
# 可以使用一次global对多个全局变量进行声明
global a, b
# 还可以用多次global声明都是可以的
# global a
# global b

查看所有的全局变量和局部变量

Python提供了两个内置函数globals()和locals()可以用来查看所有的全局变量和局部变量。

1
2
3
4
5
6
7
8
9
10
11
def test():
a = 100
b = 40
print(locals()) # {'a': 100, 'b': 40}

test()

x = 'good'
y = True
print(globals()) # {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x101710630>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/jiangwei/Desktop/Test/test.py', '__cached__': None, 'test': <function test at 0x101695268>, 'x': 'good', 'y': True}

函数参数详解

拆包和装包

问题:当需求比较大时,如下面无法满足要求

1
2
3
4
5
6
7
#求和
def get_sum(a,b):
r = a + b
print(r)

get_sum(1,2)#可行
get_sum(2,6,9)#不可行

*args

装包:

def demo(*args)此时装包

1
2
3
4
a,*b,c = 1,2,3,4,5,6
print(a)#1
print(b)#[2,3,4]
print(c)#5
1
2
3
4
5
6
def get_sum(*a):
print(a)#

get_sum(1,2)#(1,2)
get_sum(2,6,9)#(2,6,9)

拆包:

调用的时候拆包

函数(*list),函数(*tuple),函数(*set)

1
2
3
4
5
6
7
8
9
def get_sum(*args):
print(*args)#
s = 0
for i in args:
s += i
print(s)

re = [23,434,34,34,54,213,234]
get_sum(re)#报错

image

报错原因:args装包后是([23, 434, 34, 34, 54, 213, 234],)

image

解决方案:传参数时拆包后传入,即传入*re

1
2
3
4
5
6
7
8
9
def get_sum(*args):
print(*args)#
s = 0
for i in args:
s += i
print(s)

re = [23,434,34,34,54,213,234]
get_sum(*re)#参数加上*就是拆包

image

**kwargs

关键字参数,在函数调用的时候必须传递关键字参数,才可以将其转换成key:value,转到字典中。

装包

1
2
3
4
5
def show_book(**kwargs):
print(kwargs)

show_book()#{}
show_book(bookname='西游记',author='吴承恩',number=5)#{'bookname':'西游记','author':'吴承恩','number':5}

拆包

1
2
3
4
5
6
7
8
9
10
11
12
def show_book(**kwargs):
print(kwargs) #{}
for k,v in kwargs.items():
print(k,v)

book = {'bookname':'西游记','author':'吴承恩','number':5}
show_book(**book)
'''
bookname 西游记
author 吴承恩
number 5
'''
1
2
3
4
5
6
def show_book(*args,**kwargs):
print(args)
print(kwargs)

book = {'bookname':'坏小孩','author':'zzz','number':5}
show_book('龙少','小芳',**book)

一、缺省参数

调用函数时,缺省参数的值如果没有传入,则取默认值。

下例会打印默认的age,如果age没有被传入:

1
2
3
4
5
6
7
8
9
def printinfo(name, age=35):
# 打印任何传入的字符串
print("name: %s" % name)
print("age %d" % age)

# 调用printinfo函数
printinfo(name="miki") # 在函数执行过程中 age取默认值35
printinfo(age=9 ,name="miki")

以上实例输出结果:

1
2
3
4
5
name: miki
age: 35
name: miki
age: 9

总结:

  • 在形参中默认有值的参数,称之为缺省参数

  • 注意:带有默认值的参数一定要位于参数列表的最后面

    1
    2
    3
    4
    5
    >>> def printinfo(name, age=35, sex):
    ... print name
    ...
    File "<stdin>", line 1
    SyntaxError: non-default argument follows default argument

二、不定长参数

有时可能需要一个函数能处理比当初声明时更多的参数, 这些参数叫做不定长参数,声明时不会命名。

基本语法如下:

1
2
3
4
def functionname([formal_args,] *args, **kwargs):
"""函数_文档字符串"""
function_suite
return [expression]

注意

  • 加了星号*的变量args会存放所有未命名的变量参数,args为元组
  • 而加**的变量kwargs会存放命名参数,即形如key=value的参数, kwargs为字典.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def test(a, b, *args, **kwargs):
"函数在声明时,需要两个参数"
print('a={},b={},args={},kwargs={}'.format(a,b,args,kwargs))

test(2, 3, '你好', 'hi', 'how do you do', name="zhangsan", age=18)
# a=2,b=3,args=('你好', 'hi', 'how do you do'),kwargs={'name': 'zhangsan', 'age': 18}


b = ('hi', '大家好', '今天天气真好')
d = {'name': "zhangsan", "age": 19}

# 注意,在传入参数时的星号问题。
test(10, 20, *b, **d)
# a=10,b=20,args=('hi', '大家好', '今天天气真好'),kwargs={'name': 'zhangsan', 'age': 19}


# 如果在传值时,不使用星号,会把后面的参数当做 args
test(10,20,b,d)
# a=10,b=20,args=(('hi', '大家好', '今天天气真好'), {'name': 'zhangsan', 'age': 19}),kwargs={}

三、缺省参数在*args后面

1
2
3
4
5
6
7
8
9
def sum_nums_3(a, *args, b=22, c=33, **kwargs):
print(a)
print(b)
print(c)
print(args)
print(kwargs)

sum_nums_3(100, 200, 300, 400, 500, 600, 700, b=1, c=2, mm=800, nn=900)

说明

  • 如果很多个值都是不定长参数,那么这种情况下,可以将缺省参数放到 *args的后面, 但如果有kwargs的话,kwargs必须是最后的

四、引用,函数参数引用

在 Python 中,引用是指将变量名和对象之间的关系。当您使用变量名引用对象时,实际上是引用该对象的内存地址,而不是对象本身。因此,当您将一个变量赋值给另一个变量时,两个变量都指向相同的内存地址,因此它们引用相同的对象。

例如,考虑以下代码:

1
2
a = [1, 2, 3]
b = a

在这里,变量 ab 都是对列表 [1, 2, 3] 的引用,它们指向相同的内存地址。如果您更改其中一个变量的值,则另一个变量也会受到影响,因为它们引用相同的对象:

1
2
a.append(4)
print(b) # 输出 [1, 2, 3, 4]

因此,在 Python 中,理解引用非常重要,因为它们可以使您在代码中更有效地使用内存和处理对象。

如果函数传入的是可变类型则实参有可能改变也有可能不变,但是如果传入的是不可变对象则函数体内对形参的修改不会导致实参的变化。

在 Python 中,函数可以修改可变类型的实参,但无法修改不可变类型的实参。

可变类型的实参包括列表、字典和集合等,这些类型的对象是可以在原地进行修改的。因此,如果您将一个可变类型的实参传递给一个函数,并在函数中修改它,则这些更改也将影响原始对象。

例如,以下代码演示了如何在函数中修改列表类型的实参:

1
2
3
4
5
6
def append_item(lst, item):
lst.append(item)

my_list = [1, 2, 3]
append_item(my_list, 4)
print(my_list) # 输出 [1, 2, 3, 4]

在这里,append_item 函数将一个元素附加到传递的列表 lst 中。由于 my_list 是一个可变的列表对象,因此 append_item 函数可以在原地修改它。

不可变类型的实参包括整数、字符串和元组等,这些类型的对象是无法在原地进行修改的。因此,如果您将一个不可变类型的实参传递给一个函数,并在函数中尝试修改它,则会引发一个错误。

例如,以下代码演示了试图修改字符串类型的实参将引发的错误:

1
2
3
4
5
def capitalize_string(s):
s[0] = s[0].upper()

my_string = "hello"
capitalize_string(my_string) # 引发 TypeError 错误

在这里,capitalize_string 函数试图将传递的字符串 s 的第一个字符大写化。但是,由于字符串是不可变的对象,因此无法在原地进行修改,这将导致 TypeError 错误。

总之,您可以在 Python 中通过修改可变类型的实参来更改函数外部的对象,但是如果您试图修改不可变类型的实参,则将引发一个错误。

可变、不可变类型

image

总结

  • 所谓可变类型与不可变类型是指:数据能够直接进行修改,如果能直接修改那么就是可变,否则是不可变
  • 可变类型(修改数据,内存地址不会发生变化)有: 列表、字典、集合
  • 不可变类型(修改数据,内存地址必定发生变化)有: 数字、字符串、元组

函数返回值

python函数返回值与C++,java函数返回值的区别

Python、C++和Java都是面向对象编程语言,它们都支持函数返回值。但是,它们的函数返回值有一些区别。

在Python中,函数可以通过使用return语句返回一个值。如果函数没有使用return语句,则默认返回None对象。Python的函数可以返回任何类型的对象,包括字符串、数字、列表、元组、字典和自定义对象。

C++和Java中的函数返回值必须明确指定类型。在C++中,函数的返回值类型必须在函数声明或定义中指定。在Java中,函数返回值类型必须在函数声明中指定。函数可以返回任何基本数据类型或对象类型。

C++和Java还支持返回指针和引用。在C++中,可以返回指向任何类型的指针或引用。在Java中,可以返回任何对象的引用。

另外,C++和Java中的函数可以有多个返回值,这些返回值可以使用结构体或类封装在一起。在Python中,可以使用元组或字典返回多个值。

总的来说,虽然Python、C++和Java都支持函数返回值,但它们在返回值类型的指定和多返回值的处理上有所区别。

一、“返回值”介绍

现实生活中的场景:

我给儿子10块钱,让他给我买个冰淇淋。这个例子中,10块钱是我给儿子的,就相当于调用函数时传递到参数,让儿子买冰淇淋这个事情最终的目标,我需要让他把冰淇淋带回来,此时冰淇淋就是返回值

开发中的场景:

定义了一个函数,完成了获取室内温度,想一想是不是应该把这个结果给调用者,只有调用者拥有了这个返回值,才能够根据当前的温度做适当的调整

综上所述:

  • 所谓“返回值”,就是程序中函数完成一件事情后,最后给调用者的结果
  • 使用返回值的前提需求就是函数调用者想要在函数外使用计算结果

二、带有返回值的函数

想要在函数中把结果返回给调用者,需要在函数中使用return

如下示例:

1
2
3
def add2num(a, b):
c = a+b
return c # return 后可以写变量名

或者

1
2
def add2num(a, b):
return a+b # return 后可以写计算表达式

return后面可以是一个值,也可以是多个值,多个值时会将多个值封装到一个元组中,将元组作为整体返回。

1
2
3
4
5
def add2num(a, b):
return a,b # 返回一个元组(a,b)

a,b = add2num(1,2)
print(a,b)#1 2

三、保存函数的返回值

在本小节刚开始的时候,说过的“买冰淇淋”的例子中,最后儿子给你冰淇淋时,你一定是从儿子手中接过来 对么,程序也是如此,如果一个函数返回了一个数据,那么想要用这个数据,那么就需要保存

保存函数的返回值示例如下:

1
2
3
4
5
6
7
8
9
#定义函数
def add2num(a, b):
return a+b

#调用函数,顺便保存函数的返回值
result = add2num(100,98)

#因为result已经保存了add2num的返回值,所以接下来就可以使用了
print(result)

结果:

1
198

函数的文档说明

1.基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def test(a,b):
... "用来完成对2个数求和" # 函数第一行写一个字符串作为函数文档
... print("%d"%(a+b))
...
>>>
>>> test(11,22) # 函数可以正常调用
33
>>>
>>> help(test) # 使用 help 查看test函数的文档说明
Help on function test in module __main__:

test(a, b)
用来完成对2个数求和

2.高级使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_info(name: str, age: int):
"""
接收用户的名字和年龄,拼接一个字符串并返回

:param name: 接收一个名字
:param age: 接收用户的年龄,必须是 0-200 间的一个整数
:return: 返回拼接好的字符串
"""
return "我的名字叫 %s,今年是 %d 岁" % (name, age)


get_info("吴彦祖", 19)
get_info(520, 19) # 注意,形参上标注的类型只是提高代码的可读性,并不会限制实参的类型
help(get_info)

高阶函数、装饰器

递归函数

什么是递归函数

通过前面的学习知道一个函数可以调用其他函数。

如果一个函数在内部不调用其它的函数,而是自己本身的话,这个函数就是递归函数。

递归函数的作用

举个例子,我们来计算阶乘 n! = 1 * 2 * 3 * ... * n

解决办法1:使用循环来完成

1
2
3
4
5
6
7
8
def cal(num):
result,i = 1,1
while i <= num:
result *= i
i+= 1
return result

print(cal(3))

看阶乘的规律

1
2
3
4
5
6
1! = 1
2! = 2 × 1 = 2 × 1!
3! = 3 × 2 × 1 = 3 × 2!
4! = 4 × 3 × 2 × 1 = 4 × 3!
...
n! = n × (n-1)!

解决办法2:使用递归来实现

1
2
3
4
5
6
7
8
def factorial(num):
result = 1
if num == 1:
return 1
result = num * factorial(num -1)
return result
print(cal(3))

原理

image

  • 练习:使用递归实现斐波那契数列。1、1、2、3、5、8、13、21、34、……

匿名函数

用lambda关键词能创建小型匿名函数。这种函数得名于省略了用def声明函数的标准步骤。

lambda函数的语法只包含一个语句,如下:

1
lambda 参数列表: 运算表达式

如下实例:

1
2
3
4
5
sum = lambda arg1, arg2: arg1 + arg2

# 调用sum函数
print("Value of total : %d" % sum( 10, 20 ))
print("Value of total : %d" % sum( 20, 20 ))

以上实例输出结果:

1
2
Value of total :  30
Value of total : 40

Lambda函数能接收任何数量的参数但只能返回一个表达式的值

匿名函数可以执行任意表达式(甚至print函数),但是一般认为表达式应该有一个计算结果供返回使用。

python在编写一些执行脚本的时候可以使用lambda,这样可以接受定义函数的过程,比如写一个简单的脚本管理服务器。

应用场合

函数作为参数传递

1
2
3
4
5
6
7
8
9
10
>>> def fun(a, b, opt):
... print("a = " % a)
... print("b = " % b)
... print("result =" % opt(a, b))
...
>>> add = lambda x,y:x+y
>>> fun(1, 2, add) # 把 add 作为实参传递
a = 1
b = 2
result = 3

练习:

有一个列表

1
2
3
4
5
6
7
students = [
{'name': 'zhangsan', 'age': 18, 'score': 92},
{'name': 'lisi', 'age': 20, 'score': 90},
{'name': 'wangwu', 'age': 19, 'score': 95},
{'name': 'jerry', 'age': 21, 'score': 98},
{'name': 'chris', 'age': 17, 'score': 100},
]

要求,对上述列表里的数据按照score进行升序排序。

Python中使用函数作为参数的内置函数和类:

函数名或类名 功能 参数描述
sorted函数 用来将一个无序列表进行排序 函数参数的返回值规定按照元素的哪个属性进行排序
filter类 用来过滤一个列表里符合规定的所有元素,得到的结果是一个迭代器 函数参数的返回值指定元素满足的过滤条件
map类 将列表里的每一项数据都执行相同的操作,得到的结果是一个迭代器 函数参数用来指定列表里元素所执行的操作
reduce函数 对一个序列进行压缩运算,得到一个值。python3以后,这个方法被移到了functools模块 函数参数用来指定元素按照哪种方式合并

高阶函数

在Python中,函数其实也是一种数据类型。

1
2
3
def test():
return 'hello world'
print(type(test)) # <class 'function'>

函数对应的数据类型是 function,可以把它当做是一种复杂的数据类型。

既然同样都是一种数据类型,我们就可以把它当做数字或者字符串来处理。

定义一个变量指向函数

在Python中,我们还可以定义一个变量,让它来指向一个函数,相当于给函数起了一个别名。

1
2
3
4
5
6
7
8
def test():
return 'hello wrold'

fun = test # 定义了一个变量fun,让它指向了 test 这个函数
print(fun()) # 使用fun()可以直接调用test这个函数

print(id(fun)) # 1819677672040
print(id(test)) # 1819677672040

注意:在定义一个变量表示一个函数时,函数后面不能加括号!加括号表示的是调用这个函数。

1
2
3
4
5
6
7
8
def test():
return 'hello world'

result = test() # 这种写法是调用test函数,并把函数的返回值赋值给result变量
print(result()) # 这里会报错 TypeError: 'str' object is not callable

fun = test # 这种写法是给test函数起了一个别名,注意,这里的test后面不能加()
fun() # 可以使用别名调用这个函数

高阶函数

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,同样,我们还可以把一个函数当做另一个函数的返回值。这种函数的使用方式我们称之为高阶函数。

函数做为另一个函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
def test(age,action):
if age < 18:
print('您还没满十八岁,请退出')
action() # 把参数action直接当做一个函数来调用

def smoke():
print('我已经年满十八岁了,我想抽烟')

my_action = smoke # 定义一个变量my_action,让它指向smoke函数
test(21, my_action) # 将my_action传给 test 函数作为它的参数

test(21,smoke) # 还可以不再定义一个新的变量,直接传入函数名

函数作为另一个函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
def test():
print('我是test函数里输入的内容')

def demo():
print('我是demo里输入的内容')
return test # test 函数作为demo函数的返回值

result = demo() # 我是demo里输入的内容 调用 demo 函数,把demo函数的返回值赋值给 result
print(type(result)) # <class 'function'> result 的类型是一个函数

result() # 我是demo里输入的内容 我是test函数里输入的内容 既然result是一个函数,那么就可以直接使用() 调用这个函数

demo()() # 我是demo里输入的内容 我是test函数里输入的内容

闭包

函数只是一段可执行代码,编译后就“固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。函数还可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。

函数嵌套

在函数里面还可以定义函数,可以嵌套多层,执行需要被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def outer():
a = 100
def inner(): # inner这个函数是在outer函数内部定义的
b = 200
b += a #内部函数可以使用外部函数的变量
#a += b #内部函数不能修改外部函数的变量
nonlocal a #如果想修改外部函数的变量,需要在内部函数变量前面加上nonlocal
a += b #此时内部函数可以修改外部函数的变量

result = locals()#locals表示查看函数中的局部变量,已字典的形式返回(详见局部变量,全局变量)
print(result)
inner() # inner函数只在outer函数内部可见

outer()
# inner() 这里会报错,在outer函数外部无法访问到inner函数

内部函数可以使用外部函数的变量

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 100

def outer():
a = 200
def inner():
a = 300
#nonlocal a ##如果想修改外部函数的变量,需要在内部函数变量前面加上nonlocal
global a
a -= 50
print('内部函数:',a)
print(a)
inner()

outer()
print(a)

变量或者函数的搜索规则:寻找引用时先寻找内层函数,再寻找外层函数,再寻找全局环境,再寻找系统环境builtis模块,没有的话报错

在内部环境中加上global后便可以修改全局变量,加上nonglobal边可以修改外部环境的变量

什么是闭包

闭包在装饰器中用。

闭包特点:

  1. 嵌套函数
  2. 内部函数引用了外部函数的变量
  3. 返回值是内部函数

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数块+引用环境)。

1
2
3
4
5
6
7
8
def outer(n):
num = n
def inner():
return num+1
return inner #返回出去的是inner函数的地址

print(outer(3)()) # 4
print(outer(5)()) # 5

在这段程序中,函数 inner 是函数 outer 的内嵌函数,并且 inner 函数是outer函数的返回值。我们注意到一个问题:内嵌函数 inner 中引用到外层函数中的局部变量num,Python解释器会这么处理这个问题呢? 先让我们来看看这段代码的运行结果,当我们调用分别由不同的参数调用 outer 函数得到的函数时,得到的结果是隔离的(相互不影响),也就是说每次调用outer函数后都将生成并保存一个新的局部变量num,这里outer函数返回的就是闭包。 如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure).

修改外部变量的值

闭包里默认不能修改外部变量。

1
2
3
4
5
6
7
8
9
def outer(n):
num = n
def inner():
num = num + 1
return num
return inner

print(outer(1)())

上述代码运行时会报错!

1
UnboundLocalError: local variable 'num' referenced before assignment

原因分析

在python里,只要看到了赋值语句,就会认为赋值语句的左边是一个局部变量。num = num + 1 这段代码里,num=的左边,python解析器会认为我们要修改inner函数里num这个局部变量,而这个变量使用之前是未声明的,所以会报错。

解决方案

我们分析过,报错的原因在于当我们在闭包内修改外部变量时,会被python解析器误会为内部函数的局部变量。所以,解决方案就在于,我们需要想办法,让解析器知道我们不是要修改局部变量,而是要修改外部变量。

  • 解决方法:使用 nonlocal 关键字
1
2
3
4
5
6
7
8
9
def outer(n):
num = n
def inner():
nonlocal num # 修改前使用nonlocal关键字对 num 变量进行说明
num = num + 1
return num
return inner

print(outer(2)())

装饰器

引入

1
2
3
4
5
6
7
8
def foo():
print('foo')

def func():
print('func')

foo = func
foo() # func

foo 表示是函数,函数可以理解为变量,存放的是函数的地址,foo = func表示更给了函数存放的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#### 第一波 ####
def foo():
print('foo')

foo # 表示是函数
foo() # 表示执行foo函数

#### 第二波 ####
def foo():
print('foo')

foo = lambda x: x + 1

foo() # 执行lambda表达式,而不再是原来的foo函数,因为foo这个名字被重新指向了另外一个匿名函数

函数名仅仅是个变量,只不过指向了定义的函数而已,所以才能通过 函数名()调用,如果 函数名=xxx被修改了,那么当在执行 函数名()时,调用的就不知之前的那个函数了

引入例子

初创公司有N个业务部门,基础平台部门负责提供底层的功能,如:数据库操作、redis调用、监控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
############### 基础平台提供的功能如下 ###############

def f1():
print('f1')

def f2():
print('f2')

def f3():
print('f3')

def f4():
print('f4')

############### 业务部门A 调用基础平台提供的功能 ###############

f1()
f2()
f3()
f4()

############### 业务部门B 调用基础平台提供的功能 ###############

f1()
f2()
f3()
f4()

目前公司有条不紊的进行着,但是,以前基础平台的开发人员在写代码时候没有关注验证相关的问题,即:基础平台的提供的功能可以被任何人使用。现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,即:执行功能前,先进行验证。

方案一:

跟每个业务部门交涉,每个业务部门自己写代码,调用基础平台的功能之前先验证。这样一来基础平台就不需要做任何修改了。

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
############### 基础平台提供的功能如下 ############### 

def f1():
# 验证1
# 验证2
# 验证3
print('f1')

def f2():
# 验证1
# 验证2
# 验证3
print('f2')

def f3():
# 验证1
# 验证2
# 验证3
print('f3')

def f4():
# 验证1
# 验证2
# 验证3
print('f4')

############### 业务部门不变 ###############
### 业务部门A 调用基础平台提供的功能###

f1()
f2()
f3()
f4()

### 业务部门B 调用基础平台提供的功能 ###

f1()
f2()
f3()
f4()

代码量太多,冗余。

方案二:

只对基础平台的代码进行重构,其他业务部门无需做任何修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
############### 基础平台提供的功能如下 ############### 

def check_login():
# 验证1
# 验证2
# 验证3
pass

def f1():
check_login()
print('f1')

def f2():
check_login()
print('f2')

def f3():
check_login()
print('f3')

def f4():
check_login()
print('f4')

写代码要遵循开放封闭原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:

  • 封闭:已实现的功能代码块
  • 开放:对扩展开发

如果将开放封闭原则应用在上述需求中,那么就不允许在函数 f1 、f2、f3、f4的内部进行修改代码。

方案三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def w1(func):
def inner():
# 验证1
# 验证2
# 验证3
func()
return inner

@w1
def f1():
print('f1')
@w1
def f2():
print('f2')
@w1
def f3():
print('f3')
@w1
def f4():
print('f4')

对于上述代码,也是仅仅对基础平台的代码进行修改,就可以实现在其他人调用函数 f1 f2 f3 f4 之前都进行【验证】操作,并且其他业务部门无需做任何操作。

分析:单独以f1为例

1
2
3
4
5
6
7
8
9
10
11
def w1(func):
def inner():
# 验证1
# 验证2
# 验证3
func()
return inner

@w1 #f1 = w1(f1)
def f1():
print('f1')

python解释器就会从上到下解释代码,步骤如下:

  1. def w1(func): ==>将w1函数加载到内存
  2. @w1

没错, 从表面上看解释器仅仅会解释这两句代码,因为函数在 没有被调用之前其内部代码不会被执行。

从表面上看解释器着实会执行这两句,但是 @w1 这一句代码里却有大文章, @函数名 是python的一种语法糖。

上例@w1内部会执行一下操作:

执行w1函数

执行w1函数 ,并将 @w1 下面的函数作为w1函数的参数,即:**@w1 等价于 w1(f1)** 所以,内部就会去执行:

1
2
3
4
5
6
7
def inner(): 
#验证 1
#验证 2
#验证 3
f1() # func是参数,此时 func 等于 f1
return inner# 返回的 inner,inner代表的是函数,非执行函数 ,其实就是将原来的 f1 函数塞进另外一个函数中

w1的返回值

将执行完的w1函数返回值 赋值 给@w1下面的函数的函数名f1 即将w1的返回值再重新赋值给 f1,即:

1
2
3
4
5
6
f1 = def inner(): 
#验证 1
#验证 2
#验证 3
原来f1()
return inner

所以,以后业务部门想要执行 f1 函数时,就会执行 新f1 函数,在新f1 函数内部先执行验证,再执行原来的f1函数,然后将原来f1 函数的返回值返回给了业务调用者。

如此一来, 即执行了验证的功能,又执行了原来f1函数的内容,并将原f1函数返回值 返回给业务调用者。

再议装饰器

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 makeBold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped

# 定义函数:完成包裹数据
def makeItalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped

@makeBold
def test1():
return "hello world-1"

@makeItalic
def test2():
return "hello world-2"

@makeBold
@makeItalic
def test3():
return "hello world-3"

print(test1())
print(test2())
print(test3())

运行结果:

1
2
3
<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>

装饰器(decorator)功能

  1. 引入日志
  2. 函数执行时间统计
  3. 执行函数前预备处理
  4. 执行函数后清理功能
  5. 权限校验等场景
  6. 缓存

装饰器示例

例1:无参数的函数

1
2
3
4
5
6
7
8
9
10
def check_time(action):
def do_action():
action()
return do_action

@check_time
def go_to_bed():
print('去睡觉')

go_to_bed()

上面代码理解装饰器执行行为可理解成

1
2
result = check_time(go_to_bed)  # 把go_to_bed 当做参数传入给 check_time函数,再定义一个变量用来保存check_time的运行结果
result() # check_time 函数的返回值result是一个函数, result()再调用这个函数,让它再调用go_to_bed函数

例2:被装饰的函数有参数

1
2
3
4
5
6
7
8
9
10
def check_time(action):
def do_action(a,b):
action(a,b)
return do_action

@check_time
def go_to_bed(a,b):
print('{}去{}睡觉'.format(a,b))

go_to_bed("zhangsan","床上")

例3:被装饰的函数有不定长参数

1
2
3
4
5
6
7
8
9
10
11
12
13
def test(cal):
def do_cal(*args,**kwargs):
cal(*args,**kwargs)
return do_cal

@test
def demo(*args):
sum = 0
for x in args:
sum +=x
print(sum)

demo(1, 2, 3, 4)

例4:装饰器中的return

1
2
3
4
5
6
7
8
9
10
11
def test(cal):
def do_cal(*args,**kwargs):
return cal(*args,**kwargs) # 需要再这里写return语句,表示调用函数,获取函数的返回值并返回
return do_cal

@test
def demo(a,b):
return a + b


print(demo(1, 2)) #3

总结:

  • 一般情况下为了让装饰器更通用,可以有return

例5:装饰器带参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def outer_check(time):
def check_time(action):
def do_action():
if time < 22:
return action()
else:
return '对不起,您不具有该权限'
return do_action
return check_time

@outer_check(23)
def play_game():
return '玩儿游戏'

print(play_game())

提高:使用装饰器实现权限验证

以下代码不要求掌握,如果能看懂最好,如果能自己手动写出来,那就太棒了!

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
def outer_check(base_permission):
def check_permission(action):
def do_action(my_permission):
if my_permission & base_permission:
return action(my_permission)
else:
return '对不起,您不具有该权限'
return do_action
return check_permission

READ_PERMISSION = 1
WRITE_PERMISSION = 2
EXECUTE_PERMISSION = 4

@outer_check(base_permission=READ_PERMISSION)
def read(my_permission):
return '读取数据'

@outer_check(base_permission=WRITE_PERMISSION)
def write(my_permission):
return '写入数据'

@outer_check(base_permission=EXECUTE_PERMISSION)
def execute(my_permission):
return '执行程序'

print(read(5))

模块和包

在Python中有一个概念叫做模块(module)。

说的通俗点:模块就好比是工具包,要想使用这个工具包中的工具(就好比函数),就需要导入这个模块

比如我们经常使用工具 random,就是一个模块。使用 import random 导入工具之后,就可以使用 random 的函数。

导入模块

导入模块有五种方式

  • import 模块名
  • from 模块名 import 功能名
  • from 模块名 import *
  • import 模块名 as 别名
  • from 模块名 import 功能名 as 别名

下面来挨个的看一下。

import

在Python中用关键字import来引入某个模块,比如要引入系统模块 math,就可以在文件最开始的地方用import math来引入。

语法:

1
2
3
import 模块1,模块2,...  # 导入方式

模块名.函数名() # 使用模块里的函数
  • 想一想:

    为什么必须加上模块名调用呢?

  • 答:

    因为可能存在这样一种情况:在多个模块中含有相同名称的函数,此时如果只是通过函数名来调用,解释器无法知道到底要调用哪个函数。所以如果像上述这样引入模块的时候,调用函数必须加上模块名

示例:

1
2
3
4
5
6
7
import math

#这样才能正确输出结果
print math.sqrt(2)

#这样会报错
print(sqrt(2))

from…import

有时候我们只需要用到模块中的某个函数,只需要引入该函数即可,此时可以用下面方法实现:

1
from 模块名 import 函数名1,函数名2....

不仅可以引入函数,还可以引入一些全局变量、类等

  • 注意:

    通过这种方式引入的时候,调用函数时只能给出函数名,不能给出模块名,但是当两个模块中含有相同名称函数的时候,后面一次引入会覆盖前一次引入。也就是说假如模块A中有函数function( ),在模块B中也有函数function( ),如果引入A中的function在先、B中的function在后,那么当调用function函数的时候,是去执行模块B中的function函数。

例如,要导入模块fib的fibonacci函数,使用如下语句:

1
from fib import fibonacci

注意

  • 不会把整个fib模块导入到当前的命名空间中,它只会将fib里的fibonacci单个函数引入

from … import *

把一个模块的所有内容全都导入到当前的命名空间也是可行的,只需使用如下声明:

1
from modname import *

注意

  • 这提供了一个简单的方法来导入一个模块中的所有项目。然而这种声明不该被过多地使用。

as 别名

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
In [1]: import time as tt  # 导入模块时设置别名为 tt

In [2]: time.sleep(1)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-07a34f5b1e42> in <module>()
----> 1 time.sleep(1)

NameError: name 'time' is not defined

In [3]:

In [3]: tt.sleep(1) # 使用别名才能调用方法

In [4]:

In [4]: from time import sleep as sp # 导入方法时设置别名

In [5]: sleep(1)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-5-82e5c2913b44> in <module>()
----> 1 sleep(1)

NameError: name 'sleep' is not defined

In [6]:

In [6]: sp(1) # 使用别名才能调用方法

In [7]:

常见系统模块

为了方便程序员开发代码,Python提供了很多内置的模块给程序员用来提高编码效率。常见的内置模块有:

OS模块

OS全称OperationSystem,即操作系统模块,这个模块可以用来操作系统的功能,并且实现跨平台操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
os.getcwd() # 获取当前的工作目录,即当前python脚本工作的目录
os.chdir('test') # 改变当前脚本工作目录,相当于shell下的cd命令
os.rename('毕业论文.txt','毕业论文-最终版.txt') # 文件重命名
os.remove('毕业论文.txt') # 删除文件
os.rmdir('demo') # 删除空文件夹
os.removedirs('demo') # 删除空文件夹
os.mkdir('demo') # 创建一个文件夹
os.chdir('C:\\') # 切换工作目录
os.listdir('C:\\') # 列出指定目录里的所有文件和文件夹
os.name # nt->widonws posix->Linux/Unix或者MacOS
os.environ # 获取到环境配置
os.environ.get('PATH') # 获取指定的环境配置

os.path.abspath(path) # 获取Path规范会的绝对路径
os.path.exists(path) # 如果Path存在,则返回True
os.path.isdir(path) # 如果path是一个存在的目录,返回True。否则返回False
os.path.isfile(path) # 如果path是一个存在的文件,返回True。否则返回False
os.path.splitext(path) # 用来将指定路径进行分隔,可以获取到文件的后缀名
Copy

sys模块

该模块提供对解释器使用或维护的一些变量的访问,以及与解释器强烈交互的函数。

1
2
3
4
5
6
7
8
9
import sys
sys.path # 模块的查找路径
sys.argv # 传递给Python脚本的命令行参数列表
sys.exit(code) # 让程序以指定的退出码结束

sys.stdin # 标准输入。可以通过它来获取用户的输入
sys.stdout # 标准输出。可以通过修改它来百变默认输出
sys.stderr # 错误输出。可以通过修改它来改变错误删除
Copy

math模块

math模块保存了数学计算相关的方法,可以很方便的实现数学运算。

1
2
3
4
5
6
7
8
9
10
11
import math
print(math.fabs(-100)) # 取绝对值
print(math.ceil(34.01)) #向上取整
print(math.factorial(5)) # 计算阶乘
print(math.floor(34.98)) # 向下取整
print(math.pi) # π的值,约等于 3.141592653589793
print(math.pow(2, 10)) # 2的10次方
print(math.sin(math.pi / 6)) # 正弦值
print(math.cos(math.pi / 3)) # 余弦值
print(math.tan(math.pi / 2)) # 正切值
Copy

random模块

random 模块主要用于生成随机数或者从一个列表里随机获取数据。

1
2
3
4
5
6
7
print(random.random())  # 生成 [0,1)的随机浮点数
print(random.uniform(20, 30)) # 生成[20,30]的随机浮点数
print(random.randint(10, 30)) # 生成[10,30]的随机整数
print(random.randrange(20, 30)) # 生成[20,30)的随机整数
print(random.choice('abcdefg')) # 从列表里随机取出一个元素
print(random.sample('abcdefghij', 3)) # 从列表里随机取出指定个数的元素
Copy

练习:

定义一个函数,用来生成由数字和字母组成的随机验证码。该函数需要一个参数,参数用来指定验证码的长度。

datetime模块

datetime模块主要用来显示日期时间,这里主要涉及 date类,用来显示日期;time类,用来显示时间;dateteime类,用来显示日期时间;timedelta类用来计算时间。

1
2
3
4
5
6
import datetime
print(datetime.date(2020, 1, 1)) # 创建一个日期
print(datetime.time(18, 23, 45)) # 创建一个时间
print(datetime.datetime.now()) # 获取当前的日期时间
print(datetime.datetime.now() + datetime.timedelta(3)) # 计算三天以后的日期时间
Copy

time模块

除了使用datetime模块里的time类以外,Python还单独提供了另一个time模块,用来操作时间。time模块不仅可以用来显示时间,还可以控制程序,让程序暂停(使用sleep函数)

1
2
3
4
5
6
7
8
9
print(time.time())  # 获取从1970-01-01 00:00:00 UTC 到现在时间的秒数
print(time.strftime("%Y-%m-%d %H:%M:%S")) # 按照指定格式输出时间
print(time.asctime()) #Mon Apr 15 20:03:23 2019
print(time.ctime()) # Mon Apr 15 20:03:23 2019

print('hello')
print(time.sleep(10)) # 让线程暂停10秒钟
print('world')
Copy

calendar模块

calendar模块用来显示一个日历,使用的不多,了解即可。

1
2
3
4
5
6
7
8
calendar.setfirstweekday(calendar.SUNDAY) # 设置每周起始日期码。周一到周日分别对应 0 ~ 6
calendar.firstweekday()# 返回当前每周起始日期的设置。默认情况下,首次载入calendar模块时返回0,即星期一。
c = calendar.calendar(2019) # 生成2019年的日历,并且以周日为其实日期码
print(c) #打印2019年日历
print(calendar.isleap(2000)) # True.闰年返回True,否则返回False
count = calendar.leapdays(1996,2010) # 获取1996年到2010年一共有多少个闰年
print(calendar.month(2019, 3)) # 打印2019年3月的日历
Copy

hashlib模块

hashlib是一个提供字符加密功能的模块,包含MD5和SHA的加密算法,具体支持md5,sha1, sha224, sha256, sha384, sha512等算法。 该模块在用户登录认证方面应用广泛,对文本加密也很常见。

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

# 待加密信息
str = '这是一个测试'

# 创建md5对象
hl = hashlib.md5('hello'.encode(encoding='utf8'))
print('MD5加密后为 :' + hl.hexdigest())

h1 = hashlib.sha1('123456'.encode())
print(h1.hexdigest())
h2 = hashlib.sha224('123456'.encode())
print(h2.hexdigest())
h3 = hashlib.sha256('123456'.encode())
print(h3.hexdigest())
h4 = hashlib.sha384('123456'.encode())
print(h4.hexdigest())
Copy

hmac模块

HMAC算法也是一种一种单项加密算法,并且它是基于上面各种哈希算法/散列算法的,只是它可以在运算过程中使用一个密钥来增增强安全性。hmac模块实现了HAMC算法,提供了相应的函数和方法,且与hashlib提供的api基本一致。

1
2
3
4
h = hmac.new('h'.encode(),'你好'.encode())
result = h.hexdigest()
print(result) # 获取加密后的结果
Copy

copy模块

copy模块里有copy和deepcopy两个函数,分别用来对数据进行深复制和浅复制。

1
2
3
4
5
6
import copy

nums = [1, 5, 3, 8, [100, 200, 300, 400], 6, 7]
nums1 = copy.copy(nums) # 对nums列表进行浅复制
nums2 = copy.deepcopy(nums) # 对nums列表进行深复制
Copy

uuid模块

UUID是128位的全局唯一标识符,通常由32字节的字母串表示,它可以保证时间和空间的唯一性,也称为GUID。通过MAC地址、时间戳、命名空间、随机数、伪随机数来保证生产的ID的唯一性。随机生成字符串,可以当成token使用,当成用户账号使用,当成订单号使用。

方法 作用
uuid.uuid1() 基于MAC地址,时间戳,随机数来生成唯一的uuid,可以保证全球范围内的唯一性。
uuid.uuid2() 算法与uuid1相同,不同的是把时间戳的前4位置换为POSIX的UID。不过需要注意的是python中没有基于DCE的算法,所以python的uuid模块中没有uuid2这个方法。
uuid.uuid3(namespace,name) 通过计算一个命名空间和名字的md5散列值来给出一个uuid,所以可以保证命名空间中的不同名字具有不同的uuid,但是相同的名字就是相同的uuid了。namespace并不是一个自己手动指定的字符串或其他量,而是在uuid模块中本身给出的一些值。比如uuid.NAMESPACE_DNS,uuid.NAMESPACE_OID,uuid.NAMESPACE_OID这些值。这些值本身也是UUID对象,根据一定的规则计算得出。
uuid.uuid4() 通过伪随机数得到uuid,是有一定概率重复的
uuid.uuid5(namespace,name) 和uuid3基本相同,只不过采用的散列算法是sha1

一般而言,在对uuid的需求不是很复杂的时候,uuid1或者uuid4方法就已经够用了,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
import uuid

print(uuid.uuid1()) # 根据时间戳和机器码生成uuid,可以保证全球唯一
print(uuid.uuid4()) # 随机生成uuid,可能会有重复

# 使用命名空间和字符串生成uuid.
# 注意一下两点:
# 1. 命名空间不是随意输入的字符串,它也是一个uuid类型的数据
# 2. 相同的命名空间和想到的字符串,生成的uuid是一样的
print(uuid.uuid3(uuid.NAMESPACE_DNS, 'hello'))
print(uuid.uuid5(uuid.NAMESPACE_OID, 'hello'))

pip命令的使用

在安装Python时,同时还会安装pip软件,它是Python的包管理工具,可以用来查找、下载、安装和卸载Python的第三方资源包。

配置pip

可以直接在终端中输入pip命令,如果出错,可能会有两个原因:

  1. pip安装成功以后没有正确配置
  2. 安装Python时,没有自动安装pip(很少见)

配置pip

和运行Python命令一样,如果想要运行 pip 命令同样也需要将pip命令的安装目录添加到环境变量中。

管理第三方包

对第三方包的管理主要包含查找、安装和卸载三个部分的操作。

安装

使用 pip install <包名>命令可以安装指定的第三方资源包。

1
2
pip install ipython # 安装ipython包
Copy

使用 install 命令下载第三方资源包时,默认是从 pythonhosted下载,由于各种原因,在国内下载速度相对来说比较慢,在某些时候甚至会出现连接超时的情况,我们可以使用国内镜像来提高下载速度。

临时修改

如果只是想临时修改某个第三方资源包的下载地址,在第三方包名后面添加 -i 参数,再指定下载路径即可,格式为pip install <包名> -i <国内镜像路径>

1
2
pip install ipython -i https://pypi.douban.com/simple
Copy

永久修改

除了临时修改pip的下载源以外,我们还能永久改变pip的默认下载路径。

在当前用户目录下创建一个pip的文件夹,然后再在文件夹里创建pip.ini文件并输入一下内容:

1
2
3
4
5
[global]
index-url=https://pypi.douban.com/simple
[install]
trusted-host=pypi.douban.com
Copy

常见国内镜像

卸载

使用 pip install <包名>命令可以用来卸载指定的第三方资源包。

1
2
pip uninstall ipython # 卸载ipython包
Copy

查找

使用pip list 或者 pip freeze命令可以来管理第三方资源包。这两个命令的功能一致,都是用来显示当前环境里已经安装的包,区别在于pip list会列出所有的包,包括一些无法uninstall的包;而pip freeze只会列出我们安装的第三方包。

总结

开发中,我们通常会使用很多第三方的资源包,我们在将程序部署到服务器的时候,不仅要把代码上传到服务器,同时还需要把代码里用到的第三方资源包告诉服务器。那么这里就有两个问题:

  1. 当我们电脑上运行很多个项目,每个项目使用的第三方资源包又不一致时,怎样将代码和它使用到的第三方资源包放在一起呢?答:虚拟环境

  2. 怎样将自己代码使用到的第三方资源包告诉给服务器?

    1. 使用 pip freeze > requires.txt 命令,将代码里使用到的第三方资源包以及版本号写入到 requirements.txt 文件,在部署时,同时将 requirements.txt 文件上传到服务器。
    2. 服务器在拿到代码以后,首先运行 pip install -r requirements.txt 命令,将文件里列出的所有第三方框架先安装到服务器,然后才能运行代码。

使用pycharm管理第三方包

除了使用pip 命令管理第三方资源包以外,我们还能使用pycharm来对第三方包进行管理。

自定义模块

除了使用系统提供的内置模块以外,我们还能自己写一个模块供自己的程序使用。一个py文件就是一个模块,所以,自定义模块很简单,基本上相当于创建一个py文件。但是,需要注意的是,如果一个py文件要作为一个模块被别的代码使用,这个py文件的名字一定要遵守标识符的命名规则。

模块的查找路径

创建一个模块非常简单,安装标识符的命名规则创建一个py文件就是一个模块。但是问题是,我们需要把创建好的这个py文件放在哪个位置,在代码中使用 import语句才能找到这个模块呢?

Python内置sys模块的path属性,列出了程序运行时查找模块的目录,只需要把我们创建好的模块放到这些任意的一个目录里即可。

1
2
3
4
5
6
7
8
9
10
11
12
import sys
print(sys.path)
[
'C:\\Users\\chris\\Desktop\\Test',
'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\python37.zip',
'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\DLLs',
'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\lib',
'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37',
'C:\\Users\\chris\\AppData\\Roaming\\Python\\Python37\\site-packages',
'C:\\Users\\chris\\AppData\\Local\\Programs\\Python\\Python37\\lib\\site-packages'
]
Copy

__all__的使用

使用from <模块名> import *导入一个模块里所有的内容时,本质上是去查找这个模块的__all__属性,将__all__属性里声明的所有内容导入。如果这个模块里没有设置__all__属性,此时才会导入这个模块里的所有内容。

模块里的私有成员

模块里以一个下划线_开始的变量和函数,是模块里的私有成员,当模块被导入时,以_开头的变量默认不会被导入。但是它不具有强制性,如果一个代码强行使用以_开头的变量,有时也可以。但是强烈不建议这样使用,因为有可能会出问题。

总结

test1.py:模块里没有__all__属性

1
2
3
4
a = 'hello'
def fn():
print('我是test1模块里的fn函数')
Copy

test2.py:模块里有__all__属性

1
2
3
4
5
6
x = '你好'
y = 'good'
def foo():
print('我是test2模块里的foo函数')
__all__ = ('x','foo')
Copy

test3.py:模块里有以_开头的属性

1
2
3
4
5
m = '早上好'
_n = '下午好'
def _bar():
print('我是test3里的bar函数')
Copy

demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from test1 import *
from test2 import *
from test3 import *

print(a)
fn()

print(x)
# print(y) 会报错,test2的__all__里没有变量 y
foo()


print(m)
# print(_n) 会报错,导入test3时, _n 不会被导入

import test3
print(test3._n) # 也可以强行使用,但是强烈不建议
Copy

__name__的使用

在实际开中,当一个开发人员编写完一个模块后,为了让模块能够在项目中达到想要的效果,这个开发人员会自行在py文件中添加一些测试信息,例如:

test1.py

1
2
3
4
5
6
7
8
def add(a,b):
return a+b

# 这段代码应该只有直接运行这个文件进行测试时才要执行
# 如果别的代码导入本模块,这段代码不应该被执行
ret = add(12,22)
print('测试的结果是',ret)
Copy

demo.py

1
2
import test1.py   # 只要导入了tets1.py,就会立刻执行 test1.py 代码,打印测试内容
Copy

为了解决这个问题,python在执行一个文件时有个变量__name__.在Python中,当直接运行一个py文件时,这个py文件里的__name__值是__main__,据此可以判断一个一个py文件是被直接执行还是以模块的形式被导入。

1
2
3
4
5
6
7
8
def add(a,b):
return a+b

if __name__ == '__main__': # 只有直接执行这个py文件时,__name__的值才是 __main__
# 以下代码只有直接运行这个文件才会执行,如果是文件被别的代码导入,下面的代码不会执行
ret = add(12,22)
print('测试的结果是',ret)
Copy

注意事项

在自定义模块时,需要注意一点,自定义模块名不要和系统的模块名重名,否则会出现问题!

包的使用

一个模块就是一个 py 文件,在 Python 里为了对模块分类管理,就需要划分不同的文件夹。多个有联系的模块可以将其放到同一个文件夹下,为了称呼方便,一般把 Python 里的一个代码文件夹称为一个包。

1. 导入包的方式

现有以下包newmsg,包里由两个模块,分别是sendmsg.pyrecvmsg.py文件。在包的上级文件夹里,有一个test.py文件,目标是在test.py文件里引入newmsg的两个模块。

目录结构如下图所示:

image

sendmsg.py文件里的内容如下:

1
2
3
def send_msg():
print('------sendmsg方法被调用了-------')
Copy

recvmsg.py文件里的内容如下:

1
2
3
def recv_msg():
print('-----recvmsg方法被调用了--------')
Copy

可以使用以下几种方式来导入模块,使用模块里的方法。

1>. 直接使用包名.模块模块名导入指定的模块。

image

2>. 使用from xxx import xxx 方式导入指定模块。

image

3>. 使用__init__.py文件,导入包里的指定模块。

可以在newmsg里创建__init__.py文件,在该文件里导入指定的内容。

image

__init__.py文件里编写代码:

1
2
from . import sendmsg  # 导入指定的模块    . 代表的是当前文件夹
Copy

test.py文件里的代码

1
2
3
4
import newmsg # 导入时,只需要输入包名即可。在包名的__init__.py文件里,导入了指定模块
newmsg.sendmsg.sendm_msg() # 可以直接调用对应的方法
# newmsg.recvmsg.recv_msg() 不可以使用 recvmsg 模块,因为 __init__.py文件里没有导入这个模块
Copy

4.> 使用__init__.py文件,结合__all__属性,导入包里的所有模块。

newmsg包里的__init__.py文件里编写代码:

1
2
__all__ = ["sendmsg","recvmsg"]  # 指定导入的内容
Copy

test.py文件代码:

1
2
3
4
from newmsg import *  # 将newmsg里的__inint__.py文件里,__all__属性对应的所有模块都导入
sendmsg.sendmsg()
recvmsg.recvmsg()
Copy

总结

  • 包将有联系的模块组织在一起,即放到同一个文件夹下,并且在这个文件夹创建一个名字为__init__.py 文件,那么这个文件夹就称之为
  • 有效避免模块名称冲突问题,让应用组织结构更加清晰

2. __init__.py文件有什么用

__init__.py 控制着包的导入行为。__init__.py为空仅仅是把这个包导入,不会导入包中的模块。可以在__init__.py文件中编写内容。

newmsg/__init__.py文件:

1
2
print('hello world')
Copy

别的模块在引入这个包的时候,会自动调用这段代码。

image

3. __all__

__init__.py文件中,定义一个__all__变量,它控制着 from 包名 import *时导入的模块。

newmsg/__init__.py文件:

1
__all__ = ['sendmsg','recvmsg']

面向对象

python,java,C++的面向对象的区别

Python、Java 和 C++ 都是面向对象编程语言,但它们之间还是有一些区别的。

1.语法风格:

  • Python 对于面向对象编程的支持比较灵活,支持动态绑定,不需要显式地声明变量的类型。Python 的语法风格更加简洁易懂,适合快速开发,但是在大规模程序中,Python 的代码可读性可能会受到一定的影响。
  • Java 的面向对象语法比 Python 略微复杂一些,需要显式地声明变量类型,但是它的类型检查可以帮助开发人员更容易地发现程序中的错误。Java 的代码比较规范,适合大型项目开发。
  • C++ 是一门支持多种编程范式的语言,其面向对象语法相对来说较为复杂,需要程序员手动管理内存,但是它的性能较好,可以用来开发一些对性能要求较高的项目。

2.继承:

  • Python 和 Java 都支持单继承和多继承,但是 Python 的多继承有些不同寻常,有时需要格外小心使用。
  • C++ 支持单继承和多继承,但是它的继承机制比较复杂,需要程序员有更深的理解和掌握。

3.多态性:

  • Python 和 Java 都支持运行时多态性(动态绑定),使得子类能够在父类的接口上进行扩展。
  • C++ 支持编译时多态性(静态绑定),通过虚函数实现多态性。

4.内存管理:

  • Python 和 Java 采用垃圾回收机制,开发人员不需要手动管理内存。
  • C++ 需要程序员手动管理内存,需要注意内存泄漏和悬垂指针等问题。

Python面向对象学习目标

  • 说出面向对象和面向过程的特点
  • 能够使用dir内置函数查看对象的方法
  • 能够使用类创建对象
  • 能够说出self的含义
  • 能够说出魔法方法什么时候调用
  • 能够说出内置属性的作用
  • 能够说出实例属性和类属性的区别
  • 能够定义和操作私有属性

基础

面向对象与面向过程

  • 面向过程:根据业务逻辑从上到下写代码。
  • 面向对象:将变量与函数绑定到一起,分类进行封装,每个程序只要负责分配给自己的分类,这样能够更快速的开发程序,减少了重复代码。

面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,开发过程的思路是将数据与函数按照执行的逻辑顺序组织在一起,数据与函数分开考虑,面向过程基本是由函数组成的。

面向过程编程

面向过程编程的关注点在于怎么做

  • 把完成某一个需求的 所有步骤 从头到尾 逐步实现
  • 根据开发需求,将某些 功能独立 的代码 封装 成一个又一个 函数
  • 最后完成的代码,就是顺序地调用 不同的函数

特点:

  • 注重步骤与过程,不注重职责分工
  • 如果需求复杂,代码会变得很复杂
  • 开发复杂项目,没有固定的套路,开发难度很大!

面向对象基本概念

面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)和面相过程编程,是两种不同的编程方式。

面向对象编程的关注点在于谁来做

相比较函数,面向对象是更大的封装,根据职责在 一个对象中封装多个方法

  • 在完成某一个需求前,首先确定职责 —— 要做的事情(方法)
  • 根据 职责 确定不同的 对象,在对象内部封装不同的方法(多个)
  • 最后完成的代码,就是顺序地调用不同对象的相应方法。

特点:

  • 注重 对象和职责,不同的对象承担不同的职责。
  • 更加适合应对复杂的需求变化,是专门应对复杂项目开发,提供的固定套路。
  • 需要在面向过程基础上,再学习一些面向对象的语法。

类和对象

对象是面向对象编程的两个核心概念。

类是对一群具有相同特征或者行为 的事物的一个统称,是抽象的,不能直接使用

  • 特征其实就是一个变量,在类里我们称之为属性。
  • 行为其实就是一个函数,在类里我们称之为方法。
  • 类其实就是由 属性方法 组成的一个抽象概念。

类就相当于制造飞机时的图纸,是一个模板。这个模板只规定了飞机的某些特征(例如大小,颜色,型号等等)和行为(例如起飞,降落,飞行等等),它并不是一个具体的飞机,而是对飞机的一个抽象概念。它出现的目的,是为了让我们的创建飞机对象。

对象

对象是由类创建出来的一个具体存在,可以直接使用。由哪一个类创建出来的 对象,就拥有在哪一个类中定义的属性和方法。 对象 就相当于用图纸制造的飞机。在开发中,应该先有类,在类里定义好属性和行为,再根据类来创建对象。

类和对象的关系

  • 类是模板,对象是根据类这个模板创建出来的,应该先有类,再有对象。
  • 使用同一个类,能够创建出很多对象。
  • 类中定义了什么属性和方法,对象中就有什么属性和方法。
  • 不同对象对应的属性值也会不同。

例如:定义了一个狗类,这个狗类有以下属性:

  • 品种
  • 颜色
  • 性别
  • 名字

现在根据这个类创建出了两条狗,这两条狗分别是 哈士奇、灰色、母、二哈中华田园犬、黄色、公、旺财。我们发现,这两条狗都具有 品种、颜色、性别和名字这些属性,但是每条狗对应的属性值却不一样。

类的设计

在程序开发中,要设计一个类,通常需要满足一下三个要素:

  1. 类名 这类事物的名字,安照大驼峰命名法(每个单词的首字母大写)起名。
  2. 属性 这类事物具有什么样的特征。
  3. 方法 这类事物具有什么样的行为。

定义类名

名词提炼法:分析整个业务流程,出现的名词,通常就是找到的类。

属性和方法的确定

  • 对对象的特征描述,可以定义成属性
  • 对象具有的行为(动词)可以定义成方法

面向对象基本语法

在Python中,对象几乎是无处不在的,我们可以使用dir内置函数来查看这个对象里的方法。

PS:

self指对象,cls指类

定义简单的类(只包含方法)

面向对象是更大的封装,在一个类中封装多个方法,这样通过这个类创建出来的对象,就可以直接调用这些方法了!

定义类

在Python中要定义一个只包含方法的类,语法格式如下:

1
2
3
4
5
class 类名:
def 方法1(self,参数列表):
pass
def 方法2(self,参数列表):
pass
  1. 方法的定义格式和之前学习过的函数一样
  2. 方法里的第一个参数必须是self,大家暂时先记住,稍后介绍 self.
  3. 类名要遵守大驼峰命名法。
创建实例对象

当一个类定义完成之后,要使用这个类来创建对象,语法格式如下:

1
对象变量名 = 类名()
第一个面向对象代码

需求

  • 小猫 爱 吃 鱼,小猫 要 喝 水

分析

  • 定义一个猫类 Cat
  • 定义两个方法 eat 和 drink
  • 按照需求 —— 不需要定义属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cat:
"""这是个猫类"""

def eat(self):
print("小猫在吃东西")

def drink(self):
print("小猫在喝水")

tom = Cat() # 创建了一个Cat对象
tom.eat()
tom.drink()

hello_kitty = Cat() # 又创建了一个新的Cat对象
hello_kitty.eat()
hello_kitty.drink()

思考:tomhello_kitty 是同一个对象吗?

self的使用

给对象添加属性

python支持动态属性,当一个对象创建好了以后,直接使用 对象.属性名 = 属性值 就可以很方便的给对象添加一个属性。

1
2
tom = Cat()
tom.name = 'Tom' # 可以直接给 tom 对象添加一个 name 属性

这种方法很方便,但是,不建议使用这种方式给对象添加属性。

self的概念

哪个对象调用了方法,方法里的self指的就是谁。 通过 self.属性名 可以访问到这个对象的属性;通过 self.方法名() 可以调用这个对象的方法。

1
2
3
4
5
6
7
8
9
10
11
class Cat:
def eat(self):
print("%s爱吃鱼" %self.name)

tom = Cat()
tom.name = 'Tom' # 给 tom 对象添加了一个name属性
tom.eat() # Tom爱吃鱼

lazy_cat = Cat()
lazy_cat.name = "大懒猫"
lazy_cat.eat() # 大懒猫爱吃鱼
直接给对象添加属性的缺点

上述代码中,我们是先创建对象,然后再给对象添加 name 属性,但是这样做会有问题。

1
2
3
tom = Cat()
tom.eat()
tom.anme = "Tom"

程序运行时会报错:

1
2
AttributeError: 'Cat' object has no attribute 'name'
错误提示:'Cat'对象没有 'name' 属性

在日常开发中,不推荐在类的外部直接给对象添加属性这种方式。对象应该具有哪些属性,我们应该封装在类的内部。

魔法方法

Python 里有一种方法,叫做魔法方法。Python 的类里提供的,两个下划线开始,两个下划线结束的方法,就是魔法方法,普通方法需要调用,魔法方法在恰当的时候就会被激活,自动执行。 魔法方法的两个特点:

  • 两侧各有两个下划线;
  • “咒语”名字已经由 Python 官方定义好,我们不能乱写。

1.__init__方法

__init__()方法,在创建一个对象时默认被调用,不需要手动调用。在开发中,如果希望在创建对象的同时,就设置对象的属性,可以对 __init__ 方法进行改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Cat:
"""这是一个猫类"""
def __init__(self,name): # 重写了 __init__ 魔法方法
self.name = name

def eat(self):
return "%s爱吃鱼"%self.name
def drink(self):
return '%s爱喝水'%self.name

"""
tom = Cat()
TypeError: __init__() missing 1 required positional argument: 'name'
这种写法在运行时会直接报错!因为 __init__ 方法里要求在创建对象时,必须要传递 name 属性,如果不传入会直接报错!
"""

tom = Cat("Tom") # 创建对象时,必须要指定name属性的值
tom.eat() # tom爱吃鱼

注意:

  1. __init__()方法在创建对象时,会默认被调用,不需要手动的调用这个方法。
  2. __init__()方法里的self参数,在创建对象时不需要传递参数,python解释器会把创建好的对象引用直接赋值给self
  3. 在类的内部,可以使用self来使用属性和调用方法;在类的外部,需要使用对象名来使用属性和调用方法。
  4. 如果有多个对象,每个对象的属性是各自保存的,都有各自独立的地址。
  5. 方法是所有对象共享的,只占用一份内存空间,方法被调用时会通过self来判断是哪个对象调用了实例方法。

2.__del__方法

创建对象后,python解释器默认调用__init__()方法;

而当删除对象时,python解释器也会默认调用一个方法,这个方法为__del__()方法。

1
2
3
4
5
6
7
8
9
10
11
class Student:
def __init__(self,name,score):
print('__init__方法被调用了')
self.name = name
self.score = score

def __del__(self):
print('__del__方法被调用了')
s = Student('lisi',95)
del s
input('请输入内容')

PS:

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
class Student:
def __init__(self,name,score):
print('__init__方法被调用了')
self.name = name
self.score = score

def __del__(self):
print('__del__方法被调用了')
s = Student('lisi',95)
s1 = s
s2 = s
print(sys.getrefcount(s)) #4 PS:调用这个函数也引用一次s

del s1
print("删除s1后的",s.name)
print(sys.getrefcount(s)) #3

del s2
print("删除s2后的",s.name)
print(sys.getrefcount(s)) #2

del s
print("删除s后的",s.name)


注意:python的垃圾回收机制,当所有的指向都被删除时对象才会被回收,才会调用__del__方法

PS:查看引用的次数函数sys包中的sys.getrefcount()

3.__str__方法

__str__方法返回对象的描述信息,使用print()函数打印对象时,其实调用的就是这个对象的__str__方法。

1
2
3
4
5
6
7
8
9
10
class Cat:
def __init__(self,name,color):
self.name = name
self.color = color

tom = Cat('Tom','white')

# 使用 print 方法打印对象时,会调用对象的 __str__ 方法,默认会打印类名和对象的地址名
print(tom) # <__main__.Cat object at 0x0000021BE3B9C940>
Copy

如果想要修改对象的输出的结果,可以重写 __str__ 方法。

1
2
3
4
5
6
7
8
9
10
11
class Person:
def __init__(self,name,age):
self.name = name
self.age = age

def __str__(self):
return '哈哈'

p = Person('张三',18)
print(p) # 哈哈 打印对象时,会自动调用对象的 __str__ 方法
Copy

一般情况下,我们在打印一个对象时,可能需要列出这个对象的所有属性。

1
2
3
4
5
6
7
8
9
10
class Student:
def __init__(self,name,score):
self.name = name
self.score = score
def __str__(self):
return '姓名是:{},成绩是{}分'.format(self.name,self.score)

s = Student('lisi',95)
print(s) # 姓名是:lisi,成绩是95分
Copy

触发时机:打印对象名,自动触发去调用__str__里面的内容

注意:一定要在方法中添加return,return 后面的内容就是打印对象看到的内容。

4. __repr__方法

__repr__方法和__str__方法功能类似,都是用来修改一个对象的默认打印内容。在打印一个对象时,如果没有重写__str__方法,它会自动来查找__repr__方法。如果这两个方法都没有,会直接打印这个对象的内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

def __repr__(self):
return 'helllo'


class Person:
def __repr__(self):
return 'hi'

def __str__(self):
return 'good'


s = Student('lisi', 95)
print(s) # hello

p = Person()
print(p) # good
Copy

5. __call__方法

对象后面加括号,触发执行。

1
2
3
4
5
6
7
8
9
10
11
class Foo:
def __init__(self):
pass

def __call__(self, *args, **kwargs):
print('__call__')


obj = Foo() # 执行 __init__
obj() # 执行 __call__
Copy

总结

  1. 常用:重点是__init____str__,选用:__call__
  2. 当创建一个对象时,会自动调用__init__方法,当删除一个对象时,会自动调用__del__方法。
  3. 使用__str____repr__方法,都会修改一个对象转换成为字符串的结果。一般来说,__str__方法的结果更加在意可读性,而__repr__方法的结果更加在意正确性(例如:datetime模块里的datetime类)

运算相关的魔法方法

思考:

1
2
3
4
5
6
7
8
9
class Person:
def __init__(self,name,age):
self.name = name
self.age = age

p1 = Person('zhangsan',18)
p2 = Person('zhangsan',18)
print(p1 == p2)
Copy

上述代码中,使用==运算符比较两个对象,结果是True还是False?==到底比较的是什么?

比较运算符相关魔法方法

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 Student:
def __init__(self, name, age):
self.name = name
self.age = age

def __eq__(self, other):
return self.name == other.name and self.age == other.age

# def __ne__(self, other):

def __lt__(self, other):
return self.age < other.age

# def __gt__(self, other):

def __le__(self, other):
return self.age <= other.age
# def __ge__(self, other):


s1 = Student('zhangsan', 18)
s2 = Student('zhangsan', 18)
s3 = Student('lisi', 20)
print(s1 == s2)
print(s1 != s2)
print(s1 > s2)
print(s1 >= s2)
print(s1 <= s2)
print(s1 <= s2)
Copy

算数运算符相关魔法方法

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 Student:
def __init__(self, name, age):
self.name = name
self.age = age

def __add__(self, other):
return self.age + other

def __sub__(self, other):
return self.age - other

def __mul__(self, other):
return self.age * other

def __truediv__(self, other):
return self.age / other

def __mod__(self, other):
return self.age % other

def __pow__(self, power, modulo=None):
return self.age ** power


s = Student('zhangsan', 18)
print(s + 1) # 19
print(s - 2) # 16
print(s * 2) # 36
print(s / 5) # 3.6
print(s % 5) # 3
print(s ** 2) # 324
Copy

类型转换相关魔法方法

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 Student:
def __init__(self, name, age):
self.name = name
self.age = age

def __int__(self):
return self.age

def __float__(self):
return self.age * 1.0

def __str__(self):
return self.name

def __bool__(self):
return self.age > 18


s = Student('zhangsan', 18)
print(int(s))
print(float(s))
print(str(s))
print(bool(s))

if s:
print('hello')

内置属性

使用内置函数dir可以查看一个对象支持的所有属性和方法,Python中存在着很多的内置属性。

slots

Python中支持动态属性,可以直接通过点语法直接给一个对象添加属性,代码更加的灵活。但是在某些情况下,我们可能需要对属性进行控制,此时,就剋使用__slots__实现。

1
2
3
4
5
6
7
8
9
10
11
class Person(object):
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
p = Person('张三', 18)
p.name = '李四'

# 对象p只能设置name和age属性,不能再动态添加属性
# p.height = 180 # 报错
Copy

doc

表示类的描述信息。

1
2
3
4
5
6
7
8
class Foo:
""" 描述类信息,这是用于看片的神奇 """
def func(self):
pass

print(Foo.__doc__)
#输出:类的描述信息
Copy

moduleclass

module 表示当前操作的对象在那个模块;class 表示当前操作的对象的类是什么。

1
2
3
4
5
6
7
8
9
10
11
test.py
class Person(object):
def __init__(self):
self.name = 'laowang'
main.py
from test import Person

obj = Person()
print(obj.__module__) # 输出 test 即:输出模块
print(obj.__class__) # 输出 test.Person 即:输出类
Copy

dict

以字典的形式,显示对象所有的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Province(object):
country = 'China'

def __init__(self, name, count):
self.name = name
self.count = count

def func(self, *args, **kwargs):
print('func')

# 获取类的属性,即:类属性、方法、
print(Province.__dict__)
# 输出:{'__dict__': <attribute '__dict__' of 'Province' objects>, '__module__': '__main__', 'country': 'China', '__doc__': None, '__weakref__': <attribute '__weakref__' of 'Province' objects>, 'func': <function Province.func at 0x101897950>, '__init__': <function Province.__init__ at 0x1018978c8>}

obj1 = Province('山东', 10000)
print(obj1.__dict__)
# 获取 对象obj1 的属性
# 输出:{'count': 10000, 'name': '山东'}

obj2 = Province('山西', 20000)
print(obj2.__dict__)
# 获取 对象obj1 的属性
# 输出:{'count': 20000, 'name': '山西'}
Copy

_getitem__setitem____delitem__方法

这三个方法,是将对象当做字典一样进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo(object):

def __getitem__(self, key):
print('__getitem__', key)

def __setitem__(self, key, value):
print('__setitem__', key, value)

def __delitem__(self, key):
print('__delitem__', key)

obj = Foo()

result = obj['k1'] # 自动触发执行 __getitem__
obj['k2'] = 'laowang' # 自动触发执行 __setitem__
del obj['k1'] # 自动触发执行 __delitem__

实例属性、类属性

在面向对象开发中,使用类创建出来的实例是一个对象,那么,类是否是一个对象呢?

实例属性

通过类创建的对象被称为 实例对象,对象属性又称为实例属性,记录对象各自的数据,不同对象的同名实例属性,记录的数据各自独立,互不干扰。

1
2
3
4
5
6
7
8
9
10
class Person(object):
def __init__(self,name,age):
# 这里的name和age都属于是实例属性,每个实例在创建时,都有自己的属性
self.name = name
self.age = age

# 每创建一个对象,这个对象就有自己的name和age属性
p1 = Person('张三',18)
p2 = Person("李四",20)
Copy

类属性

类属性就是类对象所拥有的属性,它被该类的所有实例对象所共有,类属性可以通过类对象或者实例对象访问。

1
2
3
4
5
6
7
8
9
10
11
class Dog:
type = "狗" # 类属性

dog1 = Dog()
dog2 = Dog()

# 不管是dog1、dog2还是Dog类,都可以访问到type属性
print(Dog.type) # 结果:狗
print(dog1.type) # 结果:狗
print(dog2.type) # 结果:狗
Copy

使用场景:

  1. 类的实例记录的某项数据始终保持一致时,则定义类属性。
  2. /实例属性要求每个对象为其单独开辟一份内存空间来记录数据,而类属性为全类所共有 ,仅占用一份内存,更加节省内存空间。

注意点:

1> 尽量避免类属性和实例属性同名。如果有同名实例属性,实例对象会优先访问实例属性

1
2
3
4
5
6
7
8
9
10
11
class Dog(object):
type = "狗" # 类属性

def __init__(self):
self.type = "dog" # 对象属性

# 创建对象
dog1 = Dog()

print(dog1.type) # 结果为 “dog” 类属性和实例属性同名,使用 实例对象 访问的是 实例属性
Copy

2> 类属性只能通过类对象修改,不能通过实例对象修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lass Dog(object):
type = "狗" # 类属性

# 创建对象
dog1 = Dog()
dog1.type = "dog" # 使用 实例对象 创建了对象属性type

print(dog1.type) # 结果为 “dog” 类属性和实例属性同名,访问的是实例属性
print(Dog.type) # 结果为 "狗" 访问类属性

# 只有使用类名才能修改类属性
Dog.type = "土狗"
print(Dog.type) # 土狗
dog2 = Dog()
print(dog2.type) # 土狗
Copy

3> 类属性也可以设置为私有,前边添加两个下划线。 如:

1
2
3
4
5
6
class Dog(object):
count = 0 # 公有的类属性
__type = "狗" # 私有的类属性

print(Dog.count) # 正确
print(Dog.__type) # 错误,私有属性,外部无法访问。

私有属性和方法

类似于封装

在实际开发中,对象的某些属性或者方法可能只希望在对象的内部别使用,而不希望在外部被访问到,这时就可以定义私有属性和私有方法。

定义方法

在定义属性或方法时,在属性名或者方法名前增加两个下划线__,定义的就是私有属性或方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
self.__money = 2000 # 使用 __ 修饰的属性,是私有属性

def __shopping(self, cost):
self.__money -= cost # __money 只能在对象内部使用
print('还剩下%d元'%self.__money)

def test(self):
self.__shopping(200) # __shopping 方法也只能在对象内部使用

p = Person('张三',18)
# print(p.__money) 这里会报错,不能直接访问对象内部的私有属性
p.test()
# p.__shopping() 这里会报错,__shopping 只能在对象内部使用,外部无法访问
Copy

访问私有属性和方法

私有属性不能直接使用,私有方法不能直接调用。但是,通过一些代码,我们也可以在外部访问一个对象的私有属性和方法。

直接访问

使用方式:在私有属性名或方法名前添加 _类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
self.__money = 2000

def __shopping(self, cost):
self.__money -= cost


p = Person('李四',20)
print(p._Person__money) # 使用对象名._类名__私有属性名 可以直接访问对象的私有属性
p._Person__shopping(100) # 使用对象名._类名__函数名 可以直接调用对象的私有方法
print(p._Person__money)
Copy

注意:在开发中,我们强烈不建议使用 对象名._类名__私有属性名 的方式来访问对象的私有属性!

定义方法访问私有变量

在实际开发中,如果对象的变量使用了__ 来修饰,就说明它是一个私有变量,不建议外部直接使用和修改。如果硬要修改这个属性,可以使用定义getset方法这种方式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
self.__money = 2000 # __money 是私有变量,外部无法访问

def get_money(self): # 定义了get_money 方法,在这个方法里获取到 __money
return self.__money # 内部可以访问 __money 变量

def set_money(self,money): # 定义了set_money 方法,在这个方法里,可以修改 __money
self.__money = money

p = Person('王五',21)

# 外部通过调用 get_money 和 set_money 这两个公开方法获取和修改私有变量
print(p.get_money())
p.set_money(8000)
print(p.get_money())

进阶

类方法、静态方法

1. 类方法

  • 第一个形参是类对象的方法
  • 需要用装饰器@classmethod来标识其为类方法,对于类方法,第一个参数必须是类对象,一般以cls作为第一个参数。
1
2
3
4
5
6
7
8
9
class Dog(object):
__type = "狗"

# 类方法,用classmethod来进行修饰
@classmethod
def get_type(cls):
return cls.__type
print(Dog.get_type())
Copy

使用场景:

  • 当方法中 需要使用类对象 (如访问私有类属性等)时,定义类方法
  • 类方法一般和类属性配合使用

2. 静态方法

  • 需要通过装饰器@staticmethod来进行修饰
  • 静态方法既不需要传递类对象也不需要传递实例对象(形参没有self/cls)
  • 静态方法 也能够通过 实例对象类对象 去访问。
  • 跟类方法一样,也只能访问类的属性和方法,对象的是无法访问的。
  • 创建时机:跟类方法一样,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog(object):
type = "狗"

def __init__(self):
name = None

# 静态方法
@staticmethod
def introduce(): # 静态方法不会自动传递实例对象和类对象
print("犬科哺乳动物,属于食肉目..")

dog1 = Dog()
Dog.introduce() # 可以用 实例对象 来调用 静态方法
dog1.introduce() # 可以用 类对象 来调用 静态方法
Copy

使用场景:

  • 当方法中 既不需要使用实例对象(如实例对象,实例属性),也不需要使用类对象 (如类属性、类方法、创建实例等)时,定义静态方法
  • 取消不需要的参数传递,有利于 减少不必要的内存占用和性能消耗

注意点:

  • 类中定义了同名的方法时,调用方法会执行最后定义的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dog:

def demo_method(self):
print("对象方法")

@classmethod
def demo_method(cls):
print("类方法")

@staticmethod
def demo_method(): # 被最后定义
print("静态方法")

dog1 = Dog()
Dog.demo_method() # 结果: 静态方法
dog1.demo_method() # 结果: 静态方法

PS:python面向对象中静态方法和类方法有什么区别

区别:

  1. 装饰器不同。
  2. 类方法是有参数的,静态方法是没参数的。

相同:

  1. 只能访问类的属性和方法,对象是无法访问的。
  2. 都可以通过类名来访问。
  3. 都可以在创建对象前使用。

PS:普通方法与两者的区别

不同:

  1. 没有装饰器
  2. 普通方法是永远依赖对象,因为每个普通方法都有一个self
  3. 只有创建了对象才能调用普通方法,否则都无法使用。

单列设计模式

__new__和__init__方法

1
2
3
4
5
6
7
8
9
10
class A(object):
def __init__(self):
print("这是 init 方法")

def __new__(cls):
print("这是 new 方法")
return object.__new__(cls)

A()

总结

  • __new__至少要有一个参数cls,代表要实例化的类,此参数在实例化时由Python解释器自动提供
  • __new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意,可以return父类__new__出来的实例,或者直接是object的__new__出来的实例
  • __init__有一个参数self,就是这个__new__返回的实例,__init____new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值

单例设计模式

举个常见的单例模式例子,我们日常使用的电脑上都有一个回收站,在整个操作系统中,回收站只能有一个实例,整个系统都使用这个唯一的实例,而且回收站自行提供自己的实例。因此回收站是单例模式的应用。

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,单例模式是一种对象创建型模式。

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
# 实例化一个单例
class Singleton(object):
__instance = None
__is_first = True

def __new__(cls, age, name):
if not cls.__instance:
cls.__instance = object.__new__(cls)
return cls.__instance

def __init__(self, age, name):
if self. __is_first: # 不会再创建第二个对象
self.age = age
self.name = name
Singleton. __is_first = False


a = Singleton(18, "张三")
b = Singleton(28, "张三")

print(id(a))
print(id(b))

print(a.age) # 18
print(b.age) # 18

a.age = 19
print(b.age)

继承的基本使用

在现实生活中,继承一般指的是子女继承父辈的财产,父辈有的财产,子女能够直接使用。

PS:面向对象语言中的HAS-A,IS-A术语

“IS-A” 和 “HAS-A” 是面向对象编程语言中的两个术语:

  1. IS-A(继承关系):表示一个对象是另一个对象的子类(或派生类)。例如,一个苹果(Apple)是一个水果(Fruit),因此苹果类可以从水果类继承一些属性和方法。
  2. HAS-A(关联关系):表示一个对象包含另一个对象作为其一部分。例如,一个汽车(Car)有一个发动机(Engine),因此汽车类可以包含一个发动机类的实例作为其属性或成员变量。

IS-A 和 HAS-A 都是面向对象编程中非常基础和重要的概念。IS-A 帮助我们组织和继承类之间的关系,而 HAS-A 则帮助我们描述和构建复杂的对象关系。

程序里的继承

继承是面向对象软件设计中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。

image

  • 在程序中,继承描述的是多个类之间的所属关系。
  • 如果一个类A里面的属性和方法可以复用,则可以通过继承的方式,传递到类B里。
  • 那么类A就是基类,也叫做父类;类B就是派生类,也叫做子类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal:
def __int__(self):
pass
"""动物类"""
def sleep(self):
print('正在睡觉')


class Dog(Animal):
"""Dog类继承自Animal类"""
def __init__(self):
pass

class Cat(Animal): # 定义类时,在括号后面传入父类的类名,表示子类继承父类
"""Cat类继承自Animal类"""
def __int__(self):
pass

# Dog 和 Cat 都继承自Animal类,可以直接使用Animal类里的sleep方法
dog = Dog()
dog.sleep()

cat = Cat()
cat.sleep()

继承的注意使用

在Python中,继承可以分为单继承、多继承和多层继承。

单继承:子类只继承一个父类

继承概念:子类用于父类的所有的方法和属性。

image

继承语法:

1
2
3
class 类名(父类名):
pass

  • 子类继承自父类,可以享受父类中已经封装好的方法,不需要再次定义
  • 子类中应该根据职责,封装子类特有的属性和方法。
继承的传递性

Dog类继承自Animal,XiaoTianQuan又继承自Dog类,那么XiaoTianQuan类就具有了Animal类里的所有属性和方法。

子类拥有父类以及父类的父类中封装的所有属性和方法。

思考:

XiaoTianQuan能否调用Animal的run()方法? XiaoTianQUan能够调用Cat里的方法?

多继承

子类可以拥有多个父类,并且具有所有父类的属性和方法。

image

语法格式:

1
2
class 子类名(父类名1,父类名2...)
pass
多继承的使用注意事项

思考:

如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪个父类的方法? 说明:开发中,应该尽量避免这种容易产生混淆的情况。如果多个父类之间存在同名的属性后者方法,应该尽量避免使用多继承。

image

Python中的MRO

  • Python中针对类提供了一个内置属性__mro__可以用来查看方法的搜索顺序。
  • MRO 是method resolution order的简称,主要用于在多继承时判断方法属性的调用顺序。
1
print(C.__mro__)

输出结果:

1
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)s
  • 在调用方法时,按照__mro__的输出结果从左至右的顺序查找。
  • 如果再当前类中找到方法,就直接执行,不再向下搜索。
  • 如果没有找到,就顺序查找下一个类中是否有对应的方法,如果找到,就直接执行,不再继续向下搜索。
  • 如果找到了最后一个类,依然没有找到方法,程序就会报错。

新式类和旧式(经典)类

object是Python中所有对象的基类,提供了一些内置的属性和方法,可以时候用dir函数查看。

  • 新式类:以object为基类的类,推荐使用
  • 经典类:不以object为基类的类,不推荐使用
  • 在 Python3.x 以后定义类时,如果没有指定父类,这个类会默认继承自 object,所以,python3.x版本定义的类都是新式类。
  • 在Python2.x中定义类时,如果没有指定父类,则不会继承自object.

为了保证代码在Python2.x和Python3.x中都能够运行,在定义类时,如果一个类没有父类,建议统一继承自’object’

1
2
class 类名(object):
pass

继承的特点:

  1. 如果类中不定义__init__,调用父类super class 的__init__
  2. 如果类继承父类也需要定义自己的__init__,就需要在当前类的__init__调用一下父类__init__
  3. 如何调用父类__init__:
    • super().__init__(参数)
    • super(类名,对象).__init__(参数)
  4. 如果父类有一个方法,子类也有一个同名的方法,那默认搜索的原则是:先找当前类,再去找父类。父类提供的方法不能满足子类的需求,就需要在子类中定义一个同名的方法,这种行为称为重写。

对象相关的运算符和内置函数

Python中的身份运算符用来判断两个对象是否相等;isinstance用来判断对象和类之间的关系;issublcass用啊里判断类与类之间的关系。

身份运算符

身份运算符用来比较两个对象的内存地址,看这两个对象是否是同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age


p1 = Person('张三', 18)
p2 = Person('张三', 18)
p3 = p1

print(p1 is p2) # False
print(p1 is p3) # True
Copy

isinstance

instance内置函数,用来判断一个实例对象是否是由某一个类(或者它的子类)实例化创建出来的。

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 Person(object):
def __init__(self, name, age):
self.name = name
self.age = age


class Student(Person):
def __init__(self, name, age, score):
super(Student, self).__init__(name, age)
self.score = score


class Dog(object):
def __init__(self, name, color):
self.name = name
self.color = color


p = Person('tony', 18)
s = Student('jack', 20, 90)
d = Dog('旺财', '白色')

print(isinstance(p, Person)) # True.对象p是由Person类创建出来的
print(isinstance(s, Person)) # True.对象s是有Person类的子类创建出来的
print(isinstance(d, Person)) # False.对象d和Person类没有关系
Copy

issubclass

issubclass 用来判断两个类之间的继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age


class Student(Person):
def __init__(self, name, age, score):
super(Student, self).__init__(name, age)
self.score = score


class Dog(object):
def __init__(self, name, color):
self.name = name
self.color = color


print(issubclass(Student, Person)) # True
print(issubclass(Dog, Person)) # False

多态

面向对象的三大特性:

  • 封装:这是定义类的准则,根据对象的特点,将行为和属性抽象出来,封装到一个类中。
  • 继承:这是设计类的技巧。父类与子类,主要体现在代码的重用,不需要大量的编写重复代码。
  • 多态:不同的子类调用相同的父类方法,产生不同的执行结果,可以增加代码的外部灵活度。多态是以继承和重写父类方法为前提的,它是一种调用方法的技巧,不会影响到类的内部设计。

场景

  • 提供三个类:缉毒犬、军犬、人
  • 缉毒犬–>追查毒品,军犬–>攻击假人,人–>让小狗干活
  • 设计类来完成功能。

image

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ArmyDog(object):

def bite_enemy(self):
print('追击敌人')

class DrugDog(object):

def track_drug(self):
print('追查毒品')

class Person(object):

def work_with_army(self, dog):
dog.bite_enemy()

def work_with_drug(self, dog):
dog.track_drug()

ad = ArmyDog()
dd = DrugDog()

p = Person()
p.work_with_army(ad)
p.work_with_drug(dd)

思考:这段代码设是否有问题?

新增需求:此时,又多了一个犬种,就又需要在Person类里新建一个方法,让这个方法操作新的狗。

1
2
3
4
5
6
7
8
9
class XiaoTianDog(object):

def eat_moon(self):
print('哮天犬把月亮吃了')

class Person(object):

def work_with_xiaotian(self, dog): # 添加方法
dog.eat_moon()

Person 类总是不断的添加新的功能,每次都需要改动Person类的源码,程序的扩展性太差了!

  • 最好是提供一个父类 Dog,具备 work 的功能,其他小狗继承它,这样只要是小狗类,则行为被统一起来了,我们人类完全可以保证,只要是小狗的子类,找它干活肯定不会有问题。
  • 这样人只要一个方法就能逗任意种类的狗玩,哪怕是添加新的狗,人的类都不需要修改。

代码实现:

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
class Dog(object):

def work(self): # 父类提供统一的方法,哪怕是空方法
pass

class ArmyDog(Dog): # 继承 Dog

def work(self): # 子类重写方法,并且处理自己的行为
print('追击敌人')

class DrugDog(Dog):

def work(self):
print('追查毒品')

class Person(object):

def work_with_dog(self, dog):
dog.work() # 使用小狗可以根据对象的不同而产生不同的运行效果, 保障了代码的稳定性

# 子类对象可以当作父类来使用
dog = Dog()
ad = ArmyDog()
dd = DrugDog()


p = Person()
p.work_with_dog(dog)
p.work_with_dog(ad) # 同一个方法,只要是 Dog 的子类就可以传递,提供了代码的灵活性
p.work_with_dog(dd) # 并且传递不同对象,最终 work_with_dog 产生了不同的执行效果

  • 最终效果
    • Person 类中只需要调用 Dog 对象 work() 方法,而不关心具体是 什么狗
    • work() 方法是在 Dog 父类中定义的,子类重写并处理不同方式的实现
    • 在程序执行时,传入不同的 Dog 对象作为实参,就会产生不同的执行效果

多态总结

  • 定义:多态是一种使用对象的方式,子类重写父类方法,调用不同子类对象的相同父类方法,可以产生不同的执行结果
  • 好处:调用灵活,有了多态,更容易编写出通用的代码,做出通用的编程,以适应需求的不断变化!
  • 实现步骤:
    • 定义父类,并提供公共方法
    • 定义子类,并重写父类方法
    • 传递子类对象给调用者,可以看到不同子类执行效果不同

文件操作

序列化反序列化和异常处理

迭代器生成器

学习目标

  • 能够写出高级装饰器案例
  • 说出迭代器和生成器的概念
  • 能够自定义可迭代对象
  • 能够使用迭代器和生成器实现基本功能
  • 能够使用property属性来修改对象的属性

迭代器

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

可迭代对象

我们已经知道可以对list、tuple、str等类型的数据使用for…in…的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代

但是,是否所有的数据类型都可以放到for…in…的语句中,然后让for…in…每次从中取出一条数据供我们使用,即供我们迭代吗?

1
2
3
4
5
6
7
8
9
>>> for i in 100:
... print(i)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
# int整型不是iterable,即int整型不是可以迭代的
Copy

我们把可以通过for…in…这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable)。

如何判断一个对象是否可以迭代

可以使用 isinstance() 判断一个对象是否是 Iterable 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [50]: from collections import Iterable

In [51]: isinstance([], Iterable)
Out[51]: True

In [52]: isinstance({}, Iterable)
Out[52]: True

In [53]: isinstance('abc', Iterable)
Out[53]: True

In [54]: isinstance(mylist, Iterable)
Out[54]: False

In [55]: isinstance(100, Iterable)
Out[55]: False
Copy

可迭代对象的本质

我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for…in…中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为**迭代器(Iterator)**。

可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。

可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.

那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections.abc import Iterable
class Demo(object):
def __init__(self, n):
self.n = n
self.current = 0
def __iter__(self):
pass

demo = Demo(10)
print(isinstance(demo, Iterable)) # True

for d in demo: # 重写了 __iter__ 方法以后,demo就是一个一个可迭代对象了,可以放在for...in的后面
print(d)

# 此时再使用for...in循环遍历,会提示 TypeError: iter() returned non-iterator of type 'NoneType'
# 这是因为,一个可迭代对象如果想要被for...in循环,它必须要有一个迭代器
Copy

迭代器Iterator

通过上面的分析,我们已经知道,迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法(Python3中是对象的__next__方法,Python2中是对象的next()方法)。**所以,我们要想构造一个迭代器,就要实现它的**next**方法**。但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。

**一个实现了**iter**方法和*next*方法的对象,就是迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyIterator(object):
def __init__(self, n):
self.n = n
self.current = 0

# 自定义迭代器需要重写__iter__和__next__方法
def __iter__(self):
return self

def __next__(self):
if self.current < self.n:
value = self.current
self.current += 1
return value
else:
raise StopIteration

my_it = MyIterator(10)

for i in my_it: # 迭代器重写了__iter__方法,它本身也是一个可迭代对象
print(i)
Copy

如何判断一个对象是否是迭代器

调用一个对象的__iter__方法,或者调用iter()内置函数,可以获取到一个可迭代对象的迭代器。

1
2
3
4
names = ['hello', 'good', 'yes']
print(names.__iter__()) # 调用对象的__iter__()方法
print(iter(names)) # 调用iter()内置函数
Copy

可以使用 isinstance() 判断一个对象是否是 Iterator 对象:

1
2
3
4
from collections.abc import Iterator
names = ['hello', 'good', 'yes']
print(isinstance(iter(names), Iterator))
Copy

for…in…循环的本质

for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

现在我们想要通过for…in…循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

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
class FibIterator(object):
"""斐波那契数列迭代器"""
def __init__(self, n):
"""
:param n: int, 指明生成数列的前n个数
"""
self.n = n
# current用来保存当前生成到数列中的第几个数了
self.current = 0
# num1用来保存前前一个数,初始值为数列中的第一个数0
self.num1 = 0
# num2用来保存前一个数,初始值为数列中的第二个数1
self.num2 = 1

def __next__(self):
"""被next()函数调用来获取下一个数"""
if self.current < self.n:
num = self.num1
self.num1, self.num2 = self.num2, self.num1+self.num2
self.current += 1
return num
else:
raise StopIteration

def __iter__(self):
"""迭代器的__iter__返回自身即可"""
return self


if __name__ == '__main__':
fib = FibIterator(10)
for num in fib:
print(num, end=" ")

生成器

生成器

利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器

创建生成器方法1

要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

1
2
3
4
5
6
7
8
9
10
11
12
In [15]: L = [ x*2 for x in range(5)]

In [16]: L
Out[16]: [0, 2, 4, 6, 8]

In [17]: G = ( x*2 for x in range(5))

In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>

In [19]:
Copy

创建 L 和 G 的区别仅在于最外层的 [ ] 和 ( ) , L 是一个列表,而 G 是一个生成器。我们可以直接打印出列表L的每一个元素,而对于生成器G,我们可以按照迭代器的使用方法来使用,即可以通过next()函数、for循环、list()等方法使用。

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
In [19]: next(G)
Out[19]: 0

In [20]: next(G)
Out[20]: 2

In [21]: next(G)
Out[21]: 4

In [22]: next(G)
Out[22]: 6

In [23]: next(G)
Out[23]: 8

In [24]: next(G)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(G)

StopIteration:

In [25]:
In [26]: G = ( x*2 for x in range(5))

In [27]: for x in G:
....: print(x)
....:
0
2
4
6
8

In [28]:
Copy

创建生成器方法2

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的 for 循环无法实现的时候,还可以用函数来实现。

我们仍然用上一节提到的斐波那契数列来举例,回想我们在上一节用迭代器的实现方式:

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
class FibIterator(object):
"""斐波那契数列迭代器"""
def __init__(self, n):
"""
:param n: int, 指明生成数列的前n个数
"""
self.n = n
# current用来保存当前生成到数列中的第几个数了
self.current = 0
# num1用来保存前前一个数,初始值为数列中的第一个数0
self.num1 = 0
# num2用来保存前一个数,初始值为数列中的第二个数1
self.num2 = 1

def __next__(self):
"""被next()函数调用来获取下一个数"""
if self.current < self.n:
num = self.num1
self.num1, self.num2 = self.num2, self.num1+self.num2
self.current += 1
return num
else:
raise StopIteration

def __iter__(self):
"""迭代器的__iter__返回自身即可"""
return self
Copy

注意,在用迭代器实现的方式中,我们要借助几个变量(n、current、num1、num2)来保存迭代的状态。现在我们用生成器来实现一下。

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
In [30]: def fib(n):
....: current = 0
....: num1, num2 = 0, 1
....: while current < n:
....: yield num1
....: num1, num2 = num2, num1+num2
....: current += 1
....: return 'done'
....:

In [31]: F = fib(5)

In [32]: next(F)
Out[32]: 1

In [33]: next(F)
Out[33]: 1

In [34]: next(F)
Out[34]: 2

In [35]: next(F)
Out[35]: 3

In [36]: next(F)
Out[36]: 5

In [37]: next(F)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-37-8c2b02b4361a> in <module>()
----> 1 next(F)

StopIteration: done
Copy

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。简单来说:只要在def中有yield关键字的 就称为 生成器

此时按照调用函数的方式( 案例中为F = fib(5) )使用生成器就不再是执行函数体了,而是会返回一个生成器对象( 案例中为F ),然后就可以按照使用迭代器的方式来使用生成器了。

1
2
3
4
5
6
7
8
9
10
11
In [38]: for n in fib(5):
....: print(n)
....:
1
1
2
3
5

In [39]:
Copy

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [39]: g = fib(5)

In [40]: while True:
....: try:
....: x = next(g)
....: print("value:%d"%x)
....: except StopIteration as e:
....: print("生成器返回值:%s"%e.value)
....: break
....:
value:1
value:1
value:2
value:3
value:5
生成器返回值:done

In [41]:
Copy

总结

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
  • yield关键字有两点作用:
    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
    • 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
  • 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
  • Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)。

使用send唤醒

我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。

例子:执行到yield时,gen函数作用暂时保存,返回i的值; temp接收下次c.send(“python”),send发送过来的值,c.next()等价c.send(None)

1
2
3
4
5
6
7
8
In [10]: def gen():
....: i = 0
....: while i<5:
....: temp = yield i
....: print(temp)
....: i+=1
....:
Copy

使用send

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:
Copy

使用next函数

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
In [11]: f = gen()

In [12]: next(f)
Out[12]: 0

In [13]: next(f)
None
Out[13]: 1

In [14]: next(f)
None
Out[14]: 2

In [15]: next(f)
None
Out[15]: 3

In [16]: next(f)
None
Out[16]: 4

In [17]: next(f)
None
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)

StopIteration:
Copy

使用__next__()方法(不常使用)

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
In [18]: f = gen()

In [19]: f.__next__()
Out[19]: 0

In [20]: f.__next__()
None
Out[20]: 1

In [21]: f.__next__()
None
Out[21]: 2

In [22]: f.__next__()
None
Out[22]: 3

In [23]: f.__next__()
None
Out[23]: 4

In [24]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()

StopIteration:

property属性

property属性是一种用起来像是实例属性一样的特殊属性,可以对应于某个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo:
def func(self):
pass

# 定义property属性
@property
def prop(self):
pass

# ############### 调用 ###############
foo_obj = Foo()
foo_obj.func() # 调用实例方法
foo_obj.prop # 调用property属性
Copy

property属性的定义和调用要注意一下几点

  • 定义时,在实例方法的基础上添加 @property 装饰器;并且仅有一个self参数

  • 调用时,无需括号

    1
    2
    3
      方法:foo_obj.func()
    property属性:foo_obj.prop
    Copy

简单的实例

对于京东商城中显示电脑主机的列表页面,每次请求不可能把数据库中的所有内容都显示到页面上,而是通过分页的功能局部显示,所以在向数据库中请求数据时就要显示的指定获取从第m条到第n条的所有数据 这个分页的功能包括:

  • 根据用户请求的当前页和总数据条数计算出 m 和 n
  • 根据m 和 n 去数据库中请求数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ############### 定义 ###############
class Pager:
def __init__(self, current_page):
# 用户当前请求的页码(第一页、第二页...)
self.current_page = current_page
# 每页默认显示10条数据
self.per_items = 10

@property
def start(self):
val = (self.current_page - 1) * self.per_items
return val

@property
def end(self):
val = self.current_page * self.per_items
return val

# ############### 调用 ###############
p = Pager(1)
p.start # 就是起始值,即:m
p.end # 就是结束值,即:n
Copy

从上述可见

  • Python的property属性的功能是:property属性内部进行一系列的逻辑计算,最终将计算结果返回。

property属性的两种方式

  • 装饰器 即:在方法上应用装饰器
  • 类属性 即:在类中定义值为property对象的类属性

装饰器方式

在类的实例方法上应用@property装饰器

Python中的类有经典类和新式类,新式类的属性比经典类的属性丰富。( 如果类继object,那么该类是新式类 )

  • 经典类的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Goods:
    @property
    def price(self):
    return "laowang"

    obj = Goods()
    result = obj.price # 自动执行 @property 修饰的 price 方法,并获取方法的返回值
    print(result)
    Copy
  • 新式类的实现:

    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
    class Goods:
    """
    只有在python3中才有@xxx.setter @xxx.deleter
    """
    def __init__(self):
    # 原价
    self.original_price = 100
    # 折扣
    self.discount = 0.8

    @property
    def price(self):
    new_price = self.original_price * self.discount
    return new_price

    @price.setter
    def price(self, value):
    self.original_price = value

    @price.deleter
    def price(self):
    del self.original_price
    obj = Goods()
    obj.price # 获取商品价格
    obj.price = 200 # 修改商品原价
    del obj.price # 删除商品原价
    Copy

总结

  • 经典类中的属性只有一种访问方式,其对应被 @property 修饰的方法
  • 新式类中的属性有三种访问方式,并分别对应了三个被@property、@方法名.setter、@方法名.deleter修饰的方法

类属性方式

  • 当使用类属性的方式创建property属性时,经典类和新式类无区别。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Foo:
    def get_bar(self):
    return 'laowang'
    BAR = property(get_bar)

    obj = Foo()
    reuslt = obj.BAR # 自动调用get_bar方法,并获取方法的返回值
    print(reuslt)
    Copy

    property方法中有个四个参数

    • 第一个参数是方法名,调用 对象.属性 时自动触发执行方法
    • 第二个参数是方法名,调用 对象.属性 = XXX 时自动触发执行方法
    • 第三个参数是方法名,调用 del 对象.属性 时自动触发执行方法
    • 第四个参数是字符串,调用 对象.属性.doc ,此参数是该属性的描述信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Foo(object):
def get_bar(self):
print("getter...")
return 'laowang'

def set_bar(self, value):
"""必须两个参数"""
print("setter...")
return 'set value' + value

def del_bar(self):
print("deleter...")
return 'laowang'

BAR = property(get_bar, set_bar, del_bar, "description...")

obj = Foo()

obj.BAR # 自动调用第一个参数中定义的方法:get_bar
obj.BAR = "alex" # 自动调用第二个参数中定义的方法:set_bar方法,并将“alex”当作参数传入
desc = Foo.BAR.__doc__ # 自动获取第四个参数中设置的值:description...
print(desc)
del obj.BAR # 自动调用第三个参数中定义的方法:del_bar方法
Copy

总结

  • 定义property属性共有两种方式,分别是【装饰器】和【类属性】,而【装饰器】方式针对经典类和新式类又有所不同。
  • 通过使用property属性,能够简化调用者在获取数据的流程。

网络编程

  • 说出网络通信和网络编程的概念
  • 说出IP地址和子网掩码的作用
  • 说出网络通信的几种方式
  • 说出端口号的作用
  • 说出什么是Socket以及它的作用
  • 说出TCP/UDP协议的特点
  • 使用socket实现基于UDP的消息发送和接收
  • 使用socket实现基于TCP的服务器和客户端通信

网络通信的概念

简单来说,网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。

使用网络的目的,就是为了联通多方然后进行通信,即把数据从一方传递给另外一方。

前面的学习编写的程序都是单机的,即不能和其他电脑上的程序进行通信。为了让在不同的电脑上运行的软件,之间能够互相传递数据,就需要借助网络的功能。

  • 使用网络能够把多方链接在一起,然后可以进行数据传递
  • 所谓的网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信

image-20230314204938956

ip地址

生活中的地址指的就是,找到某人或某机关或与其通信的指定地点。在网络编程中,如果一台主机想和另一台主机进行沟通和共享数据,首先要做的第一件事情就是要找到对方。在互联网通信中,我们使用IP地址来查询到各个主机。

image

ip地址:用来在网络中标记一台电脑,比如192.168.1.1;在本地局域网上是唯一的。

ip地址的分类

每一个IP地址包括两部分:网络地址和主机地址。IP地址通常由点分十进制(例如:192.168.1.1)的方式来表示,IP地址要和子网掩码(用来区分网络位和主机位)配合使用。

image

A类地址

一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”,

地址范围:1.0.0.1-126.255.255.254

子网掩码:255.0.0.0

二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110

可用的A类网络有126个,每个网络能容纳1677214个主机

B类地址

一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,

地址范围:128.1.0.1-191.255.255.254

子网掩码:255.255.0.0

二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110

可用的B类网络有16384个,每个网络支持的最大主机数为256的2次方-2=65534台。

C类地址

一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”

范围:192.0.1.1-223.255.255.254

子网掩码:255.255.255.0

二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110

C类网络可达2097152个,每个网络支持的最大主机数为256-2=254台

D类地址

D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址,并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。

E类地址

以“1111”开始,为将来使用保留,仅作实验和开发用。

私有地址

在这么多网络IP中,国际规定有一部分IP地址是用于我们的局域网使用,也就是属于私网IP,不在公网中使用的,它们的范围是:

1
2
3
4
10.0.0.010.255.255.255
172.16.0.0172.31.255.255
192.168.0.0192.168.255.255
Copy

注意事项:

  1. 每一个字节都为0的地址(“0.0.0.0”)对应于当前主机。
  2. IP地址中的每一个字节都为1的IP地址(“255.255.255.255”)是当前子网的广播地址。
  3. IP地址中凡是以“1111”开头的E类IP地址都保留用于将来和实验使用。
  4. IP地址中不能以十进制“127”作为开头,该类地址中数字127.0.0.1到127.255.255.255用于回路测试,如:127.0.0.1可以代表本机IP地址,用 http://127.0.0.1 就可以测试本机中配置的Web服务器
  5. 网络ID的第一个8位组也不能全置为“0”,全“0”表示本地网络。

网络通信方式

直接通信

image

说明

  1. 如果两台电脑之间通过网线连接是可以直接通信的,但是需要提前设置好ip地址以及网络掩码
  2. 并且ip地址需要控制在同一网段内,例如 一台为192.168.1.1另一台为192.168.1.2则可以进行通信

使用集线器通信

image

说明

  1. 当有多态电脑需要组成一个网时,那么可以通过集线器(Hub)将其链接在一起
  2. 一般情况下集线器的接口较少
  3. 集线器有个缺点,它以广播的方式进行发送任何数据,即如果集线器接收到来自A电脑的数据本来是想转发给B电脑,如果此时它还连接着另外两台电脑C、D,那么它会把这个数据给每个电脑都发送一份,因此会导致网络拥堵

使用交换机通信

image

说明

  1. 克服了集线器以广播发送数据的缺点,当需要广播的时候发送广播,当需要单播的时候又能够以单播的方式进行发送
  2. 它已经替代了之前的集线器
  3. 企业中就是用交换机来完成多态电脑设备的链接成网络的

使用路由器连接多个网络

image

复杂的通信过程

image

说明

  1. 在浏览器中输入一个网址时,需要将它先解析出ip地址来
  2. 当得到ip地址之后,浏览器以tcp的方式3次握手链接服务器
  3. 以tcp的方式发送http协议的请求数据 给 服务器
  4. 服务器tcp的方式回应http协议的应答数据 给浏览器

总结

  • MAC地址:在设备与设备之间数据通信时用来标记收发双方(网卡的序列号)
  • IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)
  • 网络掩码:用来区分ip地址的网络号和主机号
  • 默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,成为网关
  • 集线器:已过时,用来连接多态电脑,缺点:每次收发数据都进行广播,网络会变的拥堵
  • 交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播
  • 路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化
  • DNS:用来解析出IP(类似电话簿)
  • http服务器:提供浏览器能够访问到的数据

端口

端口就像一个房子的门,是出入这间房子的必经之路。如果一个程序需要收发网络数据,那么就需要有这样的端口

在linux系统中,端口可以有65536(2的16次方)个之多!

既然有这么多,操作系统为了统一管理,所以进行了编号,这就是端口号

端口号

端口是通过端口号来标记的,端口号只有整数,范围是从0到65535.端口号不是随意使用的,而是按照一定的规定进行分配。端口的分类标准有好几种,我们这里不做详细讲解,只介绍一下知名端口和动态端口。

知名端口号

知名端口是众所周知的端口号,范围从0到1023,以理解为,一些常用的功能使用的号码是估计的,好比 电话号码110、10086、10010一样。一般情况下,如果一个程序需要使用知名端口的需要有root权限。

动态端口号

动态端口的范围是从1024到65535

之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

动态分配是指当一个系统程序或应用程序程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。

当这个程序关闭时,同时也就释放了所占用的端口号。

端口号作用

我们知道,一台拥有IP地址的主机可以提供许多服务,比如HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。 需要注意的是,端口并不是一一对应的。比如你的电脑作为客户机访问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。

socket简介

1. 不同电脑上的进程之间如何通信

首要解决的问题是如何唯一标识一个进程,否则通信无从谈起! 在1台电脑上可以通过进程号(PID)来唯一标识一个进程,但是在网络中这是行不通的。 其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用进程(进程)。 这样利用ip地址,协议,端口就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

注意:

所谓进程指的是:运行的程序以及运行时用到的资源这个整体称之为进程(在讲解多任务编程时进行详细讲解)

所谓进程间通信指的是:运行的程序之间的数据共享

2. 什么是socket

socket(简称 套接字) 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:

它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的

例如我们每天浏览网页、QQ 聊天、收发 email 等等。

3. 创建socket

在 Python 中 使用socket 模块的函数 socket 就可以完成:

1
2
3
import socket
socket.socket(AddressFamily, Type)
Copy

说明:

函数 socket.socket 创建一个 socket,该函数带有两个参数:

  • Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET
  • Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)

创建一个tcp socket(tcp套接字)

1
2
3
4
5
6
7
8
9
10
import socket

# 创建tcp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# ...这里是使用套接字的功能(省略)...

# 不用的时候,关闭套接字
s.close()
Copy

创建一个udp socket(udp套接字)

1
2
3
4
5
6
7
8
import socket

# 创建udp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# ...这里是使用套接字的功能(省略)...
# 不用的时候,关闭套接字
s.close()
Copy
说明

套接字使用流程 与 文件的使用流程很类似

  1. 创建套接字
  2. 使用套接字收/发数据
  3. 关闭套接字

UDP协议

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中,”写信”。

UDP通信模型

image

UDP发送接收数据

udp网络程序-发送数据

创建一个基于udp的网络程序流程很简单,具体步骤如下:

  1. 创建客户端套接字
  2. 发送/接收数据
  3. 关闭套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import socket
# 1. 创建一个UDP的socket连接
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2. 获取用户输入的内容
data = input('请输入内容')
# 3. 准备接收方的地址和端口号
addr = ('127.0.0.1', 8080)
# 4. 将用户的输入内容进行编码,并发送到指定地址和端口
udp_socket.sendto(data.encode('gbk'), addr)
# 5. 接收传递过来的消息,并指定接受的字节大小
recv_data = udp_socket.recvfrom(1024)
# 6. 接收到的对象是一个元组,元组里有两个元素
print(recv_data)
# 6.1 元组里的第一个数据显示接收到内容
print(recv_data[0].decode('gbk'))
# 6.2 元组里的第二个数据显示发送方的地址和端口号
print(recv_data[1])
# 7. 关闭socket连接
udp_socket.close()
Copy

image

UDP发送接收数据

udp网络程序-端口问题

  • 会变的端口号

重新运行多次脚本,然后在“网络调试助手”中,看到的现象如下:

image

说明:

  • 每重新运行一次网络程序,上图中红圈中的数字,不一样的原因在于,这个数字标识这个网络程序,当重新运行时,如果没有确定到底用哪个,系统默认会随机分配
  • 记住一点:这个网络程序在运行的过程中,这个就唯一标识这个程序,所以如果其他电脑上的网络程序如果想要向此程序发送数据,那么就需要向这个数字(即端口)标识的程序发送即可

UDP绑定信息

绑定信息

一般情况下,在一台电脑上运行的网络程序有很多,为了不与其他的网络程序占用同一个端口号,往往在编程中,udp的端口号一般不绑定 但是如果需要做成一个服务器端的程序的话,是需要绑定的,想想看这又是为什么呢? 如果报警电话每天都在变,想必世界就会乱了,所以一般服务性的程序,往往需要一个固定的端口号,这就是所谓的端口绑定。 img

绑定示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from socket import *

# 1. 创建套接字
udp_socket = socket(AF_INET, SOCK_DGRAM)

# 2. 绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
local_addr = ('', 7788) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(local_addr)

# 3. 等待接收对方发送的数据
recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数

# 4. 显示接收到的数据
print(recv_data[0].decode('gbk'))

# 5. 关闭套接字
udp_socket.close()
Copy

运行结果

image

总结
  • 一个udp网络程序,可以不绑定,此时操作系统会随机进行分配一个端口,如果重新运行此程序端口可能会发生变化
  • 一个udp网络程序,也可以绑定信息(ip地址,端口号),如果绑定成功,那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的

TCP协议

TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

TCP通信需要经过创建连接、数据传送、终止连接三个步骤。

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,”打电话”。

TCP特点

1. 面向连接

通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。

双方间的数据传输都可以通过这一个连接进行。

完成数据交换后,双方必须断开此连接,以释放系统资源。

这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。

2. 可靠传输

1)TCP采用发送应答机制

TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功

2)超时重传

发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。

TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。

3)错误校验

TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

\4) 流量控制和阻塞管理

流量控制用来避免主机发送得过快而使接收方来不及完全收下。

3. TCP与UDP的区别

  • 面向连接(确认有创建三方交握,连接已创建才作传输。)
  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无差错的数据传输
  • 阻塞/流量控制

TCP通信模型

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据

image

正则表达式

正则表达式

正则表达式是一个特殊的字符序列,计算机科学的一个概念。通常被用来检索、替换那些符合某个模式(规则)的文本。

许多程序设计语言都支持利用正则表达式进行字符串操作。在Python中需要通过正则表达式对字符串进行匹配的时候,可以使用re模块。re 模块使 Python 语言拥有全部的正则表达式功能。

特点:

  1. 灵活性、逻辑性和功能性非常强;
  2. 可以迅速地用极简单的方式达到字符串的复杂控制。
  3. 对于刚接触的人来说,比较晦涩难懂。

Python中的正则表达式

与大多数编程语言相同,正则表达式里也使用\作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符\,那么使用编程语言表示的正则表达式里将需要4个反斜杠\:前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。

1
2
print(re.match('\\\\', '\hello'))  # 需要使用四个反斜杠来匹配一个 \
Copy

Python里的原生字符串很好地解决了这个问题,有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。在Python 字符串前面添加r即可将字符串转换成为原生字符串。

1
print(re.match(r'\\', '\hello')) # 使用两个反斜杠即可匹配一个 \

正则查找

在Python中的查找匹配方法,常见的有下面四种,他们的用法大致相同,但是匹配出的结果却不同。

  • match方法(只匹配字符串开头)
  • search方法(扫描整个字符串,找到第一个匹配)
  • findall方法(扫描整个字符串,找到所有的匹配)
  • finditer方法(扫描整个字符串,找到所有的匹配,并返回一个可迭代对象)

match方法的使用

re.match尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none。

函数语法:

1
2
re.match(pattern,string,flags=0)
Copy
参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。

我们可以使用group(num)函数来获取匹配表达式。

1
2
3
4
5
6
7
import re
result1 = re.match(r'H','Hello')
result2 = re.match(r'e','Hello')
print(result1.group(0)) # 'H' 匹配到的元素
print(result1.span()) # (0,1) 匹配到的元素所在位置
print(result2) # None
Copy

search方法的使用

re.search 扫描整个字符串并返回第一个成功的匹配。

函数语法:

1
2
re.search(pattern, string, flags=0)
Copy

示例:

1
2
3
4
5
6
7
8
9
import re
result1 = re.search(r'He','Hello')
result2 = re.search(r'lo','Hello')

print(result1.group(0)) # He
print(result1.span()) # (0,2)
print(result2.group(0)) # lo
print(result2.span()) # (3,5)
Copy

re.match与re.search的区别

re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。

示例:

1
2
3
4
5
result1 = re.search(r'天气','今天天气不错哟')
result2 = re.match(r'天气','今天天气不错哟')
print(result1) # <re.Match object; span=(2, 4), match='天气'>
print(result2) # None
Copy

findall 方法的使用

在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表。

注意: match 和 search 是匹配一次 findall 匹配所有。

语法格式:

1
2
re.findall(pattern,string,flags=0)
Copy

示例代码:

1
2
3
4
5
6
7
ret = re.findall(r'\d+','he23ll34')
print(ret) # ['23', '34']
ret = re.match(r'\d+','he23ll34')
print(ret) # None match只匹配开头,所以匹配到
ret = re.search(r'\d+','he23ll34')
print(ret) # <re.Match object; span=(2, 4), match='23'> search 只能匹配到一个数字
Copy
  • 注意事项:

    findall方法匹配时,如果匹配规则里有分组,则只匹配分组数据。

    1
    2
    3
    ret = re.findall(r'\w+@(qq|126|163)\.com','123@qq.com;aa@163.com;bb@126.com')
    print(ret) # ['qq', '163', '126'] 只匹配到了分组里的内容
    Copy

    如果正则表达式里存在多个分组,则会把多个分组匹配成元组。

    1
    2
    3
    ret = re.findall(r'\w+@(qq|126|163)(\.com)','123@qq.com;aa@163.com;bb@126.com')
    print(ret) #[('qq', '.com'), ('163', '.com'), ('126', '.com')]
    Copy

    如果想要让findall匹配所有的内容,而不仅仅只是匹配正则表达式里的分组,可以使用 ?:来将分组标记为非捕获分组。

    1
    2
    3
    ret = re.findall(r'\w+@(?:qq|126|163)\.com','123@qq.com;aa@163.com;bb@126.com')
    print(ret) # ['123@qq.com', 'aa@163.com', 'bb@126.com']
    Copy

finditer方法的使用

和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回。

1
2
3
ret = re.finditer(r'\d+','he23ll34')  # 得到的结果是一个可迭代对象
for x in ret: # 遍历 ret 取出里面的每一项匹配
print(x.group(), x.span()) # 匹配对象里的group保存了匹配的结果

re.Match类介绍

当我们调用re.match方法、re.search方法,或者对re.finditer方法的结果进行迭代时,拿到的数据类型都是re.Match对象。

1
2
3
4
5
6
7
8
x = re.match(r'h','hello')
y = re.search(r'e','hello')
z = re.finditer(r'l','hello')
print(type(x)) # <class 're.Match'>
print(type(y)) # <class 're.Match'>
for a in z:
print(type(a)) # <class 're.Match'>
Copy

这个类里定义了相关的属性,可以直接让我们来使用。

属性和方法 说 明
pos 搜索的开始位置
endpos 搜索的结束位置
string 搜索的字符串
re 当前使用的正则表达式的对象
lastindex 最后匹配的组索引
lastgroup 最后匹配的组名
group(index=0) 某个分组的匹配结果。如果index等于0,便是匹配整个正则表达式
groups() 所有分组的匹配结果,每个分组的结果组成一个列表返回
groupdict() 返回组名作为key,每个分组的匹配结果座位value的字典
start([group]) 获取组的开始位置
end([group]) 获取组的结束位置
span([group]) 获取组的开始和结束位置
expand(template) 使用组的匹配结果来替换模板template中的内容,并把替换后的字符串返回
1
2
3
4
5
6
7
ret = re.search(r'(abc)+', 'xxxabcabcabcdef')
print(ret.pos) # 搜索开始的位置,默认是0
print(ret.endpos) # 搜索结束的位置,默认是字符串的长度
print(ret.group(0)) # abcabcabc 匹配整个表达式
print(ret.group(1)) # abc 第一次匹配到的结果
print(ret.span()) # (3, 12) 开始和结束位置
print(ret.groups()) # 表示当正则表达式里有多个分组时,多个分组的匹配结果

re.compile方法的使用

我们在使用正则表达式时,可以直接调用re 模块的 match,search,findall等方法,传入指定的正则表达式。同时,也可以调用re.compile方法,生成一个正则表达式对象,再调用这个正则表达式对象的相关方法实现匹配。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
re.match(r'h','hello')  # 可以使用re.match方法直接匹配
# 也可以调用re模块的compile方法,生成一个 Pattern 对象,再调用 Pattern 对象的 match方法
regex = re.compile(r'h')
regex.match('hello')

re.search(r'l','hello')
regex = re.compile(r'l')
regex.match('hello')

regex = re.compile(r'l')
regex.findall('hello')

regex = re.complie(r'l')
regex.finditer('hello')

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符

示例:

1
2
3
4
5
6
7
8
9
10
11
print(re.search(r'L','hello'))  # None
print(re.search(r'L', 'hello', re.I)) # 不区分大小写<re.Match object; span=(2, 3), match='l'>

# \w+$ 表示匹配以一个或者多个字母结尾
# re.M 可以进行多行匹配,每个换行都认为是一个结尾
print(re.findall(r'\w+$','i am boy\n you are girl\n he is man',re.M)) # ['boy', 'girl', 'man']
# 不实用re.M修饰符,只会匹配到最后的 man
print(re.findall(r'\w+$','i am boy\n you are girl\n he is man')) # ['man']

print(re.search(r'.','\n')) # None . 匹配除了 \n 以外的所有字符
print(re.search(r'.','\n',re.S)) # '\n' 匹配到了 \n

正则表达式模式

模式字符串使用特殊的语法来表示一个正则表达式:

  1. 字母和数字表示他们自身,一个正则表达式模式中的字母和数字匹配同样的字符串。

    1
    2
    re.search(r'H','Hello')  # 这里的 H 表示的就是字母 H 自身,代表有特殊含义
    Copy
  2. 多数字母和数字前加一个反斜杠时会拥有不同的含义。

    1
    2
    ret = re.search(r'\d','he12ms90') # 这里的 \d 表示的是匹配数字
    Copy
  3. 标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。

    1
    2
    3
    ret = re.search(r'.','hello') # 这里的 . 表示的是匹配任意字符
    ret = re.search(r'\.','he.llo') # 这里的 \. 进行了转义,才表示标点符号自身。
    Copy
  4. 反斜杠本身需要使用反斜杠转义。由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 **r’\t’**,等价于\\t )匹配相应的特殊字符。

下表列出了正则表达式模式语法中的特殊元素,如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。

非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符 描述
\cx 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\f 匹配一个换页符。等价于 \x0c 和 \cL。
\n 匹配一个换行符。等价于 \x0a 和 \cJ。
\r 匹配一个回车符。等价于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
\t 匹配一个制表符。等价于 \x09 和 \cI。
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

特殊字符

所谓特殊字符,就是一些有特殊含义的字符。若要匹配这些特殊字符,必须首先使字符”转义”,即,将反斜杠字符\ 放在它们前面。下表列出了正则表达式中的特殊字符:

特殊字符 描述
( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 \( 和 \)。
. 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \. 。
[ 标记一个中括号表达式的开始。要匹配 [,请使用 \[。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, ‘n’ 匹配字符 ‘n’。’\n’ 匹配换行符, \\ 匹配 \,而 \( 则匹配 ( 。
{ 标记限定符表达式的开始。要匹配 {,请使用 \{
| 指明两项之间的一个选择。要匹配 |,请使用 |。
\d 匹配一个数字字符。等价于 [0-9]。
[0-9] 匹配任何数字。等价于 \d
\D 匹配一个非数字字符。等价于 [^0-9]
[a-z] 匹配任何小写字母
[A-Z] 匹配任何大写字母
[a-zA-Z0-9] 匹配任何字母及数字。等价于\w
\w 匹配包括下划线的任何单词字符。等价于[A-Za-z0-9_]
\W 匹配任何非单词字符。等价于 [^A-Za-z0-9_]
[\u4e00-\u9fa5] 匹配纯中文

定位符

定位符使您能够将正则表达式固定到行首或行尾。它们还使您能够创建这样的正则表达式,这些正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾。

定位符用来描述字符串或单词的边界,^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。

正则表达式的定位符有:

特殊字符 描述
^ 匹配输入字符串的开始位置,例如:^h匹配以h开头;在方括号表达式中时,它表示不接受该字符集合,例如[^0-9]匹配除了数字以外的数据。要匹配 ^ 字符本身,请使用 ^。
$ 匹配输入字符串的结尾位置。要匹配 $ 字符本身,请使用 \$
\b 匹配一个单词边界,即字与空格间的位置。
\B 非单词边界匹配。

限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有 ***** 或 +?{n}{n,}{n,m} 共6种。

正则表达式的限定符有:

字符 描述
* 匹配前面的子表达式零次或多次。例如,zo 能匹配 “z” 以及 “zoo”。 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
? 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 、 “does” 中的 “does” 、 “doxy” 中的 “do” 。? 等价于 {0,1}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,} n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。

示例

1
2
3
4
5
6
7
re.search(r'\s','大家好 我是 代码')  # 匹配所有的空字符
re.search(r'\S','大家') # 匹配所有的非空字符
re.search(r'\n','大家好\n我是代码') # 匹配换行
re.search(r'n$','hello python') # 匹配以 n 结尾
re.search(r'^h.+n$','hello python') # 匹配以 h 开头,中间出现一次或多次任意字符,并且以n结尾
re.search(r'^ha*','h') # 匹配以 h 开头,a出现0次或者一次
Copy

练习

  1. 用户名匹配:由数字、大小写字母、下划线_和中横线-组成,长度为4到14位,并且不能以数字开头。

    1
    2
    r'^\D[a-z0-9A-Z_\-]{3,13}', 'sH_8'
    Copy
  2. 匹配邮箱

    1
    2
    r'^([A-Za-z0-9_\-\.])+@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$
    Copy
  3. 匹配手机号

    1
    2
    r'^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$'
    Copy
  4. 匹配身份证号。

    1
    2
    r'^[1-9]\d{5}(18|19|20|)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$'
    Copy
  5. 匹配URL地址

    1
    2
    r'((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?'
    Copy
  6. 匹配QQ号

    1
    2
    r'^[1-9][0-9]{4,10}$'
    Copy
  7. 匹配微信号

    1
    2
    r'^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$'
    Copy
  8. 匹配车牌号

    1
    r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$'

正则替换

Python中的re模块提供了re.sub用户替换字符串中的匹配项。

语法:

1
2
re.sub(pattern,repl,string,count=0)
Copy

参数:

  • pattern : 正则中的模式字符串。
  • repl : 替换的字符串,也可为一个函数。
  • string : 要被查找替换的原始字符串。
  • count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
1
2
3
4
5
6
7
8
9
10
phone = "2004-959-559 # 这是一个电话号码"

# 删除注释
num = re.sub(r'#.*$', "", phone)
print ("电话号码 : ", num)

# 移除非数字的内容
num = re.sub(r'\D', "", phone)
print ("电话号码 : ", num)
Copy

repl可以使用一个字符串用来表示替换后的结果以外,还可以传入一个函数。

1
2
3
4
5
6
def double(matched):
test = int(matched.group('test'))
return str(test * 2)


print(re.sub(r'(?P<test>\d+)', double, 'hello23hi34')) # hello46hi68

贪婪和非贪婪模式

Python里数量词默认是贪婪的(在少数语言里也可能是默认非贪婪),总是尝试匹配尽可能多的字符;

非贪婪则相反,总是尝试匹配尽可能少的字符。

*,?,+,{m,n}后面加上 ?使贪婪变成非贪婪。

1
2
3
4
5
6
7
8
9
>>> s="This is a number 234-235-22-423"
>>> r=re.match(".+(\d+-\d+-\d+-\d+)",s)
>>> r.group(1)
'4-235-22-423'
>>> r=re.match(".+?(\d+-\d+-\d+-\d+)",s)
>>> r.group(1)
'234-235-22-423'
>>>
Copy

正则表达式模式中使用到通配字,那它在从左到右的顺序求值时,会尽量“抓取”满足匹配最长字符串,在我们上面的例子里面,“.+”会从字符串的启始处抓取满足模式的最长字符,其中包括我们想得到的第一个整型字段的中的大部分,“\d+”只需一位字符就可以匹配,所以它匹配了数字“4”,而“.+”则匹配了从字符串起始到这个第一位数字4之前的所有字符。

1
2
3
4
5
6
7
8
9
10
>>> re.match(r"aa(\d+)","aa2343ddd").group(1)
'2343'
>>> re.match(r"aa(\d+?)","aa2343ddd").group(1)
'2'
>>> re.match(r"aa(\d+)ddd","aa2343ddd").group(1)
'2343'
>>> re.match(r"aa(\d+?)ddd","aa2343ddd").group(1)
'2343'
>>>
Copy

练习:

字符串为:

1
2
<img data-original="https://rpic.douyucdn.cn/appCovers/2016/11/13/1213973_201611131917_small.jpg" src="https://rpic.douyucdn.cn/appCovers/2016/11/13/1213973_201611131917_small.jpg" style="display: inline;">
Copy

请提取url地址

参考答案

1
re.search(r"https://.*?\.jpg", test_str)

多任务

  • 能够说出进程和线程的概念
  • 能够创建和管理进程
  • 能够实现不同进程之间的通信
  • 能够创建线程并实现不同线程间通信
  • 能够解决线程安全问题

线程

线程安全问题

线程访问全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading
g_num = 0
def test(n):
global g_num
for x in range(n):
g_num += x
g_num -= x
print(g_num)

if __name__ == '__main__':
t1 = threading.Thread(target=test, args=(10,))
t2 = threading.Thread(target=test, args=(10,))
t1.start()
t2.start()
Copy

在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据。缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。

线程的安全问题

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

ticket = 20


def sell_ticket():
global ticket
while True:
if ticket > 0:
time.sleep(0.5)
ticket -= 1
print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket))
else:
print('{}票卖完了'.format(threading.current_thread().name))
break


for i in range(5):
t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1))
t.start()

线程锁

同步

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。同步就是协同步调,按预定的先后次序进行运行。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

互斥锁

互斥锁为资源引入一个状态:锁定/非锁定

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的处理锁定:

1
2
3
4
5
6
7
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
Copy

注意

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。
  • 和文件操作一样,Lock也可以使用with语句快速的实现打开和关闭操作。

使用互斥锁解决卖票问题

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
import threading
import time

ticket = 20
lock = threading.Lock()


def sell_ticket():
global ticket
while True:
lock.acquire()
if ticket > 0:
time.sleep(0.5)
ticket -= 1
lock.release()
print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket))
else:
print('{}票卖完了'.format(threading.current_thread().name))
lock.release()
break


for i in range(5):
t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1))
t.start()
Copy

上锁过程

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

总结

锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

线程间通信

线程之间有时需要通信,操作系统提供了很多机制来实现进程间的通信,其中我们使用最多的是队列Queue.

Queue的原理

Queue是一个先进先出(First In First Out)的队列,主进程中创建一个Queue对象,并作为参数传入子进程,两者之间通过put( )放入数据,通过get( )取出数据,执行了get( )函数之后队列中的数据会被同时删除,可以使用multiprocessing模块的Queue实现多进程之间的数据传递。

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
import threading
import time
from queue import Queue

def producer(queue):
for i in range(100):
print('{}存入了{}'.format(threading.current_thread().name, i))
queue.put(i)
time.sleep(0.1)
return

def consumer(queue):
for x in range(100):
value = queue.get()
print('{}取到了{}'.format(threading.current_thread().name, value))
time.sleep(0.1)
if not value:
return

if __name__ == '__main__':
queue = Queue()
t1 = threading.Thread(target=producer, args=(queue,))
t2 = threading.Thread(target=consumer, args=(queue,))
t3 = threading.Thread(target=consumer, args=(queue,))
t4 = threading.Thread(target=consumer, args=(queue,))
t6 = threading.Thread(target=consumer, args=(queue,))
t1.start()
t2.start()
t3.start()
t4.start()
t6.start()

多线程版聊天

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 socket
import threading

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 8080))


def send_msg():
ip = input('请输入您要聊天的ip:')
port = int(input('请输入对方的端口号:'))
while True:
msg = input('请输入聊天内容:')
s.sendto(msg.encode('utf-8'), (ip, port))
if msg == "bye":
ip = input('请输入您要聊天的ip:')
port = int(input('请输入对方的端口号:'))


def recv_msg():
while True:
content, addr = s.recvfrom(1024)
print('接收到了{}主机{}端口的消息:{}'.format(addr[0], addr[1], content.decode('utf-8')),file=open('history.txt', 'a', encoding='utf-8'))


send_thread = threading.Thread(target=send_msg)
recv_thread = threading.Thread(target=recv_msg)

send_thread.start()
recv_thread.start()

进程

进程

程序:例如xxx.py这是程序,是一个静态的。

进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。

不仅可以通过线程完成多任务,进程也是可以的。

进程的状态

工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态。

  • 就绪态:运行的条件都已经满足,正在等在cpu执行。
  • 执行态:cpu正在执行其功能。
  • 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态。

创建进程

multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。

示例:创建一个进程,执行两个死循环。

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


def run_proc():
"""子进程要执行的代码"""
while True:
print("----2----")
time.sleep(1)


if __name__=='__main__':
p = Process(target=run_proc)
p.start()
while True:
print("----1----")
time.sleep(1)
Copy

说明

  • 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动

方法说明

Process( target [, name [, args [, kwargs]]])

  • target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
  • args:给target指定的函数传递的参数,以元组的方式传递
  • kwargs:给target指定的函数传递命名参数
  • name:给进程设定一个名字,可以不设定

Process创建的实例对象的常用方法:

  • start():启动子进程实例(创建子进程)
  • is_alive():判断进程子进程是否还在活着
  • join([timeout]):是否等待子进程执行结束,或等待多少秒
  • terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

  • name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
  • pid:当前进程的pid(进程号)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from multiprocessing import Process
import os
from time import sleep


def run_proc(name, age, **kwargs):
for i in range(10):
print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age, os.getpid()))
print(kwargs)
sleep(0.2)

if __name__=='__main__':
p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
p.start()
sleep(1) # 1秒中之后,立即结束子进程
p.terminate()
p.join()
Copy

Pool

开启过多的进程并不能提高你的效率,反而会降低你的效率,假设有500个任务,同时开启500个进程,这500个进程除了不能一起执行之外(cpu没有那么多核),操作系统调度这500个进程,让他们平均在4个或8个cpu上执行,这会占用很大的空间。

如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def task(n):
print('{}----->start'.format(n))
time.sleep(1)
print('{}------>end'.format(n))


if __name__ == '__main__':
p = Pool(8) # 创建进程池,并指定线程池的个数,默认是CPU的核数
for i in range(1, 11):
# p.apply(task, args=(i,)) # 同步执行任务,一个一个的执行任务,没有并发效果
p.apply_async(task, args=(i,)) # 异步执行任务,可以达到并发效果
p.close()
p.join()
Copy

进程池获取任务的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def task(n):
print('{}----->start'.format(n))
time.sleep(1)
print('{}------>end'.format(n))
return n ** 2


if __name__ == '__main__':
p = Pool(4)
for i in range(1, 11):
res = p.apply_async(task, args=(i,)) # res 是任务的执行结果
print(res.get()) # 直接获取结果的弊端是,多任务又变成同步的了
p.close()
# p.join() 不需要再join了,因为 res.get()本身就是一个阻塞方法
Copy

异步获取线程的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from multiprocessing.pool import Pool


def task(n):
print('{}----->start'.format(n))
time.sleep(1)
print('{}------>end'.format(n))
return n ** 2


if __name__ == '__main__':
p = Pool(4)
res_list = []
for i in range(1, 11):
res = p.apply_async(task, args=(i,))
res_list.append(res) # 使用列表来保存进程执行结果
for re in res_list:
print(re.get())
p.close()
Copy

进程间不能共享全局变量

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 multiprocessing import Process
import os

nums = [11, 22]

def work1():
"""子进程要执行的代码"""
print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))
for i in range(3):
nums.append(i)
print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))

def work2():
"""子进程要执行的代码"""
nums.pop()
print("in process2 pid=%d ,nums=%s" % (os.getpid(), nums))

if __name__ == '__main__':
p1 = Process(target=work1)
p1.start()
p1.join()

p2 = Process(target=work2)
p2.start()

print('in process0 pid={} ,nums={}'.format(os.getpid(),nums))
Copy

运行结果:

1
2
3
4
5
6
in process1 pid=2707 ,nums=[11, 22]
in process1 pid=2707 ,nums=[11, 22, 0]
in process1 pid=2707 ,nums=[11, 22, 0, 1]
in process1 pid=2707 ,nums=[11, 22, 0, 1, 2]
in process0 pid=2706 ,nums=[11, 22]
in process2 pid=2708 ,nums=[11]

进程和线程的区别

功能

  • 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ。
  • 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口。

定义的不同

  • 进程是系统进行资源分配和调度的一个独立单位.
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

区别

  • 一个程序至少有一个进程,一个进程至少有一个线程.
  • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  • 线线程不能够独立执行,必须依存在进程中
  • 可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人

优缺点

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

进程间通信

进程间通信-Queue

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
from multiprocessing import Queue
q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息
q.put("消息1")
q.put("消息2")
print(q.full()) #False
q.put("消息3")
print(q.full()) #True

#因为消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,第二个Try会立刻抛出异常
try:
q.put("消息4",True,2)
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())

try:
q.put_nowait("消息4")
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())

#推荐的方式,先判断消息列队是否已满,再写入
if not q.full():
q.put_nowait("消息4")

#读取消息时,先判断消息列队是否为空,再读取
if not q.empty():
for i in range(q.qsize()):
print(q.get_nowait())
Copy

说明

初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);

  • Queue.qsize():返回当前队列包含的消息数量;
  • Queue.empty():如果队列为空,返回True,反之False ;
  • Queue.full():如果队列满了,返回True,反之False;
  • Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;

1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出”Queue.Empty”异常;

2)如果block值为False,消息列队如果为空,则会立刻抛出”Queue.Empty”异常;

  • Queue.get_nowait():相当Queue.get(False);
  • Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;

1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出”Queue.Full”异常;

2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出”Queue.Full”异常;

  • Queue.put_nowait(item):相当Queue.put(item, False);

使用Queue实现进程共享

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

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
from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
while True:
if not q.empty():
value = q.get(True)
print('Get %s from queue.' % value)
time.sleep(random.random())
else:
break

if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 等待pw结束:
pw.join()
# 启动子进程pr,读取:
pr.start()
pr.join()
print('所有数据都写入并且读完')

进程池

当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。

初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,请看下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from multiprocessing import Pool
import os, time, random

def worker(msg):
t_start = time.time()
print("%s开始执行,进程号为%d" % (msg,os.getpid()))
# random.random()随机生成0~1之间的浮点数
time.sleep(random.random()*2)
t_stop = time.time()
print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start))

po = Pool(3) # 定义一个进程池,最大进程数3
for i in range(0,10):
# Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
# 每次循环将会用空闲出来的子进程去调用目标
po.apply_async(worker,(i,))

print("----start----")
po.close() # 关闭进程池,关闭后po不再接收新的请求
po.join() # 等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
----start----
0开始执行,进程号为21466
1开始执行,进程号为21468
2开始执行,进程号为21467
0 执行完毕,耗时1.01
3开始执行,进程号为21466
2 执行完毕,耗时1.24
4开始执行,进程号为21467
3 执行完毕,耗时0.56
5开始执行,进程号为21466
1 执行完毕,耗时1.68
6开始执行,进程号为21468
4 执行完毕,耗时0.67
7开始执行,进程号为21467
5 执行完毕,耗时0.83
8开始执行,进程号为21466
6 执行完毕,耗时0.75
9开始执行,进程号为21468
7 执行完毕,耗时1.03
8 执行完毕,耗时1.05
9 执行完毕,耗时1.69
-----end-----

multiprocessing.Pool常用函数解析:

  • apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
  • close():关闭Pool,使其不再接受新的任务;
  • terminate():不管任务是否完成,立即终止;
  • join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;

进程池中的Queue

如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:

RuntimeError: Queue objects should only be shared between processes through inheritance.

下面的实例演示了进程池中的进程如何通信:

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中的Queue为Manager
from multiprocessing import Manager, Pool
import os, time, random


def reader(q):
print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print("reader从Queue获取到消息:%s" % q.get(True))


def writer(q):
print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
for i in "helloworld":
q.put(i)


if __name__ == "__main__":
print("(%s) start" % os.getpid())
q = Manager().Queue() # 使用Manager中的Queue
po = Pool()
po.apply_async(writer, (q,))

time.sleep(1) # 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据

po.apply_async(reader, (q,))
po.close()
po.join()
print("(%s) End" % os.getpid())

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(4171) start
writer启动(4173),父进程为(4171)
reader启动(4174),父进程为(4171)
reader从Queue获取到消息:h
reader从Queue获取到消息:e
reader从Queue获取到消息:l
reader从Queue获取到消息:l
reader从Queue获取到消息:o
reader从Queue获取到消息:w
reader从Queue获取到消息:o
reader从Queue获取到消息:r
reader从Queue获取到消息:l
reader从Queue获取到消息:d
(4171) End

HTTP服务器