Skip to content

Linux

题目

在 Linux 系统中,经常需要查看正在运行的进程,下列哪条命令不能获取所有正在运行的进程信息?

A. ps aux

正确ps aux 是一个常用命令,用于显示所有用户的所有进程,包括后台进程。它是 Linux 中查看进程信息的经典命令。

ps 是 "process status" 的缩写,a: 显示所有用户的进程,u: 以用户为中心的格式显示进程信息,x: 显示没有控制终端的进程。

执行 ps aux 后,通常会看到以下字段:

bash
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

B. netstat -ant错误netstat -ant 用于显示网络连接的相关信息,尤其是查看所有网络连接的状态(如监听、已建立的连接等),并不会列出所有进程的信息。

C. ps -ef正确ps -ef 是另一个查看进程信息的常用命令,-e 选项显示系统中所有的进程,-f 选项提供详细的输出格式,包括进程的父进程、启动时间等。

D. top正确top 是一个动态显示正在运行进程信息的命令,可以实时查看 CPU、内存的使用情况,并动态更新进程列表。

Shell 脚本,是一种为 shell 编写的脚本程序。现有一个 test.sh 文件,且有可执行权限,文件中内容为:

#!/bin/bash
aa='Hello World !'

请问下面选项中哪个能正常显示 Hello World !

A. sh test.sh >/dev/null 1 && echo $aa

错误。该命令首先使用 sh 执行脚本,输出重定向到 /dev/null&& 表示如果脚本执行成功,则执行 echo $aa。但是,$aa 变量在子 shell 中定义,在父 shell 中不可见,因此不会显示 "Hello World !”。

B. ./test.sh >/dev/null 1 && echo $aa

错误。该命令使用当前目录的 test.sh 执行脚本,输出同样重定向到 /dev/null&& 同样确保只有在脚本成功执行后才执行 echo $aa。由于脚本是在子 shell 中执行的,所以在父 shell 中 $aa 变量依然不可见,因此不会输出 "Hello World !”。

C. bash test.sh >/dev/null 1 && echo $aa

错误。这个命令使用 bash 执行脚本,并将输出重定向到 /dev/null。虽然使用 bash 保证了正确的 shell,但 $aa 仍然是在子 shell 中定义,所以在父 shell 中不可见,不会输出 "Hello World !”。

D. . ./test.sh >/dev/null 1 && echo $aa

正确。使用点(.)或等价的 source 命令执行脚本可以使脚本中定义的变量在当前 shell 环境中可用。这里,. ./test.sh 表示在当前 shell 中运行脚本,不创建新的子 shell。因此,脚本中定义的 $aa 变量在脚本执行完毕后仍然可见,并且 echo $aa 将会正确输出 "Hello World !”。

执行ls -la /usr/bin/passwd后,返回如下内容,下列描述错误的是:

bash
-rwsr-xr-x 1 root root 59640 Sep 27 2017 /usr/bin/passwd

选项:

A. 程序可以被任何账户执行

正确。文件权限 rwsr-xr-x 表明所有用户(用户组和其他用户)都有执行 (x) 权限。

B. 文件属性中的s表示这个文件是系统文件

错误。在文件权限中的 s(在这个上下文中是 rws 中的 s)表示该可执行文件具有 setuid(设置用户ID)属性。这意味着不论哪个用户执行该程序,程序都将具有文件所有者(在这个案例中是 root)的权限。s 并不表示文件是系统文件。

C. 这不是一个目录

正确。根据输出的第一个字符是 -(而不是 d),我们可以确认 /usr/bin/passwd 是一个文件,不是目录。

D. 文件/目录属于root用户

正确。输出中的 root root 部分表明文件的所有者和所属组都是 root。

文件权限输出的顺序依次为 当前用户 当前用户组 其他用户

4-r-读 2-w-写 1-x-执行 s为文件所有者同等权限

7 = 1+2+4

chmod 777 为分别对各用户授权 读写执行

Linux 文件系统的文件都按其作用分门别类地放在相关的目录中,对于外部设备文件,一般应将其放在哪个目录中()

A /bin

B /etc

C /dev

D /lib

在Linux系统中,挂载的设备通常可以在 /mnt/media 目录下找到。这两个目录的使用略有区别,具体如下:

/mnt

  • /mnt 目录通常用于临时挂载存储设备。系统管理员可能会在这里手动挂载文件系统。
  • 例如,如果你有一个外部硬盘或网络文件系统,你可能会将其挂载在如 /mnt/mydisk/mnt/nfs 这样的子目录下。

/media

  • /media 目录通常由现代Linux发行版用于自动挂载可移动存储设备,如USB闪存驱动器、CD-ROMs等。
  • 当你插入一个USB驱动器或CD-ROM时,系统会自动创建一个与设备名称相关的子目录,如 /media/usb/media/[username]/[device_label]

此外,还有一个全局的挂载点目录 /dev,它包含了所有的设备文件。在Linux中,硬件设备被视为文件,可以在 /dev 目录中找到相应的设备文件,比如硬盘通常表示为 /dev/sda/dev/sdb 等。

用 vi 打开一个文件,如何用字母 “new” 来代替字母 “old”

:1,$s/old/new/g

1,$ 表示从第一行到文件尾行; s 表示替换; /g 表示在全局文件中进行替换,省略时仅对每行第一个匹配串进行替换。

知识

DNS服务的主要配置文件,通常位于/etc/named.conf

创建一个管理员用户 admin useradd -u 0 -o admin -u 用户号 指定用户的用户号;因为系统用户的用户号为 0,故指定用户号为 0,如果同时有 -o 选项,则可以重复使用其他用户的标识号,因为系统本身存在用户号为 0 的系统用户,故应该使用该参数。

/proc

是一个 虚拟文件系统,也称为 proc 文件系统。它是内核提供的一个接口,用于访问内核和系统运行时的信息。

1. 进程相关

每个正在运行的进程都有一个以进程 ID (PID) 命名的目录,例如 /proc/1234 表示 PID 为 1234 的进程。

  • /proc/<PID>/cmdline:进程的启动命令行参数。
  • /proc/<PID>/cwd:进程的当前工作目录。
  • /proc/<PID>/exe:指向进程正在执行的二进制文件。
  • /proc/<PID>/status:进程的状态信息(如 UID、GID、内存使用)。
  • /proc/<PID>/fd/:文件描述符列表。

2. 系统信息

  • /proc/cpuinfo:CPU 相关信息(如核心数、架构、频率)。
  • /proc/meminfo:系统内存的使用情况。
  • /proc/uptime:系统启动以来的运行时间。
  • /proc/loadavg:系统的平均负载。
  • /proc/devices:系统中的设备列表。

3. 文件系统信息

  • /proc/mounts:已挂载文件系统的信息。
  • /proc/swaps:当前启用的交换空间信息。

4. 内核参数

  • /proc/sys/

    :存储内核参数,可用于读取和动态修改。

    例如:

    • /proc/sys/net/ipv4/ip_forward:是否启用 IP 转发。
    • /proc/sys/kernel/hostname:系统主机名。

5. 硬件相关

  • /proc/partitions:已识别的磁盘分区。
  • /proc/scsi/:SCSI 设备信息。

命令

ps -aux:用于查看当前系统中所有运行的进程,包括正在运行的恶意程序或异常进程。

last:用于查看最近登录系统的用户信息,能够帮助检查可疑登录活动。

w:显示当前登录系统的用户信息及其执行的命令,可以用来了解当前系统的用户活动。

more .bash_history:用于查看用户的历史命令记录,.bash_history 中保存了用户执行的命令,这对排查异常操作非常有帮助。

在 UNIX 和 Linux 系统中,有三个主要的文件描述符:

0 — 标准输入 (stdin) 1 — 标准输出 (stdout) 2 — 标准错误输出 (stderr)

&

后台执行

在命令的末尾添加 & 会使该命令在后台运行。这意味着你可以继续在同一个终端会话中执行其他命令,而不需要等待当前命令完成。

例如:

python script.py &

这会使 python script.py 在后台运行,终端会立即返回提示符,允许用户继续执行其他命令。

命令列表分隔符

在一行中用 & 分隔多个命令可以连续执行这些命令,无需等待前一个命令完成。这与使用分号 ; 不同,后者会等待每个命令完成后再执行下一个命令。

例如:

command1 & command2 &

这行命令会同时启动 command1command2,都在后台运行。

文件描述符的重定向

在重定向操作中,& 用于指示后面的数字是一个文件描述符,而不是一个文件。这在将一个流重定向到另一个流时特别有用。

例如:

  • 2>&1 —— 将标准错误(文件描述符 2)重定向到标准输出(文件描述符 1)。
  • 1>&2 —— 将标准输出(文件描述符 1)重定向到标准错误(文件描述符 2)。

逻辑 AND 运算符

在逻辑控制中,&& 用作 AND 运算符,仅当左边的命令成功执行(即返回值为 0,表示成功)时,才执行右边的命令。

例如:

command1 && command2

这里 command2 只有在 command1 成功执行后才会被执行。

inode

主要内容包括:

  • 文件类型:这表明文件是普通文件、目录、符号链接还是其他类型的特殊文件。
  • 权限:这包括文件的读、写和执行权限。
  • 链接数:文件的硬链接数量。当链接数降至零时,文件会被删除。
  • 文件大小:文件的字节数。
  • 所有者:文件的所有者的用户ID(UID)。
  • :文件的组ID(GID)。
  • 时间戳
    • 访问时间(atime):文件最后一次被访问的时间。
    • 修改时间(mtime):文件内容最后一次被修改的时间。
    • 改变时间(ctime):文件的 inode 信息最后一次被修改的时间。
  • 指向数据块的指针:这些指针指向存储文件数据的磁盘块。

在 Unix-like 系统中,你可以使用 ls -i 命令来查看文件的 inode 号,或使用 stat 命令来查看文件的完整 inode 信息:

ls -i filename
stat filename

常见的符号

通配符

  • *:匹配任意数量的字符,例如 *.txt 匹配所有以 .txt 结尾的文件。
  • ?:匹配任意单个字符,例如 ?.txt 匹配所有一字符名称后跟 .txt 的文件。
  • [abc]:匹配方括号内的任何一个字符,例如 file[1-3].txt 匹配 file1.txtfile2.txtfile3.txt

逻辑操作符

  • &&:逻辑 AND,如果左边的命令成功执行(返回码为0),则执行右边的命令,例如 command1 && command2
  • ||:逻辑 OR,如果左边的命令执行失败(返回码非0),则执行右边的命令,例如 command1 || command2

命令分隔符

  • ;:允许在单行中顺序执行多个命令,无论前一个命令执行成功与否,例如 command1; command2
  • &:在后台运行命令,例如 command &

命令组合与子壳

  • {}:命令组合,命令间用分号隔开,作为一个整体执行,需要在最后一个命令后也使用分号,例如 { command1; command2; }
  • ():子壳(shell),在子shell中执行命令,例如 (command1; command2)

引用

  • "(双引号):用于引用字符串,保留某些特殊字符(如变量 $、命令替换等)的特殊意义。
  • '(单引号):用于精确引用字符串,内部所有字符均视为普通字符。
  • \(反斜杠):转义字符,用于移除紧随其后的字符的特殊意义,如 \$ 表示美元符号本身,而不是变量的引导符。

输入/输出操作符

  • >>:追加重定向,将输出追加到文件末尾而不是覆盖文件。
  • <:标准输入重定向,从文件或其他输入中读取数据。
  • <<<:Here字符串,将字符串重定向到命令的标准输入。

Cat

显示文件内容

  • 要查看一个或多个文件的内容,可以使用

    cat

    命令后跟文件名:

    cat file1.txt
  • 也可以同时查看多个文件的内容:

    cat file1.txt file2.txt

合并文件

  • cat

    命令可以用来合并多个文件的内容,并显示合并后的结果:

    cat file1.txt file2.txt > combined.txt
  • 这里,file1.txtfile2.txt 的内容被合并并重定向到新文件 combined.txt 中。

创建文件

  • 可以使用

    cat

    命令创建新文件:

    cat > newfile.txt
  • 输入此命令后,终端会等待用户输入。输入完成后,按 Ctrl+D(文件结束符)来保存输入到 newfile.txt

硬盘信息

du

  • 分析某个目录或文件的大小(如清理日志文件)。
  • 找到占用磁盘空间最多的目录。

df

  • 监控整个系统的磁盘使用情况(如查看分区剩余空间)。
  • 确认挂载点是否接近满载。

操作系统

1.死锁

采用“按序分配”策略可以破坏产生死锁的环路等待条件:正确。按序分配资源是一种避免死锁的方法。通过为资源分配一个全局顺序,确保进程只能按顺序申请资源,这样就能破坏“环路等待”这个死锁的必要条件。

产生死锁的现象是每个进程等待着某个不能得到且不可释放的资源:正确。死锁的经典定义就是每个进程都在等待另一个进程持有的资源,而这些资源又不会被释放,从而形成一个循环等待的情况。

在资源的动态分配过程中,防止系统进入安全状态,可避免发生死锁:错误。防止系统进入不安全状态才可以避免死锁。安全状态表示系统即使资源紧张,也能保证进程最终完成并释放资源,不会进入死锁。

银行家算法是最有代表性的死锁解除算法:错误。银行家算法属于死锁避免算法,不是死锁解除算法。它通过检查资源分配的安全性,防止系统进入不安全状态,从而避免死锁的发生,而不是解除已经发生的死锁。

为了形成死锁,以下四个条件必须同时满足,这些条件通常被称为死锁的四个必要条件:

互斥(Mutual Exclusion)

  • 定义:资源不能被多个进程共享,一次只能由一个进程使用。
  • 解释:如果一个资源可以被多个进程同时访问,那么互斥条件就不成立,因此不会出现因资源争夺导致的死锁。

请求与保持(Hold and Wait)

  • 定义:已经得到某些资源的进程可以请求新的资源。
  • 解释:进程至少已经保持了一个资源,并等待获取更多当前被其他进程持有的资源。

不可剥夺(No Preemption)

  • 定义:资源一旦被分配给一个进程,在该进程自愿释放资源之前,不能被强行剥夺。
  • 解释:系统无法强制进程释放它持有的资源,这使得其他进程可能会无限期地等待这个资源。

循环等待(Circular Wait)

  • 定义:存在一种进程资源的请求和分配模式,形成一个循环链,每个进程持有下一个进程所需要的至少一个资源。
  • 解释:每个进程至少持有一个资源,这个资源被链中的下一个进程所请求。因此,形成了一个封闭的循环,每个进程都在等待链中下一个进程持有的资源。

2.进程

孤儿进程是指父进程退出后,仍然在运行的子进程,这些子进程会被 init 进程(或 systemd)收养并正常运行,不会对系统产生不良影响。

僵尸进程则是子进程已经退出,但父进程未通过 waitwaitpid 回收其状态,导致其进程表项(PID)仍然保留在系统中,占用系统资源。僵尸进程可能影响系统创建新的进程,因此应尽量避免,而孤儿进程不会产生类似问题。

计算机网络

1.

若将网络192.168.0.0/22划分为5个子网,则可以划分出的最小子网的子网掩码是

A、255.255.255.0

B、255.255.255.128

C、255.255.255.192

D、255.255.255.224

子网划分可分为定长子网和变长子网。题目说的最小,故应该是变长子网。

1.对于定长子网,从32-22=10位主机号中取3位,一共可以表示8个子网,符合条件。此时答案为B

2.对于变长子网。从大的子网向小的子网划分。

192.168.0.0/22

192.168.0.0/23,192.168.2.0/23

192.168.0.0/23,192.168.2.0/24,192.168.3.0/24

192.168.0.0/23,192.168.2.0/24,192.168.3.0/25,192.168.3.128/25

192.168.0.0/23,192.168.2.0/24,192.168.3.0/25,192.168.3.128/26,192.168.3.192/26

最后面那个是最小的子网,掩码为255.255.255.192

OSI

用于描述不同计算系统之间进行网络通信的各个层面。这个模型由国际标准化组织(ISO)在1984年提出,主要用于帮助不同系统之间进行互操作。OSI模型将网络通信过程分为七个层级,每一层都有其特定的功能和协议:

  1. 物理层(Layer 1 - Physical Layer)
    • 负责在物理媒介上实现原始的比特流传输,涉及到电气、光学、和机械规格。它确保数据的物理传输。
  2. 数据链路层(Layer 2 - Data Link Layer)
    • 负责在相连的节点之间创建逻辑连接,处理帧的寻址、错误检测和修正。例如,以太网(Ethernet)和Wi-Fi都工作在这一层。
  3. 网络层(Layer 3 - Network Layer)
    • 负责在多个网络之间建立路径,并进行地址解析和路由选择。互联网协议(IP)是这一层的一个关键协议。
  4. 传输层(Layer 4 - Transport Layer)
    • 负责为两个主机上的应用提供端到端的数据传输服务。这一层包括了如TCP(传输控制协议)和UDP(用户数据报协议)。
  5. 会话层(Layer 5 - Session Layer)
    • 负责在数据传输中建立、管理和终止会话。这一层可以设置检查点,从而在通信失败时从上次成功的检查点恢复数据传送。
  6. 表示层(Layer 6 - Presentation Layer)
    • 负责数据的表示、安全(加密)和压缩。这一层确保从一个系统发出的数据能被另一个系统的应用层正确解读。
  7. 应用层(Layer 7 - Application Layer)
    • 为应用程序提供服务,并与应用程序直接交互。这一层涉及到一些高级的API,比如HTTP、FTP等。

TCP

(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它通过三次握手来建立连接,确保双方都准备好进行数据传输,并通过四次挥手来终止连接。下面详细解释这两个过程:

三次握手的目的是双方确认自身与对方的发送和接收是正常的。这一过程包括以下步骤:

  1. SYN发送:
    • 客户端发送一个SYN(同步序列编号)报文到服务器。此报文中含有客户端的初始序列号,用于同步序列号。
    • 状态标记为 SYN
  2. SYN-ACK接收:
    • 服务器收到SYN后,回送一个确认包(ACK)和它自己的同步包(SYN)。这个包含两个标志:SYNACK
    • 服务器的ACK号为客户端的初始序列号加一,表明服务器已经接收到客户端的SYN。
  3. ACK发送:
    • 客户端接收到服务器的SYN-ACK后,发送一个ACK包作为响应,确认号为服务器的序列号加一。
    • 此时,客户端到服务器的连接已经建立。

四次挥手的目的是允许双方都能够终止已经建立的连接。这一过程涉及以下步骤:

  1. FIN发送:
    • 客户端决定关闭连接,发送一个FIN(结束)报文。
    • 状态标记为 FIN
  2. ACK接收:
    • 服务器接收到FIN后,发送一个ACK报文作为响应,并进入关闭等待状态。
    • 服务器确认收到客户端的终止请求。
  3. FIN发送:
    • 服务器完成向客户端的数据传输后,发送一个FIN给客户端,请求关闭连接。
  4. ACK接收:
    • 客户端接收到服务器的FIN报文后,发送一个ACK报文作为响应,然后进入TIME-WAIT状态。客户端会等待足够的时间以确保服务器接收到其ACK报文,然后最终关闭连接。

TCP和UDP

是两种基本的传输层协议,它们在网络中扮演着至关重要的角色,但有着不同的特性和用途。以下是TCP和UDP的主要区别:

  1. 连接性:
    • TCP 是面向连接的协议。在数据开始传输之前,它需要在两个通信端点之间建立连接。这一过程涉及到“三次握手”,确保双方都准备好进行数据交换。
    • UDP 是无连接的协议。它发送数据时不需要建立连接,因此可以直接发送数据包到目的地,无需事先协商。
  2. 可靠性:
    • TCP 提供可靠的服务。它通过序列号、确认响应(ACKs)、数据重传等机制确保数据的正确传输。如果数据包丢失或错误,TCP将重新发送该数据包。
    • UDP 提供的是尽力而为的服务,不保证数据包的可靠送达。没有建立重传机制,如果数据包在传输过程中丢失,UDP不会尝试恢复。
  3. 数据完整性:
    • TCP 保证数据的顺序和完整性。如果接收到的数据片段是乱序的,TCP将重新排列成正确的顺序。
    • UDP 不保证数据的顺序,接收到的数据顺序可能与发送顺序不同。
  4. 速度和效率:
    • TCP 由于其重传机制、顺序管理和较重的头部管理,其处理速度相比UDP要慢,但这是为了交换数据的完整性和可靠性。
    • UDP 因为不进行错误检查和恢复,所以通常比TCP快,这在不需要每个数据包都完美到达的应用场景(如流媒体、实时游戏)中非常有用。
  5. 头部开销:
    • TCP 头部较大,最小为20字节,因为它需要携带更多的控制信息。
    • UDP 的头部较小,固定8字节,这使得它在网络通信中的开销更小。
  6. 用例:
    • TCP 常用于需要高度可靠性的应用,如网页浏览、电子邮件和文件传输。
    • UDP 常用于对实时性要求较高的应用,如视频和音频传输、在线游戏和广播通信。

TCP 是面向连接的,如打电话要先拨号建立连接:正确。TCP 是面向连接的协议,在传输数据之前,客户端和服务器需要通过三次握手建立连接,然后才能传输数据,类似于打电话前要先拨号连接对方。

TCP 支持一对一,一对多,多对一和多对多的交互通信:错误。TCP 通信是一对一的,即每个 TCP 连接仅有两个端点,无法直接支持一对多、多对一或多对多通信。多点通信通常使用 UDP 协议。

TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流:正确。TCP 是面向字节流的协议,它将数据视为一个连续的字节流,没有消息边界,数据可以按照任意大小分段传输。

UDP 是无连接的,即发送数据之前不需要建立连接:正确。UDP 是无连接协议,不需要建立连接就可以直接发送数据包。UDP 发送数据时不进行握手,因此传输效率较高,但不保证数据可靠到达。

POST 和 GET

GET请求通过URL(请求行)提交数据,在URL中可以看到所传参数。POST通过“请求体”传递数据,参数不会在url中显示。

GET请求提交的数据有长度限制,POST请求没有限制。

GET请求返回的内容可以被浏览器缓存起来。而每次提交的POST,浏览器在你按下F5的时候会跳出确认框,浏览器不会缓存POST请求返回的内容。

GET对数据进行查询,POST主要对数据进行增删改!简单说,GET是只读,POST是写。

POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。

HTTP协议中提到GET是安全的方法,"安全" 并不意味着数据是加密或不容易被窃听,而是指该请求不会对服务器上的资源产生修改性影响。换句话说,使用 GET 方法的请求不应该有副作用,例如修改数据、更新资源等。

URL 使用的是 ASCII 编码格式。对于非 ASCII 字符,需要进行百分号编码(URL 编码)才能在 URL 中传输。

GET 方法

请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:

/test/demo_form.php**?name1=value1&name2=value2**

有关 GET 请求的其他一些注释:

  • GET 请求可被缓存
  • GET 请求保留在浏览器历史记录中
  • GET 请求可被收藏为书签
  • GET 请求不应在处理敏感数据时使用
  • GET 请求有长度限制
  • GET 请求只应当用于取回数据
  • 在HTTP规范中,没有对URL的长度和传输的数据大小进行限制。但是在实际开发过程中,对于GET,特定的浏览器和服务器对URL的长度有限制。因此,在使用GET请求时,传输数据会受到URL长度的限制。

POST 方法

请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的:

POST /test/demo_form.php HTTP/1.1 Host: runoob.com name1=value1&name2=value2

有关 POST 请求的其他一些注释:

  • POST 请求不会被缓存
  • POST 请求不会保留在浏览器历史记录中
  • POST 不能被收藏为书签
  • POST 请求对数据长度没有要求
  • 对于POST,由于不是URL传值,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,Apache、IIS都有各自的配置。

请求过程

POST 请求过程

  1. TCP 三次握手

    • 第一次握手:浏览器发送 SYN 包请求建立连接。
    • 第二次握手:服务器回应 SYN-ACK 包。
    • 第三次握手:浏览器发送 ACK 包确认连接。
  2. 发送 POST 请求

    • 浏览器发送 POST 请求头。如果请求头中包含 Expect: 100-continue,服务器会先返回 100 Continue 响应。
  3. 发送请求体

    • 浏览器发送请求体数据。
  4. 服务器响应

    • 服务器处理请求并返回 200 OK 或其他适当的响应状态码。

GET 请求过程

  1. TCP 三次握手

    • 与 POST 请求相同。
  2. 发送 GET 请求

    • 浏览器在第三次握手后立即发送 GET 请求头和任何必要的数据(通常没有请求体)。
  3. 服务器响应

    • 服务器处理请求并返回 200 OK 和请求的资源。

其他注意事项

  • HTTP/1.1 和 HTTP/2:在 HTTP/1.1 中,100 Continue 是可选的,通常用于较大或不确定的请求体。在 HTTP/2 中,连接和数据传输方式有所优化。
  • 持久连接:HTTP/1.1 默认使用持久连接,可以复用同一个 TCP 连接进行多个请求。

这些步骤确保了浏览器和服务器之间的可靠通信。

2MSL

在TCP连接的关闭过程中,主动关闭的一方在发送完最后一个ACK后进入TIME_WAIT状态。这个状态的持续时间是2倍的MSL(Maximum Segment Lifetime),即2MSL。

为什么需要2MSL?

  1. 数据包在网络中的传播延迟

    • TCP需要确保在网络中可能存在的所有旧数据包都被安全丢弃。MSL是指一个TCP段在网络中可以存在的最长时间。2MSL保证即使在最坏情况下(数据包绕行网络),所有旧数据包都已经过期。
  2. 可靠的连接终止

    • 当主动关闭方发送最后的ACK确认对方的FIN时,可能ACK会丢失。等待2MSL确保对方有足够的时间重传FIN,并且主动关闭方可以再次发送ACK。
  3. 避免端口重用冲突

    • 在TIME_WAIT状态期间,TCP连接的端口对(源IP地址、源端口、目的IP地址、目的端口)被占用。等待2MSL时间可以防止新连接重用相同的端口对,从而避免旧连接的数据包干扰新连接。

TIME_WAIT状态的过程

  1. 发送FIN

    • 主动关闭方发送FIN报文段,进入FIN_WAIT_1状态。
  2. 收到FIN的ACK

    • 收到对方的ACK后,进入FIN_WAIT_2状态。
  3. 收到对方的FIN

    • 收到对方的FIN后,发送ACK并进入TIME_WAIT状态。
  4. 等待2MSL时间

    • 在TIME_WAIT状态中,连接将保持2MSL时间。
    • 这段时间结束后,连接资源可以被释放。

实际影响

  • 资源占用:长时间的TIME_WAIT会占用系统资源(如文件描述符),特别是在高负载的服务器上。
  • 优化措施:一些操作系统提供了优化选项,如TCP端口重用或缩短TIME_WAIT时间,但这些优化需要谨慎使用,以避免潜在的连接问题。

通过2MSL机制,TCP协议能够更可靠地管理连接的关闭过程,确保数据完整性和连接安全性。

什么是广播地址

广播地址是一个网络范围内的最后一个 IP 地址。它用于将数据包发送到网络中的所有主机。广播地址的计算规则是:将网络地址的主机部分全部置为 1

理解 192.168.0.0/22 的范围

  • 192.168.0.0/22 表示一个 22 位的子网掩码。子网掩码的二进制表示为:

    11111111.11111111.11111100.00000000

    对应的十进制子网掩码是 255.255.252.0

  • 22 位掩码意味着前 22 位用于标识网络,剩下的 10 位用于标识主机。因此,在 192.168.0.0/22 网络中,主机部分有 10 位。

计算 IP 地址范围

要计算 IP 地址范围,首先需要确定网络地址和广播地址:

  • 网络地址:是 IP 地址的最小值,主机部分全为 0。因此 192.168.0.0/22 的网络地址是 192.168.0.0

  • 广播地址:是 IP 地址范围的最大值,主机部分全为 1。要计算广播地址,可以通过将主机部分的 10 位全置为 1。

    主机部分的 10 位全为 1:

    1111111111

    转换为十进制,这相当于 1023,也就是说,从网络地址 192.168.0.0 开始,加上 1023 就得到广播地址。

    所以,广播地址为:

    yaml
    192.168.0.0 + 1023 = 192.168.3.255

广播地址是用于向同一子网内的所有设备发送消息的地址。它的作用是让同一网络中的所有设备都能接收到该数据包。广播地址是在一个网络中主机地址部分全部为二进制1的地址。

网络地址是用于标识一个子网的地址,它表示该子网中的所有设备所共有的前缀部分。网络地址的主机部分全为二进制0。在IP地址划分中,网络地址用来区分不同的网络,且不能作为主机地址使用。

子网的数量必须是 2 的幂

IP 地址是 32 位二进制数,划分子网时,子网掩码决定了多少位用来标识网络和子网,剩余的位数用于标识主机。当你从主机位借用一些位给子网时,这些位的组合数决定了子网的数量。

举个例子:

  • 假设你有一个 /24 的网络,子网掩码是 255.255.255.0,其中前 24 位用于表示网络地址,剩余的 8 位用于主机。
  • 如果你想划分子网,你可以从主机位中“借用”一些位用作子网编号。

例如:

  • 借用 1 位:2^1 = 2 个子网
  • 借用 2 位:2^2 = 4 个子网
  • 借用 3 位:2^3 = 8 个子网

这种借位方式基于二进制系统,因此子网的数量只能是 2 的幂,因为每增加一位子网位数,都会将子网的数量翻倍。

常用的网络端口

20/21 - FTP (File Transfer Protocol)

20端口用于FTP的数据传输,21端口用于控制信息的交换。

22 - SSH (Secure Shell)

用于安全地访问远程计算机,也常用于端口转发。

23 - Telnet

用于未加密的文本通信,现在已经大多被更安全的SSH所替代。

25 - SMTP (Simple Mail Transfer Protocol)

用于电子邮件的发送。

53 - DNS (Domain Name System)

用于解析域名到IP地址。

80 - HTTP (Hypertext Transfer Protocol)

用于传输Web页面。

110 - POP3 (Post Office Protocol version 3)

用于接收电子邮件。

143 - IMAP (Internet Message Access Protocol)

用于访问和管理电子邮件。

443 - HTTPS (HTTP Secure)

用于安全的Web浏览,加密HTTP协议以提供安全性。

445 - SMB (Server Message Block)

用于Windows网络文件共享和打印服务。

993 - IMAP over SSL (IMAPS)

用于通过SSL安全地连接到IMAP服务器。

995 - POP3 over SSL (POP3S)

用于通过SSL安全地连接到POP3服务器。

1433 - SQL Server

微软SQL Server数据库管理系统使用的端口。

1521 - Oracle Database

Oracle数据库通信使用的端口。

3306 - MySQL

MySQL数据库服务使用的端口。

3389 - RDP (Remote Desktop Protocol)

用于Windows远程桌面连接。

5432 - PostgreSQL

PostgreSQL数据库使用的端口。

6379 - Redis

Redis键值存储数据库使用的端口。

27017 - MongoDB

MongoDB NoSQL数据库使用的端口。

数据结构

1.

若一序列进栈顺序为a1,a2,a3,a4,问存在多少种可能的出栈序列

image-20240919210651650

卡特兰数,相关问题还有:已知前序遍历的顺序是xxxx求这棵树有多少种形状

2.

把14,27,71,50,93,39按顺序插入一棵树,插入的过程不断调整使树为平衡排序二叉树,最终形成平衡排序二叉树高度为?并且画出对应过程。

我们需要按照给定的顺序将数字插入到AVL树中,并在每次插入后进行必要的平衡调整。AVL树是一种自平衡二叉搜索树,确保任何节点的左右子树高度差不超过1。

插入过程:

  1. 插入14:

    14
    • 树的高度:1
    • 无需调整。
  2. 插入27:

      14
        \
        27
    • 树的高度:2
    • 计算平衡因子(BF):BF(14) = 0 - 1 = -1
    • 无需调整。
  3. 插入71:

      14
        \
        27
          \
          71
    • 树的高度:3
    • 计算BF(14) = 0 - 2 = -2(不平衡)
    • 这是一个右-右情况,需对节点14进行左旋。

    左旋后:

        27
       /  \
      14  71
    • 更新高度,树的高度:2
    • 所有节点平衡。
  4. 插入50:

        27
       /  \
      14  71
          /
         50
    • 树的高度:3
    • 计算BF(27) = 1 - 2 = -1
    • 无需调整。
  5. 插入93:

        27
       /  \
      14  71
          / \
         50 93
    • 树的高度:3
    • 计算BF(27) = 1 - 2 = -1
    • 无需调整。
  6. 插入39:

        27
       /  \
      14  71
          / \
         50 93
        /
       39
    • 树的高度:4
    • 计算BF(27) = 1 - 3 = -2(不平衡)
    • 这是一个右-左情况,需要先对节点71进行右旋,再对节点27进行左旋。

    右旋节点71后:

        27
       /  \
      14  50
          /  \
         39  71
               \
               93

    左旋节点27后:

         50
        /  \
       27  71
      / \    \
     14 39   93
    • 更新高度,树的高度:3
    • 所有节点平衡。

最终AVL树高度为3。

树的高度(Height of a Tree)

  • 定义:树的高度是指树中节点的最大层次(层数)。换句话说,就是从根节点到叶子节点的最长路径所经过的节点数
  • 计算方法:从根节点开始,根节点所在的层数为1,每向下经过一个节点,层数加1。

树的深度(Depth of a Tree)

  • 定义:树的深度通常指的是树中节点的最大深度,即从根节点到最深叶子节点所经过的边数
  • 计算方法:从根节点开始,根节点的深度为0,每向下经过一条边,深度加1。

二叉搜索树基本性质:

  1. 二叉搜索树的性质:
    • 左子树中所有节点的值均小于根节点的值。
    • 右子树中所有节点的值均大于根节点的值。
    • 左、右子树也分别是二叉搜索树。
  2. 平衡条件:
    • 平衡因子(Balance Factor):对于每个节点,左子树高度右子树高度之差的绝对值不超过某个特定值(例如,在 AVL 树中,平衡因子的绝对值不超过 1)。

安全

工具

Mimikatz 是一个强大的工具,可以从内存中抓取Windows登录密码和哈希值。

Pwdump7 是一个工具,专门用于从Windows系统中导出密码哈希。

sqlmap 是用于SQL注入的工具,主要针对数据库,不是专门用于抓取Windows登录密码。 hashcat 是一个密码破解工具,用于暴力破解密码或哈希值,但不是抓取密码的工具。

wevtutil:wevtutil 是 Windows 提供的一个命令行工具,可以用来查询、导出和管理 Windows 事件日志,包括安全日志。

eventquery.vbs:这是一个旧的脚本工具,虽然较为过时,但它也可以用来查询 Windows 事件日志,包括安全日志。

systeminfo:systeminfo 是用于查看系统信息的命令,如操作系统版本、补丁、启动时间等,不能用于查看事件日志。

dsquery:dsquery 是一个 Active Directory 查询工具,用于查询 AD 对象,不能用于查看事件日志。

sleep():sleep() 函数在 SQL 注入中非常常见,它用于使 SQL 查询延迟执行指定的时间。例如,sleep(5) 将延迟 5 秒。

benchmark:benchmark() 函数会执行指定次数的操作,用于测试性能或在 SQL 注入中制造延迟。例如,benchmark(1000000, md5('test')) 通过大量计算延迟查询执行。

pthread_join:这是 C/C++ 多线程编程中的函数,不是 MySQL 的延时函数。

Burpsuite 是一款用于渗透测试的综合性工具,可以拦截、修改和分析 HTTP/HTTPS 数据包,非常适合用来抓取和修改 HTTP 数据包。

Fiddler 是一个专门用于调试 HTTP 和 HTTPS 通信的工具,能够拦截和查看 HTTP 数据包,广泛应用于网络分析和应用调试。、

Hackbar 是浏览器插件,主要用于帮助执行各种常见的安全测试操作,如 XSS 和 SQL 注入,但它不具备拦截或抓取 HTTP 数据包的功能。

Nmap 是网络扫描工具,主要用于端口扫描、网络探索等,不是用于抓取 HTTP 数据包的工具。

反编译工具

Java 反编译工具

  • JD-GUI: 易于使用的图形界面工具,能够快速将Java字节码反编译成源代码。
  • CFR: 支持Java 14及以上版本的Java字节码反编译器,具有高度的复原准确性。
  • Procyon: 在处理Java 8的lambda表达式等较新特性上表现良好的反编译工具。
  • Fernflower: IntelliJ IDEA 集成的反编译器,广泛用于在IDE中直接反编译类文件。

.NET 反编译工具

  • ILSpy: 开源.NET程序集浏览器和反编译器,可以处理C#代码。
  • dnSpy: 集反编译和调试功能于一体的工具,特别适合分析和修改.NET程序。
  • JustDecompile: 免费且功能强大的.NET反编译工具,提供插件支持和频繁的更新。

C/C++ 反编译工具

  • IDA Pro (Interactive DisAssembler): 带有Hex-Rays反编译器的强大反汇编和反编译平台,支持多种架构。
  • Ghidra: 美国国家安全局开发的开源软件逆向工程工具,支持多种语言和平台。
  • Hopper Disassembler: 针对Mac和Linux的反编译工具,可以生成近似C代码的伪代码。

Android 反编译工具

  • APKTool: 解包和重打包Android APKs的工具,常用于资源解析和修改。
  • JADX: 无需Java SDK即可反编译DEX到Java源代码的工具,支持命令行和GUI界面。
  • Bytecode Viewer: 集成多个反编译器和反汇编器的Android逆向工程工具。

其他平台的反编译工具

  • Binary Ninja: 适用于专业人士的低级分析平台,提供逻辑图和伪代码。
  • Radare2: 开源的逆向工程框架,支持广泛的架构和文件类型,包括用于逆向的反编译插件。

知识

AES (Advanced Encryption Standard)

  • 类型: 对称加密算法
  • 特点
    • AES是由美国国家标准与技术研究院(NIST)于2001年发布的对称加密标准。
    • 它使用相同的密钥来进行加密和解密。
    • 密钥长度可为128位、192位或256位,因此它的安全性和效率非常高。
    • AES广泛应用于商业和政府系统中的数据加密。

DES (Data Encryption Standard)

  • 类型: 对称加密算法
  • 特点
    • DES是1977年由NIST发布的对称加密标准,已经过时。
    • 它使用56位密钥来加密数据,安全性较低,容易被暴力破解。
    • DES在许多场景中被AES取代,但它在历史上有重要意义,是早期的加密标准。

RSA (Rivest-Shamir-Adleman)

  • 类型: 非对称加密算法
  • 特点
    • RSA是一种非对称加密算法,使用公钥和私钥对来加密和解密数据。
    • 公钥用于加密,私钥用于解密。
    • RSA的安全性基于大数分解问题,它广泛用于安全通信,如HTTPS、数字签名和证书。
    • 密钥长度通常为2048位或更长,以确保足够的安全性。

对称加密

  • 定义: 对称加密算法使用相同的密钥进行加密和解密。
  • 特点
    • 速度较快,适合对大数据量的加密。
    • 主要的挑战在于安全地分发密钥,因为如果密钥泄露,通信内容将不再安全。
  • 常见算法: AES、DES、3DES等。

非对称加密

  • 定义: 非对称加密算法使用一对密钥——公钥和私钥。公钥用于加密,私钥用于解密。
  • 特点
    • 公钥可以公开,而私钥需要保密。
    • 安全性较高,适合在互联网等不安全环境下使用。
    • 计算复杂度较高,因此速度较慢,通常用于加密较小的数据或密钥交换。
  • 常见算法: RSA、ECC(椭圆曲线加密)、DSA等。

窃听:属于被动攻击,攻击者监听网络通信中的数据,不改变数据内容,目的收集信息。

流量分析:也是被动攻击,通过分析网络流量模式、通信量等,获取系统行为或用户活动的线索,不直接破坏系统。

SQL注入攻击:属于主动攻击,攻击者通过恶意输入破坏或控制数据库。

暴力破解:也是主动攻击,攻击者尝试通过暴力尝试密码等方式来获取对系统的访问权限。

XSS

**反射型XSS **是比较常见和广泛的一类,举例来说,当一个网站的代码中包含类似下面的语句:<?php echo "<p>hello, $_GET['user']</p>";?> ,那么在访问时设置 /?user=</p><script>alert("hack")</script><p> ,则可执行预设好的JavaScript代码。

反射型XSS通常出现在搜索等功能中,需要被攻击者点击对应的链接才能触发,且受到XSS Auditor、NoScript等防御手段的影响较大。

储存型XSS 相比反射型来说危害较大,在这种漏洞中,攻击者能够把攻击载荷存入服务器的数据库中,造成持久化的攻击。例如在网站评论区提交xss代码。

**DOM型XSS **不同之处在于DOM型XSS一般和服务器的解析响应没有直接关系,而是在JavaScript脚本动态执行的过程中产生的。

Python

三元表达式

Python 中没有 x>y?a:b 形式的三元表达式,但可以使用以下形式实现

python
value = true_value if condition else false_value

赋值

下列哪个语句在Python中是非法的( )

A x = y = z = 1

B x = (y = z + 1)

C x, y = y, x

D x += y

多重赋值:x=y=z=1

多元赋值:x,y,z=1,3,'a string'

增量赋值:x+=1

但是 B 非法,因为将 x = (y = z + 1) 中右半部分括起来后,相当于把一个赋值表达式赋值给变量 x,因此出现语法错误。

正确的赋值语法应把括号去掉,即:x = y = z + 1 。这种赋值方式可称为连续赋值。

计算顺序是从右向左,表达式 x = y = z + 1 的运算顺序如下:

  1. 首先计算 z + 1
  2. 将计算结果赋值给 y
  3. 然后将同一个结果赋值给 x

这意味着 xy 都会得到 z + 1 的值。

python
class Parent(object):
    x = 1

class Child1(Parent):
    pass

class Child2(Parent):
    pass

p = Parent()
c1 = Child1()
c2 = Child2()

print(p.x, c1.x, c2.x)  # 输出:1 1 1(访问类变量)
Child1.x = 2
print(p.x, c1.x, c2.x)  # 输出:1 2 1(访问修改后的类变量)
p.x = 4
print(p.x, c1.x, c2.x)  # 输出:4 2 1(p 实例有自己的 x 属性)
Parent.x = 3
print(p.x, c1.x, c2.x)  # 输出:4 2 3(p 实例的 x 不变,c1 和 c2 继续遵循类变量规则)
  • 实例化后,每个实例有自己的命名空间:修改实例变量不会影响类变量或其他实例。
  • 类变量的修改仍影响未覆盖的实例:如果实例没有独立的 x 属性,它会继续引用类变量。

总结:实例化后,实例的行为会依赖于具体的变量访问优先级(实例变量优先于类变量),因此结果会有所不同。

单例

在 Python 中,单例(Singleton)是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。换句话说,单例模式限制了类的实例化,使得在整个应用程序中只存在一个实例。

单例模式的实现

有多种方法可以在 Python 中实现单例模式,以下是几种常见的方法:

方法 1:使用类变量

python
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

# 使用示例
s1 = Singleton()
s2 = Singleton()

print(s1 is s2)  # 输出: True

方法 2:使用装饰器

python
def singleton(cls):
    instances = {}

    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

@singleton
class Singleton:
    pass

# 使用示例
s1 = Singleton()
s2 = Singleton()

print(s1 is s2)  # 输出: True

在 Python 中,模块本身就是单例的,因为模块在首次导入时只会被执行一次。因此,可以将需要单例的功能放在模块中。

python
# singleton_module.py
class Singleton:
    pass

singleton_instance = Singleton()

然后在其他地方导入这个模块即可:

python
from singleton_module import singleton_instance

s1 = singleton_instance
s2 = singleton_instance

print(s1 is s2)  # 输出: True

单例模式在某些情况下非常有用,比如需要控制资源访问、管理全局状态或配置时。然而,过度使用单例可能导致代码难以测试和维护,因此在使用时应谨慎考虑。

魔术方法

在Python中,魔法方法(也称为特殊方法或双下划线方法)是具有特殊用途的内置方法。它们通常用于实现某些操作符的行为或对象的协议。以下是一些常见的魔法方法:

  1. __init__: 构造函数,用于初始化对象。

    python
    class MyClass:
        def __init__(self, value):
            self.value = value
  2. __new__ 必须返回一个类的实例。通常是通过调用 super().__new__(cls) 来实现,如果 __new__ 返回类的实例,则调用 __init__ 方法进行初始化。接受类本身 cls 作为第一个参数,其后参数与 __init__ 相同。

    python
    class MyClass:
        def __new__(cls, *args, **kwargs):
            print("Creating instance")
            instance = super().__new__(cls)
            return instance
    
        def __init__(self, value):
            print("Initializing instance")
            self.value = value
    
    # 使用示例
    obj = MyClass(10)
  3. __str__: 返回对象的可读字符串表示。

    python
    class MyClass:
        def __str__(self):
            return f"MyClass with value {self.value}"
  4. __repr__: 返回对象的官方字符串表示,通常用于调试。

    python
    class MyClass:
        def __repr__(self):
            return f"MyClass({self.value})"
  5. __add__: 实现加法运算符 +

    python
    class MyClass:
        def __init__(self, value):
            self.value = value
        
        def __add__(self, other):
            return MyClass(self.value + other.value)
  6. __len__: 返回对象的长度。

    python
    class MyClass:
        def __len__(self):
            return len(self.value)
  7. __getitem__: 支持索引访问。

    python
    class MyClass:
        def __init__(self, items):
            self.items = items
        
        def __getitem__(self, index):
            return self.items[index]
  8. __setitem__: 支持索引赋值。

    python
    class MyClass:
        def __init__(self, items):
            self.items = items
        
        def __setitem__(self, index, value):
            self.items[index] = value
  9. __eq__: 实现等于运算符 ==

    python
    class MyClass:
        def __init__(self, value):
            self.value = value
        
        def __eq__(self, other):
            return self.value == other.value

这些魔法方法使得自定义对象可以像内置对象一样使用,提供了很大的灵活性和可扩展性。

super

在 Python 中,super().__init__(map)super(RegexConverter, self).__init__(map) 都用于调用父类的构造函数,但它们有一些细微的区别:

  1. Python 版本:

    • super().__init__(map) 是 Python 3 的简化语法,直接调用父类方法。
    • super(RegexConverter, self).__init__(map) 是 Python 2 中常用的语法,在 Python 3 中也可以使用。
  2. 可读性:

    • super().__init__(map) 更简洁,推荐在 Python 3 中使用。
    • super(RegexConverter, self).__init__(map) 明确指定类和实例,可能更易于理解在复杂继承结构中的调用顺序。
  3. 使用场景:

    • 在 Python 3 中,通常使用 super(),因为它更简洁且不容易出错。
    • 在 Python 2 中,必须使用 super(RegexConverter, self) 这种形式。

在现代 Python 代码中,推荐使用 super() 的简化形式。

类变量

python
class Animal:
    name = ""
    _name = ""
    def __init__(self, name, species):
        self._name = name
        self.name = name
        self.species = species
    
    def make_sound(self):
        print("Some generic sound")

在这段代码中,name_name 有以下区别:

  1. name (类属性):

    • 定义在类体中,作为类属性。所有实例共享这个属性。
    • __init__ 中,self.name = name 会将实例的 name 属性覆盖类属性。
  2. _name (类属性):

    • 同样定义在类体中,作为类属性,带有前导下划线,表示这是一个“受保护的”属性。
    • __init__ 中,self._name = name 会将实例的 _name 属性覆盖类属性。
  3. self.nameself._name (实例属性):

    • __init__ 方法中,通过 self 赋值,创建实例属性。这些属性是每个实例独有的。

使用 @property

你可以使用 @property 来控制对 name_name 的访问和修改。例如:

python
class Animal:
    def __init__(self, name, species):
        self._name = name
        self.species = species
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        # 这里可以加入额外的逻辑,比如验证
        self._name = value
    
    def make_sound(self):
        print("Some generic sound")
        
# 创建一个 Animal 实例
dog = Animal("Buddy", "Dog")

# 访问 name 属性(调用 @property)
print(dog.name)  # 输出: Buddy

# 修改 name 属性(调用 @name.setter)
dog.name = "Max"

# 再次访问 name 属性
print(dog.name)  # 输出: Max

# 调用 make_sound 方法
dog.make_sound()  # 输出: Some generic sound

在这个例子中:

  • @property 装饰器将 name 方法变成一个只读属性。
  • @name.setter 允许你定义一个设置器方法,以便在设置 name 属性时执行额外的逻辑。

函数

传参

以下是一些关于 *args**kwargs 的更多用法和知识点:

  1. *args**kwargs 的基本用法
  • *args 用于传递不定数量的位置参数。
  • **kwargs 用于传递不定数量的关键字参数。
python
def example(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

example(1, 2, 3, a=4, b=5)
  1. 解包
  • 使用 * 解包列表或元组。
  • 使用 ** 解包字典。
python
def add(x, y):
    return x + y

numbers = (1, 2)
print(add(*numbers))  # 输出: 3

params = {'x': 3, 'y': 4}
print(add(**params))  # 输出: 7
  1. 强制关键字参数
  • 使用 * 强制后续参数必须作为关键字参数传递。
python
def func(x, *, y):
    print(x, y)

func(1, y=2)  # 正确
  1. 组合使用
  • 可以在同一个函数中同时使用位置参数、*args、关键字参数、**kwargs
python
def complex_function(a, b, *args, c=3, **kwargs):
    print("a:", a)
    print("b:", b)
    print("args:", args)
    print("c:", c)
    print("kwargs:", kwargs)

complex_function(1, 2, 4, 5, 6, c=7, d=8, e=9)
  1. 参数顺序
  • 参数顺序为:位置参数、*args、默认参数、**kwargs
python
def ordered_function(a, b, *args, d=4, **kwargs):
    print(a, b, args, d, kwargs)

ordered_function(1, 2, 3, 4, 5, d=6, x=7, y=8)
  1. 实际应用
  • 在装饰器中,*args**kwargs 可以用于处理不定数量的输入参数。
  • 在函数重载场景中,它们可以用于灵活地处理不同数量和类型的参数。

这些用法可以帮助你更灵活地设计函数,处理各种参数传递场景。

解包

当然,关于 * 解包,有以下几个知识点:

  1. 列表和元组解包

可以使用 * 将列表或元组解包为单独的元素:

python
numbers = [1, 2, 3]
print(*numbers)  # 输出: 1 2 3
  1. 函数调用中的解包

在函数调用时,可以使用 * 解包列表或元组,将其元素作为位置参数传递:

python
def add(x, y, z):
    return x + y + z

nums = (1, 2, 3)
print(add(*nums))  # 输出: 6
  1. 字典解包

使用 ** 可以将字典解包为关键字参数:

python
def display_info(name, age):
    print(f"Name: {name}, Age: {age}")

info = {'name': 'Alice', 'age': 30}
display_info(**info)  # 输出: Name: Alice, Age: 30
  1. 合并列表和字典

可以利用解包操作符合并列表和字典:

python
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined_list = [*list1, *list2]
print(combined_list)  # 输出: [1, 2, 3, 4, 5, 6]

dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}
combined_dict = {**dict1, **dict2}
print(combined_dict)  # 输出: {'a': 1, 'b': 2, 'c': 3, 'd': 4}
  1. 解包可迭代对象

* 可以用于解包任意可迭代对象,例如字符串、集合等:

python
string = "hello"
print(*string)  # 输出: h e l l o

这些解包技巧可以帮助你更灵活地处理数据结构和函数参数。

列表

移除列表指定元素

在 Python 中,可以使用多种方法从列表中删除指定的数据:

  1. 使用 remove() 方法:删除第一个匹配的元素。

    python
    my_list = [1, 2, 3, 4, 2]
    my_list.remove(2)
    print(my_list)  # 输出: [1, 3, 4, 2]
  2. 使用 pop() 方法:根据索引删除元素,并返回该元素。

    python
    my_list = [1, 2, 3, 4]
    removed_item = my_list.pop(2)
    print(my_list)  # 输出: [1, 2, 4]
    print(removed_item)  # 输出: 3
  3. 使用 del 语句:根据索引删除元素。

    python
    my_list = [1, 2, 3, 4]
    del my_list[1]
    print(my_list)  # 输出: [1, 3, 4]
  4. 使用列表推导式:删除所有匹配的元素。

    python
    my_list = [1, 2, 3, 2, 4]
    my_list = [x for x in my_list if x != 2]
    print(my_list)  # 输出: [1, 3, 4]

选择合适的方法取决于你的具体需求。

以下python代码的输出是什么?

python
numbers = [1, 2, 3, 4]
numbers.append([5,6,7,8])
print(len(numbers))

A 4

B 5

C 8

D 12

E An exception is thrown

python
numbers = [1, 2, 3, 4]
numbers.append([5, 6, 7, 8])
print(len(numbers))

append 方法会将整个列表 [5, 6, 7, 8] 作为一个单独的元素添加到 numbers 列表中。因此,numbers 列表现在包含五个元素:[1, 2, 3, 4, [5, 6, 7, 8]]

所以,len(numbers) 的输出是 5

extend 方法则会将列表中的每个元素逐一添加。

python
numbers = [1, 2, 3, 4]
numbers.extend([5, 6, 7, 8])
print(len(numbers))

extend 方法会将 [5, 6, 7, 8] 中的每个元素添加到 numbers 列表中,所以 numbers 变成 [1, 2, 3, 4, 5, 6, 7, 8]

闭包的延迟绑定

python
def multipliers():
    return [lambda x: i * x for i in range(4)]

print([m(2) for m in multipliers()])

上面代码输出的结果是6, 6, 6, 6。

你如何修改上面的multipliers的定义产生想要的结果?

上述问题产生的原因是Python闭包的延迟绑定。这意味着内部函数被调用时,参数的值在闭包内进行查找。因此,当任何由multipliers()返回的函数被调用时,i的值将在附近的范围进行查找。那时,不管返回的函数是否被调用,for循环已经完成,i被赋予了最终的值3。

下面是解决这一问题的一些方法。一种解决方法就是用Python生成器。

python
def multipliers():
    for i in range(4): yield lambda x : i * x
print([m(2) for m in multipliers()])

另外一个解决方案就是创造一个闭包,利用默认函数立即绑定

python
def multipliers():
    return [lambda x, i=i: i * x for i in range(4)]

print([m(2) for m in multipliers()])

序列

要让一个自定义对象表现得像一个序列(如列表或元组),你需要实现 __len____getitem__ 方法。这两个方法是序列协议的一部分。

  • __len__(self):返回序列的长度。
  • __getitem__(self, index):返回给定索引的元素,可以是整数索引或切片。

下面是一个简单的示例:

python
class MySequence:
    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]
    
    def __iter__(self):
        # 返回一个迭代器对象
        return iter(self.data)

    
# 使用示例
seq = MySequence([1, 2, 3, 4, 5])
print(len(seq))        # 输出: 5
print(seq[2])          # 输出: 3
print(seq[1:4])        # 输出: [2, 3, 4]

通过实现这两个方法,你可以让自定义对象支持序列操作,比如索引访问、切片和 len() 函数。对对象的迭代需要调用__iter__,如果没有定义该方法,python会调用__getitem__(),让迭代和in运算符可用。

_slots_ 属性

__slots__属性用来限制实例对象的属性,实例对象的实例属性最多只能在__slots__属性值的范围内。

如果子类没有定义__slots__属性,则不会继承父类的__slots__属性,子类如果不定义自己的 __slots__,将会有 __dict__,从而失去父类的 __slots__ 优势。

如果子类定义了__slots__属性,则子类对象允许的实例属性包括子类的__slots__加上父类的__slots__。

在 Python 中,__dict__ 是一个字典属性,用于存储对象的可变属性。它是大多数 Python 对象的默认属性,允许动态添加和修改属性。

_dict_ 属性

  1. 动态属性:通过 __dict__,可以动态地添加、修改或删除对象的属性。

  2. 存储属性__dict__ 中的键是属性名,值是属性的值。

  3. 可访问性:可以直接访问和修改对象的属性值。

python
class MyClass:
    def __init__(self, value):
        self.value = value

obj = MyClass(10)
print(obj.__dict__)  # 输出: {'value': 10}

# 动态添加属性
obj.new_attr = 20
print(obj.__dict__)  # 输出: {'value': 10, 'new_attr': 20}

__slots__ 的关系

  • 如果类定义了 __slots__,那么该类的实例将没有 __dict__,这意味着不能动态添加未在 __slots__ 中定义的属性。
  • 使用 __slots__ 可以节省内存,因为不再需要为每个实例创建 __dict__

__dict__ 是 Python 提供的灵活机制,使得对象可以动态管理属性,但在需要优化性能和内存时,__slots__ 是一个有用的工具。

切片操作的内存策略

python
t1 = (1, 2, 3)
t2 = t1[:]
print(t1 is t2)

对于元组(tuple),切片操作会返回一个新的元组对象,但由于元组是不可变的,如果切片的范围是整个元组,Python 会优化地返回同一个对象。因此,t1 is t2 的结果是 True

python
lis1 = [1, 2, 3]
lis2 = lis1[:]
print(id(lis1) == id(lis2))

对于列表(list),切片操作会创建一个新的列表对象,即使切片的范围是整个列表。因此,id(lis1) == id(lis2) 的结果是 False

由数字,字符和下划线组成的短字符串以及[-5,256]内的整数存在内存驻留,将其赋值给多个不同的对象时,内存中只有一个副本,多个对象共享该副本。

如果对列表进行直接赋值,两个变量将引用同一个列表对象,因此它们的 id 是相同的。例如:

python
lis1 = [1, 2, 3]
lis2 = lis1
print(id(lis1) == id(lis2))  # 输出: True

在这种情况下,lis1lis2 是同一个对象的两个引用。任何对列表内容的修改通过任意一个引用都会影响到另一个。

要避免直接赋值导致的引用同一个列表对象,可以使用以下方法创建列表的副本:

  1. 使用切片

    python
    lis1 = [1, 2, 3]
    lis2 = lis1[:]
  2. 使用 list() 构造函数

    python
    lis1 = [1, 2, 3]
    lis2 = list(lis1)
  3. 使用 copy 模块中的 copy() 方法

    python
    import copy
    lis1 = [1, 2, 3]
    lis2 = copy.copy(lis1)
  4. 使用列表推导式

    python
    lis1 = [1, 2, 3]
    lis2 = [item for item in lis1]

这些方法都会创建一个新的列表对象,因此修改 lis2 不会影响 lis1

切片不会导致越界,但通过下标访问会越界。

正则表达式

python
import re

str1 = "Python's features"
str2 = re.match(r'(.*)on(.*?) .*', str1, re.M|re.I)

if str2:
    print(str2.group(1))
else:
    print("No match found")

.group(0)输出的是匹配正则表达式整体结果

.group(1) 列出第一个括号匹配部分,.group(2) 列出第二个括号匹配部分

re.M:多行匹配,影响 ^ 和 $

re.I:使匹配对大小写不敏感

对于你的正则表达式 r'(.*)on(.*?)r.*',它的含义是:

  • (.*):匹配任意字符(除换行符)0次或多次,尽可能多地匹配。
  • on:匹配字母 "on"。
  • (.*?):匹配任意字符(除换行符)0次或多次,尽可能少地匹配。
  • r:匹配字母 "r"。
  • .*:匹配任意字符(除换行符)0次或多次,尽可能多地匹配。

语法

正则表达式是一种强大的文本处理工具,下面是更详细的语法介绍:

字符匹配

  • 普通字符:直接匹配字符本身,如 a 匹配字母 "a"。
  • 特殊字符:需要转义以匹配本身,如 \. 匹配点号 "."

字符集

  • 简单字符集[abc] 匹配 "a"、"b" 或 "c"。
  • 范围字符集[a-z] 匹配从 "a" 到 "z" 的任何小写字母。
  • 组合字符集[a-zA-Z0-9] 匹配任何字母或数字。

否定字符集

  • [^abc] 匹配除 "a"、"b"、"c" 之外的任何字符。

预定义字符集

  • \d:匹配任何数字,等价于 [0-9]
  • \D:匹配任何非数字字符。
  • \w:匹配任何字母数字字符(包括下划线),等价于 [a-zA-Z0-9_]
  • \W:匹配任何非字母数字字符。
  • \s:匹配任何空白字符(空格、制表符等)。
  • \S:匹配任何非空白字符。

边界匹配

  • ^:匹配字符串的开头。
  • $:匹配字符串的结尾。
  • \b:匹配单词边界。
  • \B:匹配非单词边界。

重复限定符

  • *:匹配前一个字符零次或多次
  • +:匹配前一个字符一次或多次
  • ?:匹配前一个字符零次或一次。
  • {n}:匹配前一个字符恰好 n 次。
  • {n,}:匹配前一个字符至少 n 次。
  • {n,m}:匹配前一个字符至少 n 次,至多 m 次。

分组和捕获

  • (abc):捕获组,匹配 "abc" 并捕获。
  • (?:abc):非捕获组,匹配 "abc" 但不捕获。
  • (?P<name>abc):命名捕获组。

选择

  • a|b:匹配 "a" 或 "b"。

零宽断言

  • 正向前瞻(?=abc),匹配 "abc" 前的位置。
  • 负向前瞻(?!abc),匹配非 "abc" 前的位置。
  • 正向后顾(?<=abc),匹配 "abc" 后的位置。
  • 负向后顾(?<!abc),匹配非 "abc" 后的位置。

这些语法元素可以组合使用,以构建复杂的模式来匹配特定的文本结构。

特殊转义

使用 ]-^ 的方法

  1. -(连字符)

    • 如果要匹配连字符,可以将它放在字符集的开头或结尾,或者使用反斜杠转义,如 [-abc][abc\-]
  2. ](右方括号)

    • 如果要匹配右方括号,可以将它放在字符集的开头,或使用反斜杠转义,如 []abc][\]abc]
  3. ^(脱字符)

    • 如果要匹配脱字符,需将它放在字符集的非开头位置,或使用反斜杠转义,如 [a^bc][\^abc]
  • 匹配包含 - 的字符:[-a-z][a-z\-]
  • 匹配包含 ] 的字符:[]a-z][\]a-z]
  • 匹配包含 ^ 的字符:[a^b][\^ab]

re.match()

  • 功能: 仅在字符串的开头进行匹配。
  • 用法: re.match(pattern, string, flags=0)
  • 返回: 如果匹配成功,返回一个匹配对象;否则返回 None
  • 场景: 当你需要确认字符串是否以特定模式开始时使用。
python
import re

text = "Hello, world!"

# 尝试匹配开头的 "Hello"
match = re.match(r'Hello', text)
if match:
    print("re.match found:", match.group())  # 输出: Hello
else:
    print("re.match found nothing")

re.search()

  • 功能: 在整个字符串中搜索第一个匹配的子串。
  • 用法: re.search(pattern, string, flags=0)
  • 返回: 如果找到匹配项,返回一个匹配对象;否则返回 None
  • 场景: 当你需要在字符串的任意位置查找模式时使用。
python
import re

text = "Say hello to the world!"

# 在整个字符串中搜索 "hello"
search = re.search(r'hello', text)
if search:
    print("re.search found:", search.group())  # 输出: hello
else:
    print("re.search found nothing")
  • 匹配位置:

    • re.match() 只匹配字符串的开头。
    • re.search() 在整个字符串中查找第一个匹配项。
  • 使用场景:

    • 使用 re.match() 当你只关心字符串是否以特定模式开始。
    • 使用 re.search() 当你需要查找字符串中是否存在某个模式。

Find 函数

python
str.find(sub[, start[, end]])

参数

  • sub: 要查找的子字符串。
  • start: 可选参数,指定开始查找的位置。
  • end: 可选参数,指定结束查找的位置。

返回值

  • 返回子字符串最后一次出现的最左侧索引。
  • 如果未找到子字符串,则返回 -1
python
text = "Hello, world! Hello, Python!"

# 查找 "Hello" 最后一次出现的位置
index = text.rfind("Hello")
print(index)  # 输出: 14

# 指定查找范围
index_with_range = text.rfind("Hello", 0, 10)
print(index_with_range)  # 输出: 0

# 查找不存在的子字符串
not_found = text.rfind("Java")
print(not_found)  # 输出: -1
  • rfind()find() 的区别在于:rfind() 从右向左查找,但返回的仍是从左向右的索引位置
  • 如果只需要知道子字符串是否存在,可以使用 in 操作符。

查找字符串

  1. str.find()

    • 用于查找子字符串在字符串中的最低索引。如果未找到,则返回 -1
    python
    text = "Hello, world!"
    index = text.find("world")
    print(index)  # 输出:7
  2. str.index()

    • 类似于 find(),但如果未找到子字符串,则会抛出 ValueError
    python
    text = "Hello, world!"
    index = text.index("world")
    print(index)  # 输出:7
  3. str.rfind()str.rindex()

    • 分别用于从右侧开始查找子字符串。
  4. re.findall()

    用于查找字符串中所有与正则表达式匹配的非重叠部分。它返回一个列表,包含所有匹配的子字符串。

    python
    import re
    
    pattern = r'\d+'  # 匹配一个或多个数字
    text = "There are 3 cats, 4 dogs, and 5 birds."
    
    matches = re.findall(pattern, text)
    print(matches)  # 输出:['3', '4', '5']
    • 正则表达式模式:定义要匹配的模式。在上面的例子中,\d+ 匹配一个或多个数字。
    • 返回值:一个列表,包含所有匹配的字符串。如果没有匹配项,则返回空列表。
    1. 查找所有单词

      python
      text = "Find all words in this sentence."
      words = re.findall(r'\b\w+\b', text)
      print(words)  # 输出:['Find', 'all', 'words', 'in', 'this', 'sentence']
    2. 提取邮箱地址

      python
      text = "Contact us at info@example.com or support@example.org."
      emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text)
      print(emails)  # 输出:['info@example.com', 'support@example.org']

替换字符串

  1. str.replace()

    • 返回一个新的字符串,其中所有匹配的子字符串都被替换。
    python
    text = "Hello, world!"
    new_text = text.replace("world", "Python")
    print(new_text)  # 输出:Hello, Python!
  2. 使用正则表达式进行替换

    • 使用 re 模块,可以进行更复杂的模式替换。
    python
    import re
    
    text = "Hello, world!"
    new_text = re.sub(r"world", "Python", text)
    print(new_text)  # 输出:Hello, Python!

列表和引用机制

  1. 列表创建:

    • 使用列表推导式创建的子列表是独立的,互不影响。
    • 使用乘法运算符 * 创建的列表是对同一对象的多个引用。
  2. 引用和对象:

    • 在 Python 中,变量存储的是对象的引用,而不是对象本身。
    • 对于可变对象(如列表),多个引用可以指向同一个对象。
  3. 共享引用的影响:

    • 如果多个变量引用同一个可变对象,修改该对象会影响所有引用。
    • 这在使用 * 运算符扩展列表时尤为重要,因为它复制的是引用。
  4. 避免意外修改:

    • 可以使用列表推导式或 copy 模块的 deepcopy 方法来创建独立的副本,避免共享引用的问题。
  5. 示例:

    • 独立创建:
      python
      a = [['1', '2'] for _ in range(2)]
    • 共享引用创建:
      python
      b = [['1', '2']] * 2

理解这些概念有助于避免在处理列表和其他可变对象时出现意外行为。

枚举类型

python
lists = [1, 2, 3, 4]
tmp = 0
for i,j in enumerate(lists):
    tmp += i * j
print(tmp)

enumerate(lists) 会生成一个包含索引和值的元组序列:(0, 1), (1, 2), (2, 3), (3, 4)

装饰器

装饰器是 Python 中的一种高级功能,允许你在不修改函数代码的情况下,增强或改变函数的行为。装饰器本质上是一个函数,它接收一个函数作为输入,并返回一个新的函数。

以下是一个计时器装饰器的示例,用于记录一个函数的执行时间:

python
import time
from functools import wraps

def timing_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

# 使用装饰器
@timing_decorator
def example_function():
    time.sleep(2)  # 模拟一个耗时操作

example_function()
  • timing_decorator: 这是一个装饰器函数,接收一个函数 func 作为参数。
  • wrapper: 内部函数,负责计算执行时间并调用原始函数。
  • @wraps(func): 这个装饰器来自 functools 模块,用于保留被装饰函数的元数据(如函数名和文档字符串)。
  • start_timeend_time: 用于记录函数执行前后的时间。
  • example_function: 被装饰的函数,模拟一个耗时操作。

使用这个装饰器,可以轻松测量任何函数的执行时间。

装饰器本质上就是一个函数,它接受另一个函数作为参数,并返回一个新的函数或可调用对象。装饰器允许你在不改变原始函数代码的情况下,扩展或修改其行为。

生成器

生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用 yield 语句。每次 next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)

在 Python 中,yield 用于定义生成器函数,而生成器表达式是一种简化的语法,不需要显式使用 yield。生成器表达式直接返回一个生成器对象。

例如,使用生成器函数,你会这样写:

python
def my_generator():
    for i in range(10):
        yield i

使用生成器表达式时:

python
X = (i for i in range(10))
for value in X:
    print(value)

两者都实现了延迟计算,按需生成值,但生成器表达式更简洁,适合简单的生成器逻辑。

python
X = [i for i in range(10)]

在这个例子中,X 是一个列表,而不是生成器。列表推导式 [i for i in range(10)] 会立即计算并存储所有元素在内存中。

**当生成器被迭代完后,它就不能再返回任何值。**如果你尝试继续迭代或调用 next(),会触发 StopIteration 异常。这是生成器的正常行为。

如果你需要重新迭代生成器,你必须重新创建它。例如:

python
# 创建生成器
gen = (i for i in range(3))

# 第一次迭代
for value in gen:
    print(value)  # 输出 0, 1, 2

# 尝试再次迭代(不会输出任何内容)
for value in gen:
    print(value)

# 重新创建生成器以再次迭代
gen = (i for i in range(3))
for value in gen:
    print(value)  # 输出 0, 1, 2

生成器表达式

用于创建生成器对象。它类似于列表推导式,但生成器表达式不会立即计算和存储所有元素,而是按需生成元素。这在处理大量数据时非常有用,因为它节省内存。

python
gen = (expression for item in iterable if condition)
python
# 创建一个生成器表达式
gen = (i * i for i in range(10))

# 使用生成器
for value in gen:
    print(value)

要通过 for 循环生成一个元组,可以使用元组推导式(类似列表推导式,但直接用 tuple() 转换)。不过,元组推导式并不存在,但你可以通过生成器表达式结合 tuple() 来实现。

python
# 使用生成器表达式和 tuple() 生成元组
a = tuple(i * i for i in range(5))

print(a)  # 输出: (0, 1, 4, 9, 16)

这里,生成器表达式 (i * i for i in range(5)) 会生成平方数序列,然后 tuple() 将其转换为元组。

可变和不可变对象

传递可变对象列表

python
def a(b):
    b = [1, 2, 3]

c = [4, 5, 6]
a(c)
print(c)
[4, 5, 6]

解释

  • a(c) 被调用时,b 是一个指向 c 的局部变量。
  • 语句 b = [1, 2, 3] 创建了一个新的列表 [1, 2, 3],并让 b 指向这个新列表。
  • 这并没有改变 c,因为 b 的重新赋值仅在函数内部生效。
  • 因此,print(c) 输出 [4, 5, 6],原列表 c 保持不变。
python
def a(b):
    b[0] = 5

c = [4, 5, 6]
a(c)
print(c)
[5, 5, 6]

解释

  • a(c) 被调用时,b 指向与 c 相同的列表对象。

  • 语句 b[0] = 5 直接修改了列表的第一个元素。

  • 因为列表是可变对象,这种修改影响到了原列表 c

  • 因此,print(c) 输出 [5, 5, 6],反映了对列表内容的修改。

  • 重新赋值:在函数中使用 b = [1, 2, 3] 这种形式只是改变了局部变量 b 的指向,不会影响到传入的列表 c

  • 直接修改内容:使用 b[0] = 5b[:] = [1, 2, 3] 这样的操作修改了列表的实际内容,因此会影响到传入的列表 c

这体现了Python中可变对象(如列表)的特性:可以直接修改其内容,而不改变其引用。

传递不可变对象列表

这两个代码示例展示了Python中变量的引用和标识符(ID)的使用:

python
def a(b):
    b = 4
    print(id(b))

c = 5
a(c)
print(id(c))

解释

  • c 被赋值为 5,并传递给函数 a
  • 在函数 a 中,b 最初指向与 c 相同的整数对象 5
  • 语句 b = 4b 重新指向一个新的整数对象 4。此时,b 的 ID 发生了变化。
  • print(id(b)) 输出的是整数 4 的 ID。
  • print(id(c)) 输出的是整数 5 的 ID。c 的 ID 没有改变,因为 b 的重新赋值不影响 c
python
def a(b):
    print(id(b))

c = 5
a(c)
print(id(c))

解释

  • c 被赋值为 5,并传递给函数 a
  • 在函数 a 中,b 指向与 c 相同的整数对象 5
  • print(id(b)) 输出的是整数 5 的 ID。
  • print(id(c)) 也输出整数 5 的 ID,因为 bc 指向同一个对象。

整数对象的不可变性:整数是不可变对象。即使在函数内部重新赋值,外部变量的引用不会受到影响。并且只有修改时才会重新复制一份,导致id不同。

ID的输出id() 函数返回对象的唯一标识符。对于相同的不可变对象(如整数),ID 是相同的。重新赋值会改变局部变量的 ID,但不会影响外部变量。

is 和==的区别

is 判断的是a对象是否就是b对象,是通过id来判断的。

==判断的是a对象的值是否和b对象的值相等,是通过value来判断的。

系统编程

进程

python
from multiprocessing import Process, Queue

def worker(queue, data):
    queue.put(f"Processed {data}")

if __name__ == '__main__':
    q = Queue()
    processes = [Process(target=worker, args=(q, i)) for i in range(5)]

    for p in processes:
        p.start()

    for p in processes:
        p.join()

    while not q.empty():
        print(q.get())

进程池

python
# -*- coding: utf-8 -*-
from multiprocessing import Pool
import os
import time
import 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("%s 执行完毕,耗时 %0.2f 秒" % (msg, t_stop - t_start))

if __name__ == "__main__":
    po = Pool(3)  # 定义一个进程池,最大进程数 3
    for i in range(10):
        # Pool().apply_async(要调用的目标, (传递给目标的参数元组,))
        # 每次循环将会用空闲出来的子进程去调用目标
        po.apply_async(worker, (i,))
    
    print("----start----")
    po.close()  # 关闭进程池,关闭后 po 不再接收新的请求
    po.join()   # 等待 po 中所有子进程执行完成,必须放在 close 语句之后
    print("-----end-----")

multiprocessing.Pool 提供了一些常用的方法来管理和使用进程池。以下是这些方法的解析:

  1. apply(func, args=(), kwds={}):

    • 同步执行函数 func,并传递参数 argskwds
    • 阻塞调用,直到结果返回。
  2. apply_async(func, args=(), kwds={}, callback=None, error_callback=None):

    • 异步执行函数 func
    • callback 是可选的,执行成功后调用。
    • error_callback 是可选的,发生错误时调用。
  3. map(func, iterable, chunksize=None):

    • 类似于内置的 map 函数,但并行执行。
    • 同步阻塞,直到所有结果返回。
  4. map_async(func, iterable, chunksize=None, callback=None, error_callback=None):

    • 异步版本的 map
    • callbackerror_callback 用于处理结果或错误。
  5. imap(func, iterable, chunksize=1):

    • 类似于 map,但返回迭代器,结果按顺序返回。
    • 可以在结果生成时逐步处理。
  6. imap_unordered(func, iterable, chunksize=1):

    • 类似于 imap,但结果顺序不确定。
    • 更快,因为不需要等待按顺序返回。
  7. starmap(func, iterable, chunksize=None):

    • 类似于 map,但用于解包参数。
    • 每个元素应该是一个元组,解包后传给 func
  8. starmap_async(func, iterable, chunksize=None, callback=None, error_callback=None):

    • 异步版本的 starmap
  9. close():

    • 关闭进程池,防止更多任务提交。
    • 必须在 join() 之前调用。
  10. terminate():

    • 立即停止所有工作进程,不处理未完成任务。
  11. join():

    • 等待所有工作进程退出。
    • 必须在 close()terminate() 之后调用。

这些方法提供了多种并行执行任务的方式,适合不同的应用场景。

apply 和 apply_async 区别

下面是一个使用 applyapply_async 的示例代码,以及它们的对比和解释。

python
from multiprocessing import Pool
import time

def square(x):
    time.sleep(1)  # 模拟耗时操作
    return x * x

# 使用 apply
def use_apply():
    with Pool(4) as pool:
        results = []
        for i in range(5):
            result = pool.apply(square, args=(i,))
            results.append(result)
        print("Results with apply:", results)

# 使用 apply_async
def use_apply_async():
    with Pool(4) as pool:
        results = []
        async_results = []
        for i in range(5):
            async_result = pool.apply_async(square, args=(i,))
            async_results.append(async_result)
        
        # 获取结果
        results = [async_result.get() for async_result in async_results]
        print("Results with apply_async:", results)

if __name__ == "__main__":
    start_time = time.time()
    use_apply()
    print("Time with apply:", time.time() - start_time)

    start_time = time.time()
    use_apply_async()
    print("Time with apply_async:", time.time() - start_time)

apply

  • 行为:每次调用 apply 时,主进程会等待任务完成,然后继续下一个任务。
  • 结果:由于是阻塞的,即使有多个进程可用,任务也是一个接一个地执行。
  • 时间:总时间约为任务数乘以每个任务的执行时间。

apply_async

  • 行为:每次调用 apply_async 时,任务会被异步提交,主进程不等待任务完成,可以立即提交下一个任务。

  • 结果:多个任务可以并行执行,充分利用可用的进程。

  • 时间:总时间约为任务执行时间和进程池大小的比值。

  • apply 适合需要顺序执行并立即获取结果的场景。

  • apply_async 适合需要并行处理多个任务而不必立即获取结果的场景。

如果使用 Pool(3),即使设置了三个进程,使用 apply 仍然会导致每次只执行一个任务。原因是 apply 是同步阻塞的,主进程会等待每个任务完成后再提交下一个任务。因此,多个进程的优势无法体现。

with 语句: 确保在块结束时自动调用 pool.close()pool.join(),无需手动关闭。

注意

如果要使用Pool创建进程,就需要使用 multiprocessing.Manager() 中的 Queue(),而不是 multiprocessing.Queue(),否则会得到一条如下的错误信息:

RuntimeError:Queue objects should only be shared between processes through inheritance.

这是因为 multiprocessing.Pool 中的进程不能直接继承父进程的内存空间。

  1. 进程启动方式:

    • Pool 使用的进程启动方式(forkspawn)会导致子进程无法直接继承父进程中的对象。
    • multiprocessing.Queue() 依赖于父进程的内存空间来管理进程间通信。
  2. 对象共享限制:

    • multiprocessing.Queue() 使用管道和锁,这些不能在 Pool 的子进程中直接共享。
  3. Manager 的作用:

    • multiprocessing.Manager() 创建的 Queue 是通过一个独立的服务器进程管理的,能够在不同的进程间安全地共享。
    • 这种方式不依赖于进程的继承关系,因此适合 Pool

使用 Manager().Queue() 可以确保在 Pool 中的所有进程都能安全地访问和操作队列。

Python 全局解释器锁 GIL

简单来说,Python 全局解释器锁(Global Interpreter Lock) 是一个互斥锁(或锁),只允许一个线程保持 Python 解释器的控制权。注意,锁的是解释器。

为 python 解决了什么问题

引用计数是一种垃圾回收机制,用于记录Python对象被引用的次数。当一个对象的引用计数为0时,表示没有任何引用指向该对象,因此它会被垃圾回收机制自动释放。

在多线程环境中,如果没有GIL锁的保护,多个线程可能会同时对同一个Python对象进行操作,这可能会导致引用计数混乱,从而导致内存泄漏或误删。而GIL锁的存在可以保证在任何时刻只有一个线程在执行,这使得引用计数能够正确地记录每个对象的引用次数。

综上所述,GIL锁和引用计数是相互关联的,它们共同保证了Python多线程的安全性和内存管理的正确性。

那么多线程安全如何解决呢?

  1. 使用线程安全的对象和数据结构:Python中提供了一些线程安全的对象和数据结构,如Queue、Lock、Semaphore等。这些对象可以用于实现线程之间的通信和同步,确保多线程安全。
  2. 避免共享数据:尽可能减少多个线程同时访问共享数据的需要,可以将共享数据封装在对象中,并使用锁或其他同步机制来保护数据的访问。
  3. 使用锁机制:锁是一种同步机制,可以用于防止多个线程同时访问共享资源。Python中提供了Lock对象来实现锁的功能,确保同一时间只有一个线程可以执行特定的操作。
  4. 使用线程安全的设计模式:在设计程序时,可以采用一些线程安全的设计模式,如临界区、信号量、条件变量等,以确保多线程安全执行。
  5. 使用线程池:线程池可以预先创建一定数量的线程,并将任务分配给这些线程执行,这样可以避免多个线程同时创建和销毁的问题,提高程序的效率和稳定性。

那么在python里面,如何才能实现真正的并发

一种方法是使用多进程。Python的multiprocessing模块提供了创建和管理进程的功能,可以创建多个进程并在进程之间进行通信和同步。由于每个进程有自己的解释器和内存空间,因此它们可以真正地并发执行。

另一种方法是使用异步编程模型。Python的asyncio模块提供了异步I/O和协程的功能,可以编写基于事件循环的异步代码,实现真正的并发执行。异步编程模型可以与多线程和多进程结合使用,以充分利用多核CPU和并行执行任务。

threading

在 Python 中,threading 模块可以实现多线程编程,但由于全局解释器锁(GIL)的存在,线程并不能真正并行地执行 Python 字节码。

  • 线程创建:
    • 使用 threading.Thread 类创建线程,每个线程可以独立运行一个函数。
  1. 线程调度:

    • Python 解释器会在 I/O 操作或某些特定的字节码执行后释放 GIL,使其他线程有机会获得执行权。
    • 操作系统也会调度线程,可能在不同的时间片切换执行。
  2. 线程同步:

    • 提供了锁、条件变量、事件、信号量等同步原语,帮助开发者在多线程环境中安全地访问共享数据。
  3. 适用场景:

    • threading 适合 I/O 密集型任务,因为这些任务在等待 I/O 操作时会释放 GIL,让其他线程有机会执行。
python
import threading

def worker():
    print("Thread is running")

threads = []
for i in range(5):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()
  • CPU 密集型任务: 由于 GIL 的存在,多线程对 CPU 密集型任务的性能提升有限。
  • 解决方案:
    • 使用 multiprocessing 模块来创建多个进程,每个进程有独立的 Python 解释器和 GIL。
    • 使用 C 扩展模块或其他语言实现性能关键部分。

尽管 GIL 限制了多线程的并行能力,但对于 I/O 密集型任务,threading 仍然是一个有效的工具。

socket

TCP 通信

服务端 (Server)

python
import socket

# 创建 TCP/IP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定套接字到地址和端口
server_address = ('localhost', 65432)
server_socket.bind(server_address)

# 监听传入连接
server_socket.listen(1)

print('等待连接...')
connection, client_address = server_socket.accept()

try:
    print('连接来自', client_address)
    while True:
        data = connection.recv(1024)
        if data:
            print('收到:', data.decode())
            connection.sendall(data)
        else:
            break
finally:
    connection.close()

客户端 (Client)

python
import socket

# 创建 TCP/IP 套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
server_address = ('localhost', 65432)
client_socket.connect(server_address)

try:
    message = '这是一个测试消息'
    client_socket.sendall(message.encode())

    # 接收响应
    data = client_socket.recv(1024)
    print('收到:', data.decode())
finally:
    client_socket.close()

UDP 通信

服务端 (Server)

python
import socket

# 创建 UDP/IP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 绑定套接字到地址和端口
server_address = ('localhost', 65432)
server_socket.bind(server_address)

print('等待接收消息...')
while True:
    data, address = server_socket.recvfrom(1024)
    print('收到来自 {} 的消息: {}'.format(address, data.decode()))
    if data:
        sent = server_socket.sendto(data, address)

客户端 (Client)

python
import socket

# 创建 UDP/IP 套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

server_address = ('localhost', 65432)
message = '这是一个测试消息'

try:
    # 发送消息
    sent = client_socket.sendto(message.encode(), server_address)

    # 接收响应
    data, server = client_socket.recvfrom(1024)
    print('收到:', data.decode())
finally:
    client_socket.close()

这些示例展示了如何使用 Python 的 socket 模块来进行基本的 TCP 和 UDP 通信。

WSGI

过程

  1. 发送 HTTP 请求:浏览器发送 HTTP 请求到 Web 服务器,请求动态资源。

  2. Web 服务器调用 WSGI:Web 服务器接收到请求后,通过 WSGI 调用应用程序框架,传递请求环境(environ)和一个回调函数(start_response)。

  3. 框架调用 start_response:应用程序框架调用 start_response 方法,设置返回的状态码和头信息。

  4. 保存状态和头信息:调用 start_response 后,Web 服务器保存应用程序设置的状态和头信息。

  5. 生成动态内容:应用程序框架查询数据库或执行其他逻辑,生成动态页面的主体内容(body)。

  6. 返回响应体:应用程序框架将生成的响应体返回给 Web 服务器。

  7. 发送响应给浏览器:Web 服务器将完整的 HTTP 响应(包括状态、头信息和响应体)发送回浏览器,浏览器接收并呈现内容。

请求环境(environ)是一个包含请求信息的字典,传递给 WSGI 应用程序。它包含了许多关于请求和服务器的信息。以下是一些常见的键:

  1. REQUEST_METHOD:HTTP 方法(如 GET、POST)。
  2. PATH_INFO:请求的路径部分。
  3. QUERY_STRING:URL 中的查询字符串。
  4. SERVER_NAMESERVER_PORT:服务器的主机名和端口。
  5. HTTP_ 前缀的键:所有 HTTP 请求头信息。
  6. wsgi.input:一个文件对象,包含请求体(如 POST 数据)。
  7. wsgi.url_scheme:请求的协议(http 或 https)。

这些信息帮助应用程序理解请求的上下文,从而生成适当的响应。

Flask

Flask 使用正则表达式来定义 URL 路由

python
from flask import Flask
from werkzeug.routing import BaseConverter

app = Flask(__name__)

class RegexConverter(BaseConverter):
    def __init__(self, map, *args):
        super(RegexConverter, self).__init__(map)
        self.regex = args[0]

app.url_map.converters['regex'] = RegexConverter

@app.route('/user/<regex("[a-zA-Z0-9_]+"):username>')
def user_profile(username):
    return f"User: {username}"

if __name__ == '__main__':
    app.run()

在 Flask 中,自定义 URL 转换器通过设置 self.regex 来定义匹配 URL 部分的正则表达式。

self.regex 是一个字符串,表示正则表达式,用于匹配 URL 中特定部分的格式。这个正则表达式决定了 Flask 的路由系统如何解析和验证该部分的 URL。

例如:

python
class RegexConverter(BaseConverter):
    def __init__(self, map, *args):
        super().__init__(map)
        self.regex = args[0]

在这个例子中,self.regex = args[0] 将传入的正则表达式字符串赋值给 self.regex。这意味着当路由使用这个转换器时,它会根据 self.regex 来匹配 URL 的相应部分。

BaseConverter

BaseConverter 是 Flask 中用于创建自定义 URL 转换器的基类。它允许你定义自己的规则来解析和验证 URL 的特定部分。

  • 初始化: BaseConverter 接受一个 map 参数,表示 URL 映射。
  • regex 属性: 你可以通过设置 self.regex 来定义匹配规则。
  • to_python: 可以重写此方法,将 URL 部分转换为 Python 对象。
  • to_url: 可以重写此方法,将 Python 对象转换为 URL 格式。
python
from werkzeug.routing import BaseConverter

class RegexConverter(BaseConverter):
    def __init__(self, map, *args):
        super().__init__(map)
        self.regex = args[0]

# 使用示例
app.url_map.converters['regex'] = RegexConverter

@app.route('/user/<regex("[a-zA-Z0-9_]+"):username>')
def user_profile(username):
    return f"User: {username}"

在这个例子中,RegexConverter 继承自 BaseConverter,并通过 self.regex 设置了一个自定义的正则表达式来匹配 URL 部分。

布尔值

下列表达式的值为True的是( )

A. (5+4j > 2-3j)

复数不能直接比较大小,因此这个表达式无效。

B. (3 > 2 > 2)

这个表达式是 False,因为虽然 (3 > 2) 为 True,但 (2 > 2) 为 False。

C. ((3,2) < ('a','b'))

元组之间的比较是基于字典序的。数字和字符串不能直接比较,因此这个表达式会引发错误。 Python2能直接比较数字和字符串,而Python3不行。

D. 'abc' > 'xyz'

字符串比较是基于字典序的。在字典序中,'abc' 小于 'xyz',因此这个表达式是 False。

下列哪种说法是错误的(A )

A 除字典类型外,所有标准对象均可以用于布尔测试

B 空字符串的布尔值是False

C 空列表对象的布尔值是False

D 值为0的任何数字对象的布尔值是False

字典类型也可以用于布尔测试。实际上,所有标准对象都可以进行布尔测试,空的容器类型(如列表、字典、集合)和数值0的布尔值都是 False

NONE; 
False(布尔类型) 
所有的值为零的数 
0(整型) 
0.0(浮点型) 
0L(长整型) 
0.0+0.0j(复数) 
""(空字符串) 
[](空列表) 
()(空元组) 
{}(空字典)

pandas

执行以下程序,如果希望输出 df 对象中 x 列数据大于其平均值的所有行,下列选项中,做法正确的一项是:

python
import pandas as pd
df = pd.DataFrame({'x':[1, 2, 3, 4], 'y':[3, 4, 5, 6]})
a = df.x.mean()

A. print(df.query('x > a'))

B. print(df.query('x > @a'))

C. print(df.query('x > &a'))

D. print(df.query('x > $a'))

正确答案是 B。使用 @ 符号来引用外部变量 a。本题考查pandas。

df.query(expr)可以使用布尔表达式查询 DataFrame 的列,表达式是一个字符串,类似于SQL中的where从句,如果希望在表达式中引入变量,可以通过@符号来引入。本题中,查询字符串中的x即为df对象的x列,a为变量,需要借助@符号来引入,B选项符合题意。

Released under the MIT License.