Python自学笔记D9——IO编程、进程、线程和正则表达式

文章目录

  • IO编程
    • 文件读写
    • StringIO和BytesIO
    • 操作文件和目录
    • 序列化
  • 进程和线程
    • 多进程
    • 多线程
    • ThreadLocal
    • 多进程和多线程对比
    • 分布式进程
  • 正则表达式

IO编程

由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

比如你打开浏览器,访问网站,先output请求html信息,接下来Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。

IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。

而由于CPU和内存速度远高于外设,因此出现了两种不同的IO方式:

同步IO:CPU等到外设读取完后再继续下一步。
异步IO:CPU先执行后续程序去了(效率高,编程模型复杂人,如:回调、轮询)

文件读写

用法基本和c兼容

>>> f = open('/Users/michael/test.txt', 'r')#读文件
>>> f.read()
>>> f.close()
#两种方法避免文件不存在
try:
    f = open('/path/to/file', 'r')
    print(f.read())
finally:
    if f:
        f.close()
#第二种
with open('/path/to/file', 'r') as f:
    print(f.read())

调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。

另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。(读取配置文件较为方便)因此,要根据需要决定怎么调用。

for line in f.readlines():
    print(line.strip()) # 把末尾的'\n'删掉

file-like Object

像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。

StringIO就是在内存中创建的file-like Object,常用作临时缓冲。

二进制文件和不同字符编码文件读取

下为读取二进制和gbk编码文件,遇到乱码时忽略。

>>> f = open('/Users/michael/gbk.txt', 'rb')
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk',errors='ignore')

写文件

同读文件,r改成w即可,如果同名想要添加至末尾则可以传入一个’a’。

StringIO和BytesIO

StringIO:在内存中读写str,和文件一样。

 from io import StringIO
>>> f = StringIO('Hello!\nHi!\nGoodbye!')
>>> while True:
...     s = f.readline()
...     if s == '':
...         break
...     print(s.strip())
...
Hello!
Hi!
Goodbye!

BytesIO

顾名思义,读取bytes的IO

>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

注意!有个指针指向问题

如果使用stringIO进行初始化,指针永远会回到开头的位置
如果使用write,则指针会指向write写入的字符串最后!

>>>a = StringIO('abcdefg') 
>>>>a.getvalue() 'abcdefg' 
>>>>a.write('12')
>>>>a.getvalue() '12cdefg'
>>>>a.write('12')
>>>>a.getvalue() '1212efg'

操作文件和目录

Python内置的os模块也可以直接调用操作系统提供的接口函数。
jion,split合并拆分

# 查看当前目录的绝对路径:
>>> os.path.abspath('.')
'/Users/michael'
# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'#一定要先join合在一起,不要手打!
# 然后创建一个目录:
>>> os.mkdir('/Users/michael/testdir')
# 删掉一个目录:
>>> os.rmdir('/Users/michael/testdir')
>>> os.path.split('/Users/michael/testdir/file.txt')#拆分
('/Users/michael/testdir', 'file.txt')
>>> os.path.splitext('/path/to/file.txt')#获取拓展名
('/path/to/file', '.txt')
# 对文件重命名:
>>> os.rename('test.txt', 'test.py')
# 删掉文件:
>>> os.remove('test.py')
>>> [x for x in os.listdir('.') if os.path.isdir(x)]#输出所有目录
['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]

shutil模块中也补充了一些os没有的功能,比如copyfile()等

作业:编写一个能在目录及子子子目录中找到文件名带有某字符串的文件的程序,打印相对路径。

import os
from os import path
def str_in_filename(folder, keystr):
    all_files = os.walk(folder)        # 用walk方法遍历目标路径
    files_list = []
    folder_list = []
    for all_folder, all_folder_name, all_filename in all_files:
        for file_name in all_filename:              
            if keystr.lower() in file_name.lower():         
                files_list.append(path.join(all_folder, file_name)) 
    print(files_list)
if __name__ == "__main__":
    str_in_filename(r'.', 'xls')
    #递归方式实现:方法2
def str_in_filename_re(folder, keystr):
    files_list = os.listdir(folder)
    for filename in files_list:
        full_filename = os.path.join(folder, filename)
        if os.path.isdir(full_filename):
            try:                # 有些目录没有权限进入,会出错,这里要try一下
                str_in_filename_re(full_filename, keystr)       
            except BaseException as e:
                print(e)
        elif keystr.lower() in filename.lower():
            target_list.append(full_filename)
if __name__ == "__main__":
    # str_in_filename(r'.', 'xls')
    target_list = []
    str_in_filename_re(r'D:\Downloads', 'xls')
    print(target_list)

序列化

更改变量时,只存在于内存中,如果不进行存储,就相当于没有进行改变。

我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
Python提供了pickle模块来实现序列化。
过于鸡肋!只能在py中使用,不进行展开!

JSON!
Python自学笔记D9——IO编程、进程、线程和正则表达式_第1张图片

最基础的转换:dumps和loads

>>> import json
>>> d = dict(name='Bob', age=20, score=88)
>>> json_str = json.dumps(d)
'{"age": 20, "score": 88, "name": "Bob"}'
>>> json.loads(json_str)
{'age': 20, 'score': 88, 'name': 'Bob'}

进阶:class与JSON的转换

可以使用default,把他变成一个可以序列为JSON的对象

#前提是类没有定义__slot__
print(json.dumps(s, default=lambda obj: obj.__dict__))
#无前提,但是每一个不同的类都要重写一次,转换成dic再存
def student2dict(std):
    return {
        'name': std.name,
        'age': std.age,
        'score': std.score
    }
#
obj = dict(name='小明', age=20)
s = json.dumps(obj, ensure_ascii=False)#打印中文,如果True则unicode

进程和线程

单核多任务是假多任务:就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。

多核可以做到多任务,分为以下三种,一般如果任务过多也采用轮询制度:
多进程模式;
多线程模式;
多进程+多线程模式。

一个任务一般是一个进程
一个进程至少有一个线程,可以有多个线程(任务)

多进程

Unix/Linux操作系统提供了一个fork()系统调用(window无!!)

调用一次,会创建父子进程返回两次,子进程永远返回0,而父进程返回子进程的ID,因此子进程通过getppid()可以轻松拿到父进程ID,getpid拿自己的。

multiprocessing
window中只能使用这个来进行多进程的编写

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))
#以上为子进程拿自己的ID
if __name__=='__main__':
    print('Parent process %s.' % os.getpid())#父进程拿自己的ID
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

当启动多个线程时,使用线程池来控制线程的多少!

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()#线程池必须得close
    p.join()
    print('All subprocesses done.')
    #结果如下
Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.
#其中主进程创建进程池,启动子进程,关闭进程池,等待子进程完毕,打印。

子进程

可以使用子进程来在python编译中模拟命令行的方式。

import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

进程间通信

import multiprocessing
import time
def foo(aa):
    time.sleep(0.1)
    ss = aa.get()  # 管子的另一端放在子进程这里,子进程接收到了数据
    print('子进程已收到数据...')
    print(ss)  # 子进程打印出了数据内容...

if __name__ == '__main__':  # 要加这行...

    tx = multiprocessing.Queue()  # 创建进程通信的Queue,你可以理解为我拿了个管子来...
    jc = multiprocessing.Process(target=foo, args=(tx,))  # 创建子进程
    jc.start()  # 启子子进程

    print('主进程准备发送数据...')
    tx.put('有内鬼,终止交易!')  # 将管子的一端放在主进程这里,主进程往管子里丢入数据↑
    jc.join()

多线程

Python的多线程是Posix Thread,不是模拟出来的线程。
一般使用import threading即可

import time, threading

# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

#结果
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

LOCK

多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,

而多线程中,所有变量都由所有线程共享,任何一个变量都可以被任何一个线程修改,多个线程同时改一个变量内容就很完全混乱!

import time, threading

# 假定这是你的银行存款:
balance = 0
lock = threading.Lock()
def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - n
def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
#必须加上run_thread中的锁!不然答案就会变成40 -11 30等

本质上是因为高级语言运算,一条balance+=n会分成两条进行运算,因此每次都要进行4条运算,两次一共进行八条,这八条语句运行的顺序是随机的,因此会导致数值混乱。

参考着改了个画图的小例子,如果加入join则主线程会等子线程完全完成后再继续。

import threading, time, turtle
def draw():
    print("Thread '%s' is running..." % threading.current_thread().name)
    t = turtle.Pen()
    turtle.bgcolor('black')
    colors = ['red', 'yellow', 'green', 'blue']
    for x in range(50):
        t.pencolor(colors[x % 4])
        t.forward(x*2)
        t.right(91)
    print("Thread '%s' is ending..." % threading.current_thread().name)
if __name__ == "__main__":
    print("Thread '%s' is running..." % threading.current_thread().name)
    t = threading.Thread(target=draw, name='DrawThread')
    t.start()
    # t.join()    # 如果加了join,那么主线程就必须一直等待子线程画完之后才能继续,否则就是看上去同时进行。
    n = 0
    while n < 20:
        n += 1
        time.sleep(0.5)
        print('Thread %s ==> %d' % (threading.current_thread().name, n))
    print("Thread '%s' is ending..." % threading.current_thread().name)

Python无法使用多线程利用多核

任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

因此Python只能使用多进程利用多核,以及使用C扩展多线程多核!

ThreadLocal

使用ThreadLocal可以轻松地调用每一个线程中的独立变量,而不需要一个个写。

每一个线程都可以调用ThreadLocal的变量,ThreadLocal会处理并创建一个个对应不同线程的不同变量,因此不会造成冲突,比如下文中的local_school。

import threading
    
# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

#结果
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

多进程和多线程对比

多进程:
优点:稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
缺点:创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

多线程:
优点:在Windows下,多线程的效率比多进程要高。
缺点:任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

多任务有很多好处,但是也有坏处。由于有一些切换的开销(保存现场、准备新环境等),因此线程过多也会带来坏处

计算密集型和IO密集型

前者主要是要通过大量的计算来消耗CPU的资源,全靠运算。
后者主要涉及网络、磁盘等,时间花在等IO传输上,一般任务越多效率越高。

异步IO

异步之前解释过了,这里主要是使用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型

分布式进程

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。

以下为一个简单的分布式例子,分别在两个cmd打开,先master后worker

# task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()

# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
    pass
def gettask():
    return task_queue
def getresult():
    return result_queue
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
def do_task_master():
    QueueManager.register('get_task_queue', callable=gettask)
    QueueManager.register('get_result_queue', callable=getresult)
# 绑定端口5000, 设置验证码'abc':
    manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')
# 启动Queue:
    manager.start()
# 获得通过网络访问的Queue对象:
    task = manager.get_task_queue()
    result = manager.get_result_queue()
# 放几个任务进去:
    for i in range(10):
        n = random.randint(0, 10000)
        print('Put task %d...' % n)
        task.put(n)
# 从result队列读取结果:
    print('Try get results...')
    for i in range(10):
        r = result.get(timeout=10)
        print('Result: %s' % r)
# 关闭:
    manager.shutdown()
    print('master exit.')
if __name__ == '__main__':
    do_task_master()
#
# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 创建类似的QueueManager:
class QueueManager(BaseManager):
    pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
    try:
        n = task.get(timeout=1)
        print('run task %d * %d...' % (n, n))
        r = '%d * %d = %d' % (n, n, n*n)
        time.sleep(1)
        result.put(r)
    except Queue.Empty:
        print('task queue is empty.')
# 处理结束:
print('worker exit.')

结果计算n^2,如果将其改成发送邮件则可以做到邮件异步发送

正则表达式

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

\d 可以匹配一个数字
\w 可以匹配一个字母或数字
.可以匹配任意字符
*表示任意个字符(包括0个)
+表示至少一个字符
?表示0个或1个字符
{n}表示n个字符
{n,m}表示n-m个字符:
\s可匹配一个空格或者tab等
\符号可以转义,输出该符号

进阶
[a-zA-Z_][0-9a-zA-Z_]*匹配python合法对象
[a-zA-Z_][0-9a-zA-Z_]{0, 19}最多1+19个字母
A|B可以匹配A或B
^表示行的开头, ^\d表示必须以数字开头
$表示行的结束, $ \d表示必须以数字结束。

re模块
建议使用r来避免转义符号的出现

>>> import re
>>> re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> re.match(r'^\d{3}\-\d{3,8}$', '010 12345')
>>>

切分字符串

>>> re.split(r'\s+', 'a b   c')
['a', 'b', 'c']
>>> re.split(r'[\s\,]+', 'a,b, c  d')
['a', 'b', 'c', 'd']

分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)

>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'

贪婪匹配

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符,加一个?可以非贪婪匹配。

>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')#贪婪则'102300',''

编译
当我们在Python中使用正则表达式时,re模块内部会干两件事情:

1、编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
2、用编译后的正则表达式去匹配字符串

因此比较复杂时可以进行预编译

>>> import re
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')

作业:一、验证email
二、验证email并提取名字

import re 
def is_valid_email(addr):
    return bool(re.match(r'^(\w+)(\.\w+)*@(\w+)(\.\w+)*$', addr))
#测试
# 测试:
assert is_valid_email('[email protected]')
assert is_valid_email('[email protected]')
assert not is_valid_email('bob#example.com')
assert not is_valid_email('[email protected]')
print('ok')
#二
import re
def name_of_email(addr):
    ret=re.match(r'?\s*\w*@\w+\.\w+',addr)
    return ret.group(1)
# 测试:
assert name_of_email(' [email protected]') == 'Tom Paris'
assert name_of_email('[email protected]') == 'tom'
print('ok')

你可能感兴趣的:(python自学笔记)