编程笔记

编程笔记

python从入门到实战:高阶实用技巧
2025-01-29

1、闭包

在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数成为闭包。我们看下面这段代码:

account_amount = 0
def atm(num,deposit=True):
    global account_amount
    if deposit:
        account_amount += num
        print(f"存款:+{num},账户余额:{account_amount}")
    else:
        account_amount -= num
        print(f"取款:-{num},账户余额:{account_amount}")
atm(300)
atm(300)
atm(100,False)

通过全局变量account_amount来记录余额,尽管功能上是可以的,但仍有问题:

1、代码在命名空间上(变量定义)不够干净、整洁

2、全局变量又被修改的风险 

为了解决种种问题,就引入了闭包的概念。

def account_creat(initial_amount=0):
    def atm(num,deposit=True):
        nonlocal initial_amount
        if deposit:
            initial_amount += num
            print(f"存款:+{num},账户余额:{initial_amount}")
        else:
            initial_amount -= num
            print(f"取款:-{num},账户余额:{initial_amount}")
    return atm
fn = account_creat()
fn(300)
fn(200)
fn(50,False)

 nonlocal关键字可以用来修改外部函数的值。

闭包的优点:

1、实现了数据的私有性。外部无法直接访问闭包内部的变量,只能通过闭包提供的函数接口来操作,这就像是一个”黑盒“,保护了内部数据。

2、在函数执行完毕后,仍然保留其内部变量的状态。

闭包的缺点:

由于内部函数持续引用外部函数的值,所以会导致这一部分内存空间不被释放,一直占用内存。 

2、装饰器

装饰器其实也是一种闭包,其功能就是在不破坏目标函数原有的代码和功能的前提下,为目标函数增加新功能。

def sleep():
    import random
    import time
    print("睡眠中....")
    time.sleep(random.randint(1,5))

现在我们希望在不改变代码逻辑的基础上,增加功能:

1、在调用sleep前输出:我要睡觉了

2、在调用sleep后输出:我要起床了 

def outer(func):
    def inner():
        print("我要睡觉了")
        func()
        print("我要起床了")
    return inner
def sleep():
    import random
    import time
    print("睡眠中...")
    time.sleep(random.randint(1,5))
fn=outer(sleep)
fn()

 装饰器的快捷写法(语法糖):

def outer(func):
    def inner():
        print("我要睡觉了")
        func()
        print("我要起床了")
    return inner
@outer # 使用@outer定义在目标函数sleep上
def sleep():
    import random
    import time
    print("睡眠中...")
    time.sleep(random.randint(1, 5))
sleep()

3、设计模式

设计模式是一种编程套路,可以极大地方便程序的开发。最常见、最经典的设计模式,就是面向对象。除了面向对象外,在编程中也有很多既定的套路可以方便开发,我们称之为设计模式:

1、单例模式和工厂模式

2、建造者、责任链、状态、备忘录、解释器、访问者、观察者、中介、模板、代理模式等等 

3.1、单例模式

单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。在整系统中,某个类只能出现一个实例时,单例对象就能派上用场。

定义:保证一个类只有一个实例,并提供一个访问它的全局访问点

使用场景:当一个类只能有一个实例,而客户可以从一个众所周知的访问点访问它时

class test:
    pass
t1 = test()
t2 = test()
print(t1)
print(t2)
#运行结果<__main__.test object at 0x000001F7619882C0>
#       <__main__.test object at 0x000001F7610A7080>

创建类的实例后,就可以得到一个完整的、独立的类对象。通过print语句可以看出,他们的内存地址是不完全相同的,即t1和t2是完全独立的两个对象。

某些场景下,我们需要一个类无论获取多少次类对象,都仅仅提供一个具体的实例,用于节省创建类对象的开销和内存开销。比如某些工具类,仅需要一个实例,即可以在各处使用,这就是单例模式所要实现的效果。

单例的实现模式:

class StrTools:
    pass
str_tools = StrTools()

在test文件中定义如上代码;

from test import StrTools, str_tools

s1 = str_tools
s2 = str_tools
print(s1)
print(s2)
# <test.StrTools object at 0x00000241BD166C30>
# <test.StrTools object at 0x00000241BD166C30>

在另一个文件中导入对象,这样以来就节省了空间,节省了创建对象的开销。

3.2、工厂模式

当需求大量创建一个类的实例的时候,可以使用工厂模式。即,从原生的使用类的构造去创建对象的形式。迁移到,基于工厂提供的方法去创建对象的形式。

class Person:
    pass
class Worker(Person):
    pass
class Student(Person):
    pass
class Teacher(Person):
    pass
worker = Worker()
student = Student()
teacher = Teacher()

以上这种写法,不容易去维护,形式并不统一:

class Person:
    pass
class Student(Person):
    pass
class Teacher(Person):
    pass
class Worker(Person):
    pass
class Factory:
    def get_person(self,p_type):
        if p_type == 's':
            return Student()
        elif p_type == 't':
            return Teacher()
        else 
            return Worker()
factory = Factory()
worker = factory.get_person('t')
student = factory.get_person('s')
teacher = factory.get_person('t')

 使用工厂类的get_person()方法去创建具体的类对象有点:

1、大批创建对象的时候有统一的入口,易于代码维护

2、当发生修改,进修改工厂类的创建方法即可

3、符合现实世界的模式,即由工厂来制作产品(对象)

4、多线程

4.1、进程、线程和并行执行

现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持”多任务“的操作系统。

进程:就是一个程序,运行在系统之上,那么便称这个程序为一个运行进程,并分配进程ID方便系统管理。

线程:线程是归属于进程的,一个进程可以开启多个线程,执行不同的工作,是进程的实际工作最小单位。

进程就好比一家公司,是操作系统对程序进行运行管理的单位;线程就好比公司的员工,进程可以有多个线程(员工),是进程的实际工作者。

操作系统中可以运行多个进程,即多任务运行;一个进程可以运行多个线程,即多线程运行。

注意:

进程之间是内存隔离的,即不同的进程拥有各自的内存空间。这就类似与不同的公司拥有不同的办公场所。

线程之间是内存共享的,线程是属于进程的,一个进程内的多个线程之间是共享这个进程所拥有的内存空间的。这就好比,公司员工之间是共享公司的办公场所。

并行执行的意思指的是同一时间做不同工作。进程之间就是并行执行的,操作系统可以同时运行好多程序,这些程序都是在并行执行。除了进程外,线程其实也是可以并行执行的。即一个程序在同一个时间做两件乃至不同多的事情,我们称之为:多线程并行执行。 

4.2、多线程编程

绝大多数编程语言,都允许多线程编程,python也不例外。python的多线程可以通过threading模块来实现。

import threading

thread_obj = threading.Thread( [group [, target [, name [, args [, kwargs] ] ] ] )

- group:暂时无用,未来功能的预留参数

- target:执行的目标任务名

- args:以元组的方式给执行任务参数

- kwargs:以字典方式给执行任务传参

- name:线程名,一般不用设置

# 启动线程,让线程开始工作

thread_obj.start()

import threading
import time
def sing():
    while True:
        print("唱歌...")
        time.sleep(1)
def dance():
    while True:
        print("跳舞...")
        time.sleep(1)
"""多线程模式运行"""
sing_thread = threading.Thread(target=sing)
dance_thread = threading.Thread(target=dance)
sing_thread.start()
dance_thread.start()

 假如用正常方式去写,会发现仅仅只有sing函数运行,dance函数压根不会运行。

需要传参的话可以通过:

1、args参数通过元组(按参数顺序)的方式传参

2、使用kwargs参数u用字典的形式传参

import threading
import time
def sing(msg):
    while True:
        print(msg)
        time.sleep(1)
def dance(msg):
    while True:
        print(msg)
        time.sleep(1)
"""多线程模式运行"""
sing_thread = threading.Thread(target=sing,args=("唱歌...",))
dance_thread = threading.Thread(target=dance,kwargs={"msg":"跳舞..."})
sing_thread.start()
dance_thread.start()

5、网络编程

socket(简称 套接字)是进程之间通信的一个工具,好比现实生活中的插座,所有的家用电器要想工作都是基于插座进行,进程之间想要网络通信需要socket。scoket负责进程之间的网络数据传输,好比数据的搬运工。

客户端和服务端两个进程之间通过socket进行相互通讯,就必须由服务端和客户端。

socket服务端:等待其他进程的连接、可以接受发来的消息、可以回复消息

socket客户端:主动连接服务端、可以发送消息、可以接受回复消息

 

5.1、服务端开发

主要有以下几个步骤:

1、创建scoket对象

import socket

scoket_server = socket.socket( )

2、绑定socket_sercer到指定IP和地址

socket_server.bind(host,port)

3、服务端开始监听端口

 scoket_server.listen(back log)

# back log为int整数,表示允许的连接数量,超出的会等待,可以不填,不填会自动设置一个合理值

4、接收客户端连接,获得连接对象

coon,address = socket_server.accept( )

print(f"接收到客户端连接,连接来自:{adress}")

# accept方法是阻塞方法,如果没有连接,会卡在当前这一行,不会向下执行代码

# accept返回的是一个二元元组,可以使用上述形式,用两个变量接收二元元组的两个元素 

5、客户端连接后,通过recv方法,接收客户端发送的消息

while True:

        data = conn.recv(1024).decode("UTF-8")

        #recv方法的返回值是字节数组(Bytes),可以通过decode使用UTF-8解码为字符串

        #recv方法的传参buffsize,缓冲区大小,一般设置为1024即可

        if data=="exit":

                break

        print("接收到发送来的数据:",data)

# 可以通过while True无限循环来持续和客户端进行数据交互

# 可以通过判定客户端发来的特殊标记,如 exit,来退出无限循环 

6、通过coon(客户端当次连接对象),调用send方法可以回复消息

while True:

        data = conn.recv(1024).decode("UTF-8")

        if data=="exit":

                break

        print("接收到发送来的数据:",data)

        conn.send("你好",encode("UTF-8"))

7、conn(客户端当次连接对象)和socket_server对象调用close方法关闭连接

coon.close( )

socket_server.close( )

import socket
socket_server = socket.socket()
socket_server.bind(('localhost', 8888))
socket_server.listen(1)
# result: tuple = socket_server.accept()
# conn = result[0] #客户端和服务端连接对象
# adress = result[1] # 客户端的地址信息
conn, addr = socket_server.accept()
# accept()是阻塞的方法,等待客户端的连接,如果没有连接,否则不会向下执行
print(f"接收到客户端连接,客户端信息:{addr}")
while True:
    data:str = conn.recv(1024).decode("utf-8")
    print(f"客户端发来的消息为:{data}")
    msg = input("请输入要回复的消息:") # encode可以将字符串编码为字节数组对象
    if msg == "exit":
        break
    conn.send(msg.encode("utf-8"))
conn.close()
socket_server.close()

 这里并没有客户端来给我们验证,我们可以下载一个程序进行验证:网页

选择TCP Client开始连接即可发送信息验证! 

5.2、客户端开发

主要分为以下几个步骤:

1、创建scoket对象:

import scoket

scoket_client = socket.socket( )

2、连接到服务端

socket_client.connect(("localhost",8888)) 

3、发送信息

while True:

        send_msg = input("请输入要发送的信息")

        if send_msg ==exit:

                break

        socket_client.send(send_msg.encode("utf-8")) 

4、接收返回消息

while True:

        send_msg = input("请输入要发送的消息").encode("utf-8")

        socket_client.send(send_msg)

        recv_data = socket_client.recv(1024)

        #recv方法是阻塞式的,即不接收到返回,就卡在这里等待

        print("服务端的回复消息为:",recv_data.decode("utf-8")) 

5、关闭连接

coon.close( )

socket_client.close( )

import socket
socket_client = socket.socket()
socket_client.connect(('127.0.0.1', 8888))
while True:
    send_msg = input("请输入要发送的信息:")
    if send_msg == "exit":
        break
    socket_client.send(send_msg.encode("utf-8"))
    # 接受返回消息
    recv_msg = socket_client.recv(1024)
    print(f"服务端回复的消息:{recv_msg.decode("utf-8")}")
socket_client.close()

6、正则表达式

正则表达式,又称规则表达式(Regular Expression),是使用单个字符串来描述、匹配某个句法规则的字符串,常被用来检索,替换那些符合某个模式(规则)的文本。

简单来说,正则表达式就是使用:字符串定义规则,并通过规则去验证字符串是否匹配。比如,验证一个字符串是否符合条件的电子邮箱。

比如通过正则规则:(^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$)即可匹配一个字符是否是标准邮箱格式。若不使用正则,使用if else来对字符串做判断就非常困难了。

6.1、基础匹配

python正则表达式,使用re模块,并基于re模块中三个基础方法来做正则匹配。分别是:match、search、findall三个基础方法

1、re.match(匹配规则,被匹配字符串):从被匹配字符串开头进行匹配,匹配成功返回匹配对象(包含匹配信息),匹配不成功则返回空。

import re
s = "python java python"
result = re.match("python", s)
print(result) # <re.Match object; span=(0, 6), match='python'>
print(result.span()) # (0, 6)
print(result.group()) # python

s1 = "1python java python"
result1 = re.match("python", s1)
print(result1) # None
# 从第一个开始匹配,如果开头不匹配,后面不再看

 2、search(匹配规则,被匹配字符串):搜索整个字符串,找出匹配的。从前向后,找到第一个后,就停止,不会继续向后。

import re
s1= "1python666java"
result1 = re.search("python",s1)
print(result1) # <re.Match object; span=(1, 7), match='python'>
print(result1.span()) # (1,7)
print(result1.group()) # python

s2= "1java666"
result2 = re.search("python",s2)
print(result2) # None

3、findall(匹配规则,被匹配字符串):匹配整个字符串,找出全部匹配项

import re
s1 = "1python666python"
result1 = re.findall("python", s1)
print(result1) # ['python', 'python']

s2 = "1java666java"
result2 = re.findall("python", s2)
print(result2) # []

6.2、元字符匹配

在刚刚我们只是进行了基础的字符串匹配,正则最强大的功能在于元字符匹配规则。

单字符匹配:

字符功能
.匹配任意一个字符(除了\n),\.匹配点本身
[ ] 匹配[ ] 中列举的字符
\d匹配数字,即0-9
\D匹配非数字
\s匹配空白,及空格、tab键
\S匹配非空白
\w匹配单词字符,即a-z、A-Z、0-9、_
\W匹配非单词字符

s = "csdn @@python3 !!666 ##java"

# 找出全部数字:re.findall(r"\d",s) 

# 字符串的r标记,表示当前字符串是原始字符串,及内部的转移字符无效而是普通字符

# 找出特殊字符:re.findall(r"\W",s)

# 找出全部英文字母:re.findall(r"[a-zA-Z]",s)

# [ ]内可以写:[a-zA-Z0-9]这三种范围组合或指定单个字符如[aceDGF135]

import re
s = "csdn @@python3 !!666 ##java"
result1 = re.findall(r"\d",s)
print(result1) #['3', '6', '6', '6']
result2 = re.findall(r"\W",s)
print(result2) #[' ', '@', '@', ' ', '!', '!', ' ', '#', '#']
result3 = re.findall(r"[a-zA-Z]",s)
print(result3) #['c', 's', 'd', 'n', 'p', 'y', 't', 'h', 'o', 'n', 'j', 'a', 'v', 'a']

 数量匹配:

字符功能
*匹配前一个规则的字符出现0至无数次
+匹配前一个规则的字符出现1至无数次
匹配前一个规则的字符出现0或1次
{m}匹配前一个规则的字符出现m次
{m,}匹配前一个规则的字符出现最少m次
{m,n}匹配前一个规则的字符出现m到n次

边界匹配:

字符功能
^匹配字符串开头
$匹配字符串结尾
\b匹配一个单词的边界
\B匹配非单词边界

 分组匹配:

字符功能
|匹配做有任意一个表达式
()将括号中字符作为一个分组

案例:

1、匹配账号,只能由字母和数字组成,长度限制6到10位

规则为:^[0-9a-zA-Z]{6,10}$

2、匹配QQ号,要求纯数字,长度5-11位,第一位不为0

规则为:^[1-9][0-9{4,10}&

3、匹配邮箱地址,只允许qq、163、gmail这三种邮箱地址 

规则为:^[\w-]+(\.[\w-]+)*@(qq|163|gamil)(\.[\w-]+)+$

import re
# 1、匹配账号,只能由字母和数字组成,长度限制6到10位
r ="^[0-9a-zA-Z]{6,10}$"
s = "123457Ab"
result = re.findall(r,s)
print(result)
# 2、匹配QQ号,要求纯数字,长度5-11位,第一位不为0
r = "^[1-9][0-9]{4,10}$"
s = "123457Ab"
result = re.findall(r,s)
print(result)
# 3、匹配邮箱地址,只允许qq、163、gmail这三种邮箱地址 
r= r"(^[\w-]+(\.[\w-]+)*@(qq|163|gamil)(\.[\w-]+)+$)"
s = "a.b.c.d@163.com.a.d.e"
print(re.findall(r,s))

7、递归

递归在编程中是一种非常重要的思想,即方法(函数)自己调用自己的一种特殊编程写法。

比如我们熟悉的斐波那契数列。

def fib(n):
    if n<=2:
        return 1
    else:
        return fib(n-1)+fib(n-2)
print(fib(10))

最经典的递归场景就是为找出一个文件夹中全部的文件。

import os
def test_os():
    print(os.listdir('D:/test'))# 找出当前文件夹下有哪些文件
    # print(os.path.isdir('D:/test/a')) # 判断路径是否为文件夹
    # print(os.path.exists('D:/test')) # 判断某个路径是否存在
def get_files_recursion_from_dir(path):
    """
    从指定的文件夹使用递归的方式获取全部文件列表
    :param path: 被判断的文件夹
    :return: list,包含全部文件,如果目录不存在或无文件就返回一个空列表
    """
    file_list = []
    if os.path.exists(path):
        for f in os.listdir(path):
            new_path = path + "/" +f
            if os.path.isdir(new_path):
                file_list += get_files_recursion_from_dir(new_path)
            else:
                file_list.append(new_path)
    else:
        print(f"指定目录{path}不存在")
        return []
    return file_list
if __name__ == '__main__':
    print(get_files_recursion_from_dir('D:/test'))

1、注意递归的终止条件,如果没有终止条件,就会变成死递归及无限递归 

2、注意返回值的传递,确保从最内层,层层传递到最外层