若想技术精进,当然得把基础知识打得牢牢的。
廖雪峰的官方网站 python3教程,该网站提供的教程浅显易懂,还附带了讲学视频,非常适合初学者正规入门。
以下是通过廖雪峰python官方网站学习的个人查漏补缺。
主要内容包括:1.IO编程 【 1)输入/输出流 2)异步编程VS同步编程 3)文件读写 4)StringIO和BytesIO 5)操作文件和目录 6)序列化pickle和json 】 2.进程和线程 【 1)多进程multiprocessing 2)多线程 3)ThreadLocal线程局部变量 4)进程VS线程 5)分布式进程 】 3.正则表达式 【 此处思路清晰、简单易懂 (re模块、切分、分组、编译) 】
1.IO编程
1)输入/ 输出流
IO( Input/Output ),也就是输入和输出。IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。
2)异步编程 VS 同步编程
由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。同步和异步的区别就在于是否等待IO执行的结果。
好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。
你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO。
3)文件读写
读文件:使用Python内置的open()函数,传入文件名和标示符:
>>> f = open('/Users/michael/test.txt', 'r')
>>> f = open('路径及文件名', '文件操作标示符‘)
Python引入了with语句来自动帮我们调用close()方法:
注:::调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
小技巧:如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便。
读二进制文件:要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:
>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
字符编码:需要给open()函数传入encoding参数,遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数( 遇到字符编码错误,可忽略 ),表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')
写文件:同样使用Python内置的open()函数,传入文件名和标示符,传入标识符'w'或者'wb'表示写文本文件或写二进制文件,传入'a'以追加(append)模式写入。
在Python中,文件读写是通过open()函数打开的文件对象完成的。使用with语句操作文件IO是个好习惯。
4)StringIO和BytesIO
StringIO:数据读写不一定是文件,也可以在内存中读写。StringIO顾名思义就是在内存中读写str。
BytesIO:StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
5)操作文件和目录
对于文件和目录操作,Python内置的os模块可以解决。Python内置的os模块可以直接调用操作系统的接口函数。
如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。
环境变量:在操作系统中定义的环境变量,全部保存在os.environ这个变量中,可以直接查看。要获取某个环境变量的值,可以调用os.environ.get('key')。
>>> os.environ
>>> os.environ.get('PATH')
操作文件和目录:操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块中,这一点要注意一下。查看、创建和删除目录可以这么调用:
>>> os.path.abspath('.') # 查看当前目录的绝对路径
>>> os.path.join('/Users/michael', 'testdir') # 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来
>>> 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.path.join()函数,这样可以正确处理不同操作系统的路径分隔符。shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
利用Python的特性来过滤文件:
6)序列化
--只能用Pickle保存那些不重要的数据
系列化:我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。Python提供了pickle模块来实现序列化。
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。
为什么需要序列化,也就是JSON存在的意义。
JSON:如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式。更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。
JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON:
由于JSON标准规定JSON编码是UTF-8,所以我们总是能正确地在Python的str与JSON的字符串之间转换。
JSON进阶
JSON序列化一个class类对象:
不过,下次如果遇到一个Teacher类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class的实例变为dict:
print(json.dumps(s, default=lambda obj: obj.__dict__))
JSON反序列化一个对象实例:
同样得自定义一个额外的函数,将dict转换为Student类。
Python语言特定的序列化模块是pickle,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json模块。
json模块的dumps()和loads()函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。
2.进程和线程
多任务:一边听MP3,一边用Word赶作业,一边用浏览器上网。真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。对于操作系统来说,一个任务就是一个进程(Process)。有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。真正地同时执行多线程需要多核CPU才可能实现。
总结一下就是,多任务的实现有3种方式:
多进程模式;
多线程模式;
多进程+多线程模式。
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。
1)多进程multiprocessing
了解操作系统的相关知识:Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
import os
print('Process (%s) start...' % os.getpid())# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
由于Windows没有fork调用,上面的代码在Windows上无法运行。有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
multiprocessing
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,multiprocessing模块就是跨平台版本的多进程模块。
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
Pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
Pool(4)表示最多能同时执行4个进程,这是Pool有意设计的限制,并不是操作系统的限制。Pool的默认大小是CPU的核数。
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
在Unix/Linux下,可以使用fork()调用实现多进程。要实现跨平台的多进程,可以使用multiprocessing模块。进程间通信是通过Queue、Pipes等实现的。
2)多线程
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
'balance=balance+n' 高级语言的一条语句在CPU执行时是若干条语句。两个线程同时一存一取,就可能导致余额不对。如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
多核CPU
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
3)ThreadLocal 线程的局部变量
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。为解决以上问题,ThreadLocal应运而生。
你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
4)进程 VS 线程
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换:多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。(自己在liunx服务器上运行程序尝试过)
计算密集型 vs. IO密集型:计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。对于计算密集型任务,最好用C语言编写。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO:考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
5)分布式进程 --用了Queue队列举例
在Thread(线程)和Process(进程)中,应当优选Process(进程),因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
3.正则表达式
正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。
首先了解如何用字符来描述字符。
1) \d表示可以匹配一个数字 eg: '00\d' 可匹配 '009'
2) \w表示可以匹配一个字母或数字 eg: '\w\w\d' 可匹配 'py3'
3) \s表示可以匹配一个空格(也包括Tab等空白符) eg: '\s' 可匹配 ' '
4) .表示可以匹配任意字符 eg: 'py.' 可匹配 'pyc','pyo','py7','py*'
要匹配变长字符
1) *表示任意个字符(包括0个)
2) +表示至少一个字符
3) ?表示0个或1个字符
4) {n}表示n个字符
5) {n,m}表示n-m个字符
eg: \d{3}\s+\d{3,8} 解释:匹配3个数字+空格+3~8个数字
要做更精确匹配,使用[]表示范围,使用{}表示现在字符长度
1)^表示行的开头 eg: ^\d表示必须以数字开头
2)$表示行的结束 eg:\d$表示必须以数字结束
3)A|B表示可以匹配A或B eg:(P|p)ython可以匹配'Python'或者'python'
4)[0-9a-zA-Z\_] 表示可以匹配一个数字、字母或者下划线
5)[0-9a-zA-Z\_]+ 可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','Py3000'等等
6)[a-zA-Z\_][0-9a-zA-Z\_]* 可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量
7)[a-zA-Z\_][0-9a-zA-Z\_]{0, 19} 更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)
正则表达式模块:re模块
强烈建议使用Python的r前缀,就不用考虑转义的问题了:
s = r'ABC\-001'
切分字符串---使用re正则表达式,强悍、灵活
分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。eg:^(\d{3})-(\d{3,8})$
贪婪匹配:正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。
编译
当我们在Python中使用正则表达式时,re模块内部会干两件事情:
1)编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
2)用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:
编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。