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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "malloc.h"
#include "stdlib.h"

#define LIST_INIT_SIZE 80
#define LIST_INCREMENT 10
#define OK 1;

typedef int Status;

typedef struct{
char name[8];
int age;
}ElemType;

typedef struct {
ElemType *elem;
int length; //当前长度
int listSize; //当前分配的存储容量
}SqList;

Status InitList_Sq( SqList *L ){
L->elem = (ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
if(!L->elem) _exit(0);
L->length = 0;
L->listSize = LIST_INIT_SIZE;
return OK;
}

Status ListInsert_Sq(SqList *L,int i,ElemType e){
if(i<0 || i > L->length+1 )
_exit(0);

if(L->length+1>L->listSize){
L->elem = (ElemType*)realloc(L->elem,LIST_INCREMENT*sizeof(ElemType)+L->listSize*sizeof(ElemType));
L->listSize += LIST_INCREMENT;
if(!L->elem)
exit(0);
}
i -= 1;
int j;
for(j=L->length;j>i-1;j--)
L->elem[j] = L->elem[j-1];
L->elem[j] = e;
L->length++;

return OK;
}

Status ListDelete_Sq(SqList *L,int i,ElemType *e){
int j;
if(i<1 || i>L->length)
exit(0);

for(j=0;j<i-1;j++)
L->elem[j] = L->elem[j+1];

L->length--;
return OK;
}

顺序表的链式表示和实现

头结点:在单链表上设置一个结点,它本身不存放数据,它的指针域指向第一个元素的地址。

首元结点: 链表中第一个元素所在的节点,它是头节点后边的第一个节点。

头指针:链表的头指针永远指向链表中第一个节点的位置,换句话说,如果链表有头节点,头指针指向头节点;否则,头指针指向首元节点。

是实话,不一理解为啥一定要有头指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "stdio.h"
#include "malloc.h"
#include "stdlib.h"

#define OK 1
#define ERROR 0

typedef int Status;

//typedef struct{
// char name[8];
// int age;
//}ElemType;

typedef int ElemType;

typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, * LinkList;

Status InitList(LinkList& L) {
L = (LinkList)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = nullptr;
return OK;
}

Status InsertList(LinkList& L, int i, ElemType m) {
int j = 1;
LNode* pre = L;
if (i < 1) return ERROR;
while (j++ < i && pre->next != nullptr)
pre = pre->next;
LNode* newLNode = (LNode*) malloc(sizeof (LNode));
newLNode->data = m;
newLNode->next = pre->next;
pre->next = newLNode;
return OK;
}

Status FindInList(LinkList& L, int i, ElemType &m) {
int j = 1;
LNode* pre = L;
if (i < 1) return ERROR;
while (j++ < i && pre->next != nullptr)
pre = pre->next;
if (j - 1 != i || pre->next == nullptr)
return ERROR;

m = pre->next->data;
return OK;
}

int main() {
LinkList L = nullptr;
int i;

if (InitList(L) != OK) {
printf("hhhhhh");
return 0;
}

for (i = 1; i < 10; i++) {
if (InsertList(L, i,i) != OK) {
printf("hhhhh:%d", i);
return 0;
} else{
printf("insert Ok:%d\n",i);
}
}

ElemType m = 0;
for (i = 1; i < 10; i++) {
if (FindInList(L, i, m) == OK) {
printf("%d %d\n", i, m);
}
else{
printf("find wrong:%d\n",i);
}
}

printf("Ok");
return 1;
}

循环链表与双向链表

循环链表:单链表最后一个结点的指针域没有利用, 如果使其指向头指针(头结点),则首尾构成 一个循环,称作循环链表。

优点:从表中任一结点出发均可找到表中其它结点。

特点:在循环链表中多采用尾指针,指向表尾的指针称为是尾指针。采用头指针时,查找第一个结点容易,查找尾结 点不容易,如果采用尾指针,则查找第一个结点和最后 一个结点都容易。

双向链表: 一个链表的每一个结点含有两个指针域:一个指针指向 其前驱结点,另一个指针指向其后继结点,这样的链表称为 双向链表。

小练习

  • 设L为带头结点的单链表,编写算法从头到尾反向输出每个结点的值。

    1
    2
    3
    4
    5
    6
    7
    void VersePrint(LinkList L){
    if(L->next == NULL){}
    else{
    VersePrint(L->next);
    printf("%d\n",L->next->data);
    }
    }

一,C语言网题目1003

要将”China”译成密码,译码规律是:用原来字母后面的第4个字母代替原来的字母.

例如,字母”A”后面第4个字母是”E”.”E”代替”A”。因此,”China”应译为”Glmre”。

请编一程序,用赋初值的方法使cl、c2、c3、c4、c5五个变量的值分别为,’C’、’h’、’i’、’n’、’a’,经过运算,使c1、c2、c3、c4、c5分别变为’G’、’l’、’m’、’r’、’e’,并输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int main()
{
int i;
char c[6];
gets(c);
for(i=0;c[i]!='\0';i++){
if(c[i] < 'a' && c[i] > 'A' - 1)
c[i] = (c[i]+4-'A')%26 + 'A';
if(c[i] < 'z' + 1 && c[i] > 'Z')
c[i] = (c[i]+4-'a')%26 + 'a';
}
puts(c);
}
  • 知识点一:ASCII码

    二进制 十进制 十六进制 字符/缩写 解释
    00000000 0 00 NUL (NULL) 空字符
    00000001 1 01 SOH (Start Of Headling) 标题开始
    00000010 2 02 STX (Start Of Text) 正文开始
    00000011 3 03 ETX (End Of Text) 正文结束
    00000100 4 04 EOT (End Of Transmission) 传输结束
    00000101 5 05 ENQ (Enquiry) 请求
    00000110 6 06 ACK (Acknowledge) 回应/响应/收到通知
    00000111 7 07 BEL (Bell) 响铃
    00001000 8 08 BS (Backspace) 退格
    00001001 9 09 HT (Horizontal Tab) 水平制表符
    00001010 10 0A LF/NL(Line Feed/New Line) 换行键
    00001011 11 0B VT (Vertical Tab) 垂直制表符
    00001100 12 0C FF/NP (Form Feed/New Page) 换页键
    00001101 13 0D CR (Carriage Return) 回车键
    00001110 14 0E SO (Shift Out) 不用切换
    00001111 15 0F SI (Shift In) 启用切换
    00010000 16 10 DLE (Data Link Escape) 数据链路转义
    00010001 17 11 DC1/XON (Device Control 1/Transmission On) 设备控制1/传输开始
    00010010 18 12 DC2 (Device Control 2) 设备控制2
    00010011 19 13 DC3/XOFF (Device Control 3/Transmission Off) 设备控制3/传输中断
    00010100 20 14 DC4 (Device Control 4) 设备控制4
    00010101 21 15 NAK (Negative Acknowledge) 无响应/非正常响应/拒绝接收
    00010110 22 16 SYN (Synchronous Idle) 同步空闲
    00010111 23 17 ETB (End of Transmission Block) 传输块结束/块传输终止
    00011000 24 18 CAN (Cancel) 取消
    00011001 25 19 EM (End of Medium) 已到介质末端/介质存储已满/介质中断
    00011010 26 1A SUB (Substitute) 替补/替换
    00011011 27 1B ESC (Escape) 逃离/取消
    00011100 28 1C FS (File Separator) 文件分割符
    00011101 29 1D GS (Group Separator) 组分隔符/分组符
    00011110 30 1E RS (Record Separator) 记录分离符
    00011111 31 1F US (Unit Separator) 单元分隔符
    00100000 32 20 (Space) 空格
    00100001 33 21 !
    00100010 34 22
    00100011 35 23 #
    00100100 36 24 $
    00100101 37 25 %
    00100110 38 26 &
    00100111 39 27
    00101000 40 28 (
    00101001 41 29 )
    00101010 42 2A *
    00101011 43 2B +
    00101100 44 2C ,
    00101101 45 2D -
    00101110 46 2E .
    00101111 47 2F /
    00110000 48 30 0
    00110001 49 31 1
    00110010 50 32 2
    00110011 51 33 3
    00110100 52 34 4
    00110101 53 35 5
    00110110 54 36 6
    00110111 55 37 7
    00111000 56 38 8
    00111001 57 39 9
    00111010 58 3A :
    00111011 59 3B ;
    00111100 60 3C <
    00111101 61 3D =
    00111110 62 3E >
    00111111 63 3F ?
    01000000 64 40 @
    01000001 65 41 A
    01000010 66 42 B
    01000011 67 43 C
    01000100 68 44 D
    01000101 69 45 E
    01000110 70 46 F
    01000111 71 47 G
    01001000 72 48 H
    01001001 73 49 I
    01001010 74 4A J
    01001011 75 4B K
    01001100 76 4C L
    01001101 77 4D M
    01001110 78 4E N
    01001111 79 4F O
    01010000 80 50 P
    01010001 81 51 Q
    01010010 82 52 R
    01010011 83 53 S
    01010100 84 54 T
    01010101 85 55 U
    01010110 86 56 V
    01010111 87 57 W
    01011000 88 58 X
    01011001 89 59 Y
    01011010 90 5A Z
    01011011 91 5B [
    01011100 92 5C \
    01011101 93 5D ]
    01011110 94 5E ^
    01011111 95 5F _
    01100000 96 60 `
    01100001 97 61 a
    01100010 98 62 b
    01100011 99 63 c
    01100100 100 64 d
    01100101 101 65 e
    01100110 102 66 f
    01100111 103 67 g
    01101000 104 68 h
    01101001 105 69 i
    01101010 106 6A j
    01101011 107 6B k
    01101100 108 6C l
    01101101 109 6D m
    01101110 110 6E n
    01101111 111 6F o
    01110000 112 70 p
    01110001 113 71 q
    01110010 114 72 r
    01110011 115 73 s
    01110100 116 74 t
    01110101 117 75 u
    01110110 118 76 v
    01110111 119 77 w
    01111000 120 78 x
    01111001 121 79 y
    01111010 122 7A z
    01111011 123 7B {
    01111100 124 7C |
    01111101 125 7D }
    01111110 126 7E ~
    01111111 127 7F DEL (Delete) 删除
  • 知识点二:c中单引号和双引号的区别

    单引号中的字符是一个整数,对应ASCII表,而双引号不仅可以引单个字符,还可以引字符串。

  • python实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    s = input()

    for c in s:
    if ord(c) in range(ord('a'),ord('z')+1):
    c = chr((ord(c)+4-ord('a'))%26+ord('a'))
    print(c,end='')
    if ord(c) in range(ord('A'),ord('Z')+1):
    c = chr((ord(c)+4-ord('A'))%26+ord('A'))
    print(c,end='')

二,杭电2000

输入三个字符后,按各字符的ASCII码从小到大的顺序输出这三个字符。

要点

  • 基础微服务架构
  • 服务注册,服务发现
  • 配置中心
  • 负载均衡
  • 链路追踪
  • 熔断、限流机制
  • api网关
  • 自动化部署

python负责微服务的底层接口开发,go处于中间层负责底层接口调用、与网关交互、负载均衡

知识点

go相关

  • gin web框架
  • Viper配置
  • Zap日志
  • Validator表单验证
  • Json web token
  • 阿里云oss
  • grpc_middleware

python相关

  • Peewee orm
  • Loguru日志
  • Passlib加密
  • grpcio/protobuf
  • Dns服务查询
  • Mq消息队列
  • Redis操作

微服务相关

  • Rpc框架-grpc
  • Consul服务注册和发现
  • Nacos配置中心
  • Yapi接口管理
  • Jaeger链路追踪
  • Sentinel熔断,限流
  • Kong服务网关

分布式相关

  • 负载均衡
  • 分布式锁
  • 分布式事务
  • 幂等性机制

痛苦的,偏激着。

予我一颗狭隘且嫉妒的心,却不予我获得我所羡慕的事物的能力。

每天都很痛苦。

深陷于泥沼之中,却又无能为力。

注释

1
2
3
//一行
/* 多行 */
/** 文档 */

基本数据类型

Java与OOP有关的关键字

  • 限定访问权限修饰符:

  • 存储方式修饰符——static

    用途:方便在没有创建对象的情况下调用方法/变量。

    静态变量:静态变量被所有的对象所共享,在内存中只有一个副本。

    特殊用途:运用static代码块来优化程序性能,往往将只需要进行一次的初始化操作放在static代码块中运行

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
private Date birthDate;

public Person(Date birthDate) {
this.birthDate = birthDate;
}

boolean isBornBoomer() {
Date startDate = Date.valueOf("1946");
Date endDate = Date.valueOf("1964");
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}

每次调用isBornBoomer,都会生成startDate和endDate两个对象,存储空间浪费

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person{
private Date birthDate;
private static Date startDate,endDate;
static{
startDate = Date.valueOf("1946");
endDate = Date.valueOf("1964");
}

public Person(Date birthDate) {
this.birthDate = birthDate;
}

boolean isBornBoomer() {
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
  • final

    用final修饰的类断子绝孙

  • abstract

    抽象类,方法留给子类来实现

  • this,super

    代指子类和父类

Java-OOP的一些说明

  • java对类的内存分配分为两步:

    • 说明变量时,为其建立一个引用并置初值null
    • 用new申请内存空间
  • java把class类型的变量看作是引用,赋值语句含义由此发生变化

    1
    2
    3
    4
    5
    6
    7
    int x = 7;
    int y = x;
    y = 1; //此时x为7

    String s = "Hello";
    String t = s;
    t = "World" //此时s为"World"

Java输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.text.DecimalFormat;
import java.util.Scanner;

public class hello_world {
public static void main(String[] args){
String message1;
String message2;
double double1;

Scanner scan = new Scanner(System.in);
message1 = scan.next();
message2 = scan.nextLine();
double1 = scan.nextDouble();

System.out.println(message1+"\n"+message2+"\n"+double1);

DecimalFormat fmt = new DecimalFormat("0.###");
System.out.println(fmt.format(double1));
}
}

数组复制

1
System.arraycopy(src, srcPos, dest, destPos, length);

src表示源数组

srcPos表示源数组中拷贝元素的起始位置。

dest表示目标数组

destPos表示拷贝到目标数组的起始位置

length表示拷贝元素的个数

Vector类

四种构造方法:

1
2
3
4
Vector() //默认大小为10
Vector(int size)
Vector(int size,int incr) //指定大小和增量
Vector(Collection c) //包含集合c

方法:

1
2
3
4
5
6
7
8
9
10
void setElementAt(Object obj, int index) 
void removeElementAt(int index)
boolean removeElement(Object obj)
void removeAllElements()
Object elementAt(int index)
int indexOf(Object elem)
int indexOf(Object elem, int index)
int lastIndexOf(Object elem)
int lastIndexOf(Object elem, int index)
...

Ping

ping用于确定本机是否能与另一台主机交换数据包,根据返回的信息,推断TCP/IP参数设置是否正确,以及运行是否正常、网络是否通畅。ping发送接收的消息是报文(IP报文)。

ping的过程:

  • 源主机发送”请求回显“(ping请求)
  • 目标主机返回”回显应答“(ping请求)
  • 源主机通过”回显应答“类型报文中的内容来判断是否能够正常连接

IP报文:

IP协议并不可靠,不保证数据能否被成功送达,由此引入ICMP(网络控制报文)协议。

ICMP传递差错报文以及其它需要注意的信息,供IP层或更高层(TCP或UDP)使用。经常被认为是IP层的一部分。

1931443-20200924185032171-662267624

ICMP报文类型有多种,根据type和code来判断ICMP报文类型。

ICMP报文 = ICMP报头+Data((ICMP_Packet = ICMPHeader + Data))

1931443-20200924185437832-1468675305

  • type和code
TYPE CODE Description Query Error
0 0 Echo Reply——回显应答(Ping应答) x
3 0 Network Unreachable——网络不可达 x
3 1 Host Unreachable——主机不可达 x
3 2 Protocol Unreachable——协议不可达 x
3 3 Port Unreachable——端口不可达 x
3 4 Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特 x
3 5 Source routing failed——源站选路失败 x
3 6 Destination network unknown——目的网络未知 x
3 7 Destination host unknown——目的主机未知 x
3 8 Source host isolated (obsolete)——源主机被隔离(作废不用) x
3 9 Destination network administratively prohibited——目的网络被强制禁止 x
3 10 Destination host administratively prohibited——目的主机被强制禁止 x
3 11 Network unreachable for TOS——由于服务类型TOS,网络不可达 x
3 12 Host unreachable for TOS——由于服务类型TOS,主机不可达 x
3 13 Communication administratively prohibited by filtering——由于过滤,通信被强制禁止 x
3 14 Host precedence violation——主机越权 x
3 15 Precedence cutoff in effect——优先中止生效 x
4 0 Source quench——源端被关闭(基本流控制)
5 0 Redirect for network——对网络重定向
5 1 Redirect for host——对主机重定向
5 2 Redirect for TOS and network——对服务类型和网络重定向
5 3 Redirect for TOS and host——对服务类型和主机重定向
8 0 Echo request——回显请求(Ping请求) x
9 0 Router advertisement——路由器通告
10 0 Route solicitation——路由器请求
11 0 TTL equals 0 during transit——传输期间生存时间为0 x
11 1 TTL equals 0 during reassembly——在数据报组装期间生存时间为0 x
12 0 IP header bad (catchall error)——坏的IP首部(包括各种差错) x
12 1 Required options missing——缺少必需的选项 x
13 0 Timestamp request (obsolete)——时间戳请求(作废不用) x
14 Timestamp reply (obsolete)——时间戳应答(作废不用) x
15 0 Information request (obsolete)——信息请求(作废不用) x
16 0 Information reply (obsolete)——信息应答(作废不用) x
17 0 Address mask request——地址掩码请求 x
18 0 Address mask reply——地址掩码应答 x
  • checksum:用于检验数据包是否正确。
  • ID:即每次运行可以Ping的程序都会获得一个进程ID,我们通过这个比较发送进程ID和回显应答中的ID,如果一样则说明是同一个Ping程序
  • sequence:每发送一个信息就有一个sequence
  • data:存放其它信息,如timeout

traceroute

向目标主机发送消息并依据回答判断目标主机状态,同时遍历源主机和目标主机交互线路上的所有路由器并判断其状态,一般以30为最大TTL(time to live),每经过一台主机,TTL就减少一。每次遍历的前提:交互线路有不超过TTL-1数量的路由器。

  • 原理:

    源主机发送一份TTL字段为n的IP数据给某个主机,就可以得到该主机的IP地址,我们让TTL字段从1开始依次递增,就可以得到源主机与目标主机之间所有的路由器的IP地址。当一个路由器收到一个IP数据报,若其TTL为1,那么该数据报在网络空间中的生存周期已经结束,这时,若收到该数据报的主机不是目标主机,那么该主机就会返回一份ICMP超时报文(包含该主机IP地址);若在TTL>=1的情况下到达目标主机,目标主机就会返回一份ICMP应答报文。

  • Timeout:

    设置该参数的意义在于应对发出消息后消息丢失一直不会返回的情况,避免因为消息没有返回而一直等待下去。

  • type和code

    根据ICMP报文的type和code判断该主机状态。

  • tries:

    消息发出后可能会意外不会返回,设定tries规定尝试的次数

  • DNS

    发包速度以ms为单位,很快;但通过DNS查询返回消息的主机信息以s为单位,不快。

chekcsum

着重讲述一下checksum(检验和):

checksum是一个从数据包中通过特定计算方式而得出的值,通过检验这个值是否正确,来判断数据包完整性。在网络上传输时,数据包存在损坏的风险,接收端用checksum检验数据包是否损坏。源主机计算cheksum并将其作为字段设置在报文中,目标主机收到数据包再次计算checksum,并与数据包中已有的checksum进行交叉检验。

计算cheksum的步骤:

  • 将cheksum设置为0
  • 每两个字节,后一个设为高位,前一个设为低位,相加。若和溢出(超过16位),取超出位与前16位相加。
  • 字节数若为奇,那么最后一定剩一个字节,将该字节与前面计算所得相加,若和溢出(超过16位),取超出位与前16位相加。。若为偶,不会剩字节。
  • 若还是溢出,继续取超出位与前16位相加。
  • 取反,得到一个十进制数
  • 主机字节序转为网络字节序(小端序转大端序)

小端序转大端序:

  • 大端序(Big Endian):高位字节存放到低位地址(高位字节在前)。
  • 小端序(Little Endian):高位字节存放到高位地址(低位字节在前)。

例子(0x12345678):

大端序:

大端序

小端序:

网路字节序统一为大端序。

网络通信

  • 使用网络是为了联通多方然后进行通信,把数据从一方传递给另一方

  • 网络编程,让不同的主机进行数据传递,进程之间互相通信

  • ip地址:用来在网络中标记一台主机

  • ipv4:ip version 4 格式:xxx.xxx.xxx.xx 0-255

  • ipv6:ip version 6

ipv4地址分类:

每一个ip地址分为两部分:网络地址和主机地址

1440532-20180912093201807-306001370

D类用来多播

播:单播(一对一)多播(一对多,只让部分人听到)广播(一对多,所有人可听到)

端口:

端口就像一座房子的门,是进出房子的必经之路

端口用于标记一台主机的不同进程

分为两类:

  • 知名端口:每一台主机都默认使用的端口,端口号:0-1023,80分配给HTTP服务,21分配给FTP服务(文件传输)

  • 动态端口:范围:1024-65535

进程:应用程序启动实例,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。

线程:程序的实际执行者,一个进程至少包含一个主线程,也可以拥有更多子线程,线程拥有自己独立的栈空间

线程是最小的执行单元,进程是最小的资源管理单元。

协程:比线程更加轻量级,一个线程可拥有多个协程

socket

首要解决的问题是如何唯一表示一个进程

在一台主机上可用端口来唯一标识一个进程,而在网络中TCP/IP协议簇已经解决了这个问题:网络层中ip地址唯一标识一台主机,而传输层协议+端口唯一标识一台主机的一个进程。由此,网络中进程标识:ip地址+协议+端口

格式:

1
2
# family:指定协议簇(ipv4/ipv6)
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
  • socket类型
socket类型 描述
socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
socket.AF_INET 服务器之间网络通信,socket网络层使用IPv4协议
socket.AF_INET6 IPv6
socket.SOCK_STREAM 流式socket , for TCP,传输层使用TCP协议
socket.SOCK_DGRAM 数据报式socket , for UDP,传输层使用UDP协议
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_SEQPACKET 可靠的连续数据包服务
创建TCP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
创建UDP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
  • socket函数
socket函数 描述
服务端socket函数
s.bind(address) 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址.
s.listen(backlog) 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
客户端socket函数
s.connect(address) 连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex(adddress) 功能与connect(address)相同,但是成功返回0,失败返回errno的值。
公共socket函数
s.recv(bufsize[,flag]) 接受TCP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send(string[,flag]) 发送TCP数据。将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall(string[,flag]) 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom(bufsize[.flag]) 接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto(string[,flag],address) 发送UDP数据。将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字。
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile() 创建一个与该套接字相关连的文件

着重解释一下setsockopt():

当默认socket选项不够时,使用setsockopt()

  • level:选项定义的层次(套接字描述符)。支持SOL_SOCKET(让setsockopt能够设置套接字层的选项,这些选项独立于协议之外)、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
  • optname:指定准备设置的选项,可以设置哪些选项,取决于level参数。

TCP

tcp协议:进行通讯的两方,分为服务端和客户端,需要先建立一个虚拟连接,然后双方程序才能发送业务数据信息

通过三次握手进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# description:TCP service
# author:GuiYoung
# time:2021.9.11

import socket

# 主机地址为0.0.0.0,表示绑定本机所有IP地址
IP = '0.0.0.0'
# 端口号
PORT = 5000
# 定义一次从socket缓存区最多读入512字节数据
BUFLEN = 512

# 实例化一个socket对象
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址端口,告知服务端IP地址和端口号
listen_socket.bind((IP,PORT))

# 让socket处于监听状态,等待客户端连接请求
# 参数5表示最多能有五个客户端连接
# 此时已开始连接
listen_socket.listen(5)

# 返回一个元组,包含两个对象
# 第一个是一个新的socket,用于通讯:接收和发送信息
# 第二个包括客户端IP地址和端口号
data_socket, addr = listen_socket.accept()

# 用一个循环来通信
while True:
# 尝试读取对方发送的消息
# BUFLEN指定多少个字节
# recved是字节串,若对方关闭连接,返回一个空字节
recved = data_socket.recv(BUFLEN)

if not recved:
break

info = recved.decode()
print(f'client信息:{info}')

# 发送的类型必须是bytes,需要编码
data_socket.send(f'收到信息:{info}'.encode())

data_socket.close()
listen_socket.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# description:TCP client
# author:GuiYoung
# time:2021.9.11
import socket

IP = '127.0.0.1'
# 服务端客户端端口需要一致
SERVER_PORT = 5000
BUFLEN = 512

data_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
data_socket.connect((IP,SERVER_PORT))

while True:
# 读入用户输入的字符串
to_send = input('>>> ')
if to_send == 'exit':
break
# 需要编码
data_socket.send(to_send.encode())

# 等待接收服务端消息
recved = data_socket.recv(BUFLEN)
print(f'info:{recved.decode()}')

data_socket.send(f'收到信息:{recved.decode()}'.encode())

data_socket.close()

UDP

UDP:UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# description:UDP service
# author:GuiYoung
# time:2021.9.11
import socket

IP = '127.0.0.1'
PORT = 8888
BUFLEN = 512

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto('hello, I am client'.encode(),(IP,PORT))
data, _ = s.recvfrom(512)
print(f'receive info from service:{data.decode()}')
s.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# description:UDP service
# author:GuiYoung
# time:2021.9.11
import socket

IP = '0.0.0.0'
PORT = 8888
BUFLEN = 512
print("服务器启动")

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((IP,PORT))

data, client_addr = s.recvfrom(512)
print(f'receive info!{data.decode()}')

s.sendto('hello'.encode(),client_addr)

s.close()

创建连接

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

#创建连接
r = redis.Redis(
host="localhost",
port=6379,
db=0 #逻辑库号数,只能和一个数据库绑定在一起,无法中途改变
)

#创建连接池
try:
pool = redis.ConnectionPool(
host="localhost",
port=6379,
db=0,
max_connections=20
)
except Exception as e:
logging.error(e)
else:
#从连接池中获取连接,不必关闭,垃圾回收时,连接会自动被归还到连接池
r = redis.Redis(
connection_pool=pool
)
del r#此时连接已被归还到连接池

字符串操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r.set("country","Singapore")
r.set("city","Singapore")
r.expire("city",3)
time.sleep(3)
try :
city = r.get("city").decode("utf-8")#若key不存在,则会报错
except AttributeError:
pass
else:
print(city)
r.delete("country","city") #当key已不存在时,不会报错
dict_ = {"country":"德国","city":"柏林"}
r.mset(dict_)
result = r.mget("country","city")#返回元组类型
for one in result:
print(one.decode("utf-8"))

列表操作

1
2
3
4
5
6
7
8
try:    
r.rpush("dname","d1","d2","d3")
r.lpop("dname")
result=r.lrange("dname",0,-1)#返回元组类型
for one in result:
print(one.decode("urf-8"))
except Exception as e:
pass

集合操作

1
2
3
4
5
6
7
8
9
r.sadd("employee",8001,8002,8003)
r.srem("employee",8001)
result = r.members("employ")#元组

#有序集合
dict_ = {"QAQ":0,"QWQ":0,"QVQ":0}#前面为词条,后面为分数值
r.zadd("keyword",dict_)
r.zincrby("keyword","10","QAQ")#为QAQ词条加十分
result = r.zrevrange("keyword",0,-1)#元组类型

哈希表

1
2
3
4
5
r.hmset("she",{"name":"Lily","age":19}) #redis4.0开始弃用hmset,推荐使用hset
r.hset("she","dream","flower and star")
r.hdel("she","dream")
print(r.hexists("she","name"))#返回True
result = r.hgetall("she")#元组

事务函数

1
2
3
4
5
6
pipline = r.pipline() #创建pipline对象
pipline.watch("she") #监视记录
pipline.multi() #开启事务
#各种操作用pipline完成而不是r
pipline.execute() #递交事务
pipline.reset() #关闭pipline,格外注意

线程池

为什么要创建线程池?

  • 若在程序中经常需要用到线程,线程的频繁创建和销毁会浪费很多硬件资源,所以需要分离线程和任务,线程可以反复利用,省去重复创建的麻烦。

    我们可以把每个任务写成函数,把其交给线程池,而线程池会选择一个空闲的线程去执行这个任务,这和数据库的连接池原理十分相似。

用模拟商品秒杀活动:利用pythonmo’ni商品秒杀的过程,Redis采用单线程机制,不会存在超买和超卖的情况。

1
2
3
4
5
6
7
8
9
10
11
12
from concurrent.futures import ThreadPoolExecutor

#定义线程任务
def say_hello():
print("hello")

if __name__ == "__main__":
#将线程池保存到变量中
executor = ThreadPoolExecutor(20)
for i in range(0,10):
#submit用于向线程池提交任务
executor.submit(say_hello)

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import logging
import random
from concurrent.futures import ThreadPoolExecutor
import redis
from models import redis_db

def buy(user_id):
try:
con_for_buy = redis.Redis(
connection_pool=redis_db.pool
)
pipline = con_for_buy.pipeline()
if con_for_buy.exists("kill_flag"):
total_num = int(con_for_buy.get("kill_total").decode("utf-8"))
num = int(con_for_buy.get("kill_num").decode("utf-8"))
pipline.watch("kill_num", "kill_user") # watch()里放即将要修改的key
if num < total_num:
pipline.incr("kill_num")
pipline.lpush("kill_user", user_id)
pipline.execute()
pipline.reset()
except Exception as e:
logging.error(e)
finally:
del con_for_buy

def is_param_exist():
try:
con = redis.Redis(
connection_pool=redis_db.pool
)
if con.exists("kill_total", "kill_flag", "kill_num"):
return True
else:
return False
except Exception as e:
logging.error(e)
return False
finally:
del con

def create_param():
try:
con = redis.Redis(
connection_pool=redis_db.pool
)
con.set("kill_total", 20)
con.set("kill_num", 0)
con.set("kill_flag", 1)
con.expire("kill_flag",600)
return True
except Exception as e:
logging.error(e)
return False
finally:
del con

def delete_param():
try:
con = redis.Redis(
connection_pool=redis_db.pool
)
con.delete("kill_total","kill_num","kill_flag","kill_user")
return True
except Exception as e:
logging.error(e)
return False
finally:
del con

if __name__ == "__main__":
s = set()
while True:
if len(s) == 1000:
break
s.add(random.randint(10000,100000))


if delete_param() is not True:
exit()
if create_param() is not True:
exit()
if is_param_exist() is not True:
exit()

try:
executor_ = ThreadPoolExecutor(50)
for i in range(0,1000):
user_id = s.pop()
executor_.submit(buy,user_id)
con = redis.Redis(
connection_pool=redis_db.pool
)
except Exception as e:
logging.error(e)
finally:
del executor_,con

Redis介绍

存储数据的方式:内存,提高数据读写速度。

在一个业务系统中,数据使用频率不同,使用频率高的被称为热数据。

结构:应用程序-缓存-数据库-HDD。

案例一:以微博为例:微博大V的数据被放到高速缓存(Redis)中,而普通人的放入NoSql中(如mongodb)。

案例二:门户网站、视频网站首页放入高速缓存中。

案列三:双十一时,电商平台要利用高速的缓存来弥补数据库吞吐能力的不足。订单先放入高速缓存集群,然后在负载低谷期时,再延时写入数据库。

Redis:Vmware开源的NoSQL数据库产品,基于Key-Value存储格式,可将数据保存在内存或硬盘中。

单线程模型的NoSQL数据库,C语言编写,QPS(每秒可查询次数)可达到100000+。

Redis提供了两种持久化的保存方案:

  • RDB:满足触发条件会将数据保存到硬盘中
  • AOF:用日志的方式来记录数据写入,倘若服务器宕机,则会在重启之后读取日志来恢复数据

类型:key一定为字符串类型,value有字符串、哈希、列表、集合和有序集合五种类型

在redis上数据的并发修改是顺序执行的

Redis参数

  • port:端口号,默认为6379

  • bind:允许的ip,默认仅允许本机访问

  • time:client空闲多少秒后关闭连接,默认0代表无限制

  • loglevel:日志级别

  • logfile:日志文件地址

  • syslog-enabled:是否将日志输出到控制台,默认为yes

  • databases:逻辑库数量,默认16

  • save:RDB文件同步的频率

  • rdbcompression:同步RDB文件是否采用压缩,默认yes

  • dbfilename:定义RDB文件名称,默认为dump.rdb

  • dir:存放RDB文件的地址

  • requirepass:访问密码,默认无密码

  • maxclients 最大连接数,默认无限制

  • maxmemory:redis占用内存的最大值

  • appendonly:开启AOF备份

  • appendfsync:AOF同步的频率,分为no|everysec|always

Redis字符串类型

String类型可保存普通文字,也可保存序列化的二进制数据(如图片)

String类型最大可存储512M数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
redis > SET email hhh@.com
redis > GET email
reids > DEL email
redis > GETRANGE email 0 3 #获取前四个字符
redis > STRLEN emai
redis > SETEX city 5 New_York #设置带有过期时间(秒)的key-value
redis > MSET city New_York name Lily #设置多个key-value
redis > MGET city name
redis > APPEND name _Queen_Country #在字符串末尾附上
redis > INCR num #数字自增1
redis > INCRBY num 25 #数字加25,可以加负数
redis > INCRBYFLOAT num 3.5 #加浮点数 可以加负数
reids > DECR num
reids > DECRBY num 10

Redis哈希类型

当我们觉得value需要保存更复杂的结构化数据时使用

与python字典类似

1
2
3
4
5
6
7
8
9
10
11
12
13
HSET 8000 her_name Lily # 一次可以向哈希表中添加一个key-value
HMSET 8000 her_name Lily age 19 hometown New_York
flushdb # 情空逻辑库中数据
HGET 8000 her_name #获取某一个字段的值
HMGET 8000 her_name hometown #获取多个字段的值
HGETALL 8000 # 获取哈希表所有key-value
HKEYS 8000 # 获取所有字段的名称
HELN 8000 # 获取字段数量
HEXISTS 8000 her_name # 验证是否存在某字段
HVALS 8000 # 获取哈希表中所有字段值
HDEL 8000 her_name #删除哈希表的字段
HINCRYBY 8000 age 1 #让哈希表某个字段加上指定值
HINCRYBYFLOAT 8000 num -0.5

Redis列表类型

当需要向VALUE保存序列化数据时使用

与python列表类似

列表允许保存重复元素

1
2
3
4
5
6
7
8
9
10
11
RPUSH blue QAQ QWQ QVQ # 右加数据
LPUSH blue ToT # 左加数据
LSET blue 2 hhh # L代表list,代表修改第三个元素
LRANGE blue 0 -1 # 输出指定范围数据,开始为0,结束为-1
LLEN blue # 获取列表长度
LINDEX blue 0 #获取列表某个元素
LINSERT blue BEFORE QWQ 233 #将233元素插到QWQ之前
LINSERT blue AFTER QWQ 666 #将666元素插到QWQ之后
LPOP blue # 删除最左侧的元素
RPOP blue # 删除最右侧的元素
LREM blue 1 QAQ # 删除1个QAQ元素

Redis集合类型

与列表类似

不允许保存重复元素

最先插入的元素可能在最后,最后的可能在最前,redis对集合中的元素按哈希值排序,哈希值小的在前,哈希值大的在后

1
2
3
4
5
6
7
SADD empno 8000 8001 # S代表set(集合),向empno中添加8000,8001这两个元素
SMEMBERS empno # 输出集合所有元素
SCARD empno # 输出元素总数
SISMEMBER empno 8000 #判断是否有8000这个元素
SREM empno 8000 8001 #删除8000,8001这两个元素
SPOP empno # 随机删除并返回集合的某个元素
SRANDMEMBER empno 5 # 随机挑选5个元素返回

Redis有序集合类型

Redis按照元素分数值排序,有编号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ZADD blue 0 QAQ 0 QWQ 0 QVQ # 当都为0时,按哈希值排序
ZINCRBY blue 1 QAQ # 让QAQ这个词条分数值加一
ZCARD blue #获取有序集合的长度
ZCOUNT blue 2 4 # 查询分数区间(2,4)内的元素数量(包含2,4)
ZSCORE blue QAQ # 返回元素分数值
ZREVRANGE blue 0 -1 # Z reverse range 降序排序获取某个区间内元素 0:第一个 -1:最后一个
ZRANGE blue 0 -1 # 升序排序获取某个区间内元素
ZRANGEBYSCORE blue 5 10 #获取分数区间5到10内的集合内容
ZRANGEBYSCORE blue (5 10 #不包括5
ZRANGEBYSCORE blue 10000 +inf # 获取10000到正无穷内的
ZREVRANGEBYSCORE blue 10 5 #降序.注意最大值写在前,最小值写在后
ZRANK blue QAQ # 获取元素分数排名,升序,从0开始
ZREVRANK blue QAQ # 获取元素分数排序,降序,从0开始
ZREM blue QAQ QWQ # 删除QAQ,QWQ这两个元素
ZREMRANGEBYRANK blue 0 -1 # 删除所有元素(按排名区间)
ZREMRANGEBYSCORE blue -inf (5000 # 删除负无穷到5000的元素(按分数区间)

REDIS key操作

1
2
3
4
5
6
7
8
DEL keyword
EXISTS keyword #判断是否存在某个key,存在返回1,否则返回0
EXPIRE employee 5 # 为key设置过期时间(几秒后)
EXPIREAT employee 155555555555555 # 设置过期时间(那一天哪一分哪一秒),格式需要为UNIX时间戳
MOVE keyword 1 #记录迁移到其它数据库
RENAME hhh 233 #将key hhh 改名为 233
PERSIST keyword # 移除过期时间
TYPE keywrod # 判断value数据类型

REDIS事务

Redis的异步单线程机制决定了一个线程对应所有的客户端,不能保证当一个客户端发布多个命令时,不会被其它客户端插队

引入事物机制,来应对这个问题,Redis的事务处理机制更像是一个批处理机制,客户端一次把所有命令传递给Redis,然后Redis一次处理所有命令,等一个客户端发布的所有命令执行完之后,另一个客户端才能发布命令。

Redis 一致性,隔离性
MySQL 原子性,持久性,一致性,隔离性
  • 为了保证事务一致性,在开启事务之前必须使用WATCH命令监视要操作的记录。开启事务监视之后,如果其它客户端想要修改监视的数据,那么事务就会自动关闭。

    1
    WATCH key1 key2
  • MULTI #开启一个事务
    ... #一些操作
    EXEC #开启事务后所有操作都不会立即执行,直到EXEC命令发布后才会一并执行。
    
    1
    2
    3

    例子:

    SET num 0 WATCH num MULTI INCRBY num 10 EXEC # 在执行该命令之前,若其它客户端想要修改num,那么事务就会结束
    1

    DISCARD #用于在事务提交/监视数据发生变化之前,可用该命令取消事务

[TOC]

如何在创建大量实列时节省内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#desp:如何在创建大量实例时节省内存
#author:GuiYoung
#data:9/9/2021

class Date:
__slots__ = ['year','month','day']
def __init__(self,year=0,month=0,day=0):
self.year = year
self.month = month
self.day = day

if __name__ == '__main__':
date = Date(2021,9,9)
print(date.day)

这样做的好处:这样使用的Data类内存量相当于将数据保存在元组中,而创建一个Data字典则保存在字典中

副作用:没办法再对实例添加任何新的属性,只允许使用__slots__中列出的属性名;此外,定义了__slots__属性的类不支持某些特定功能,如多重继承

使用范围:仅在程序中呗当做数据结构而频繁使用的类中采用该技法

类中的下划线之辩

  • 无下划线:一般变量
  • 前单下划线:这样命名变量表示不希望变量在类外访问,实际上,子类和对象都可以访问
  • 前置双下划线:子类也不可访问的变量
  • 后置下划线:用于命名变量时与关键字区别

isinstance函数

用于判断一个对象是否是一种已知的类型

  • isinstance():考虑继承关系,会认为子类是一种父类类型
  • type():不考虑继承关系

语法:

1
isinstance(object, classinfo)

创建可管理的属性

在对实例的设置上,增加一些额外的处理过程(如类型检查或验证)

一种方式:将其定义为property,把类中定义的函数当做一种属性来使用

示例:类型检查或验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#desp:创建可管理的属性
#author:GuiYoung
#data:9/9/2021

class Person:
def __init__(self,first_name,last_name="xixi"):
self.first_name = first_name
self.last_name = last_name

# Getter function
@property
def first_name(self):
return self._first_name

# Repeated property code, but for a different name (bad!)
@property
def last_name(self):
return self._last_name

# Setter function
@first_name.setter
def first_name(self, value):
if not isinstance(value,str):
raise TypeError("Expected a string")
self._first_name = value

@last_name.setter
def last_name(self, value):
if not isinstance(value,str):
raise TypeError("Expected a string")
self._last_name = value

# Deleter function (optional)
@first_name.deleter
def first_name(self):
raise AttributeError("Can't delete attribute")

if __name__ == '__main__':
person_1 = Person("Lily")
print(person_1.first_name) # Calls the getter
try:
person_1.last_name = 1
except Exception as error:
print(error)

try:
del person_1.first_name
except Exception as error:
print(error)

不要重复property语句,这会导致代码膨胀,丑陋。

创建一种新形式的类属性或实例属性

作为上一节不建议重复property语句的解决方案

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
#desp:创建可管理的属性
#author:GuiYoung
#data:9/9/2021

#设置一个描述符类来帮助实现类型检查的功能
class Integer:
def __init__(self,name):
self.name = name

def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
instance.__dict__[self.name] = value

def __delete__(self, instance):
del instance.__dict__[self.name]

class Point:
#注意,描述符只能在类的层次上定义
x = Integer('x')
y = Integer('y')
def __init__(self,x,y):
self.x = x
self.y = y

if __name__ == "__main__":
p = Point(2, 3)
try:
p.x = 2.3
except TypeError as error:
print(error)

总结:若只是像访问某个特定的类中的一种属性,那么property会更简单;描述符适用于大量重用代码的情况,适合将其作为库使用。

调用父类中的方法

super()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#desp:创建可管理的属性
#author:GuiYoung
#data:9/9/2021

class A:
def __init__(self):
self.x = 0

def spam(self):
print("A.spam")

class B(A):
def __init__(self):
super().__init__()
self.y = 0

def spam(self):
print("B.spam")
super().spam()