python基本常识

高级特性

切片操作

比如有一个list表格:

L = ['sense', 'fundation', 'arrest']

常规的方法一般就是按下标取:

数量一少还能行,数量一多就不行了,用上切片操作就简单多了:

取0到1个下标的内容出来。如果是从头开始是可以不写0,直接:接结束即可

如果是最后一个,也是可以忽略不写

创建一个有序的列表:

可以用切片很轻松的取出一段数字,比如取出前十个数字,或者是后十个数字:

前十个数每隔2个数字取一个:

每隔5个数字取一个:

tuple,str都可以看做是一种list,都可以进行切片操作。 利用切片操作,去掉一个字符串的前后空格。要注意是是前后空格是不止一个的,可能有很多个。

由于前后的空格都不止一个,所以需要迭代进行,还要注意下标的空格。

迭代

python里面的迭代,也就是for循环有点厉害,封装的比较好,不仅仅可以迭代list,tuple,还可以迭代迭代对象,封装的太完美了,有时候自己实现的一些迭代器是很难用上的。

像list,array这种有下标的数组,是可以直接迭代的,但是如果像dict,set这种没有下标的也可以迭代:

这个字典看起来是有序的输出,但是事实上是无序的输出,重复的迭代几次就会出现一些乱序的了。如果不加任何的修饰,默认就是迭代key。如果想要迭代value,那就要在后面加上values():

如果想要一起迭代,那么可以用items修饰:

这种是没有下标的迭代,如果我们要把这些迭代对象弄成下标一样的进行迭代:

使用迭代找到最大值和最小值:

列表生成式

生成一个

的列表,可以直接用range生成:

但如果是要生成

这些就有点麻烦了,列表生成式就方便在这里:

后面跟

还可以帅选,比如要选出偶数的平方:

也可以嵌套双重循环形成全排列:

运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

如果list中既包含字符串,又包含整数,由于非字符串类型没有lower()方法,所以列表生成式会报错:

生成器

生成器和列表生成式的区别就是,列表生成式是

,而生成器是

可以使用next输出下一个数字,但是这样太多了,如果要输出完那也太多了。使用可以用for循环做输出,生成器也是一个迭代对象。

如果前面用了next输出,那么再次使用for循环迭代

只会接着下一个next继续迭代。generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

可以看出其实就是前两个数相加得到后面的一个,这其实可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。只需要把print改成yield即可。

当使用next执行到yield的时候会暂停,等到下次再调用就会从暂停的地方开始。如果是用next可能出现StopIterator的情况,就是迭代到最后没有数字了,这个时候就要用异常来捕捉:

杨辉三角定义如下:

          1
         / \
        1   1
       / \ / \
      1   2   1
     / \ / \ / \
    1   3   3   1
   / \ / \ / \ / \
  1   4   6   4   1
 / \ / \ / \ / \ / \
1   5   10  10  5   1

把每一行看做一个list,试写一个generator,不断输出下一行的list:

迭代器

可以使用for循环进行迭代的: 一类是集合数据类型,如list、tuple、dict、set、str等; 一类是generator,包括生成器和带yield的generator function。 这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。 是不是一个可迭代对象可以用

函数来判断。可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。 可以使用isinstance()判断一个对象是否是Iterator对象。一个是iterable,一个是iterator。 list,tuple,dict这些是iterable,但不是iterator,这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。 但是iterable是可以通过iter()变成iterator对象的,而python里面的for循环实质上就是一个iterator一直next的过程。

函数式编程

map/reduce

比如有一个iterable, 想要把这个iterable里面的所有元素都乘上两倍,当然可以使用for循环一个一个处理,但是用map就可以一起做:

会把iterable的元素一个一个的取出来作用在f函数上面。map之后返回的是一个iterator,是一个惰性的数组,所以是需要用list来转变一下。再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

上面的一个例子看起来是毫无作用,对于字符串转数字的的问题其实是很适用的:

利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']:

Python提供的sum()函数可以接受一个list并求和,请编写一个prod()函数,可以接受一个list并利用reduce()求积:

利用map和reduce编写一个str2float函数,把字符串'123.456'转换成浮点数123.456:

filter

filter是一个过滤器,它的主要用法和map差不多的,只不过它要求的函数是返回True or False,返回True的留下,False的离开。

计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单: 首先,列出从2开始的所有自然数,构造一个序列: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉: 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取新序列的第一个数5,然后用5把序列的5的倍数筛掉: 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 不断筛下去,就可以得到所有的素数。

not_divisible这个函数返回的就是一个函数,lambda的匿名函数,使用filter过滤即可。

回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()筛选出回数:

sorted

看名字就知道是排序算法了。

sorted是一个高阶函数,可以接收一个key来自定义排序的方式:

也可以用于对字符串的排序

由于

的ASCII码是大于

的,所以自然就是在后面,如果想不区分排序,可以全部转成小写再排。

假设我们用一组tuple表示学生名字和成绩: L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)] 请用sorted()对上述列表分别按名字和成绩排序:

返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。 比如想计算一些数字的和,但是并不想立刻就知道,这个时候就可以返回一个函数了:

等到调用了f函数的时候才会进行计算,调用lazy_sum的时候会保留一些结果。 事实上如果是在传入一个一模一样的参数得到结果也还是一样的。但是这得到的这两个返回函数是两个不一样的返回函数:

两个函数指向的地址不是一样的。 这种返回函数的方法叫做闭包:

这个时候返回的其实是三个9,因为他们全部都是引用了i,而这个函数返回是等到循环执行完才会返回的,所以到循环执行完这个i就是3了,所以就是三个9。所以这如果要使用闭包,返回函数不要引用任何循环变量,或者后续会发生变化的变量。 如果要正常的输出

,需要改成这样:

匿名函数

其实就是lambda表达式,如果是求一个列表的平方,那么要写一个函数,然后map即可。如果是lambda匿名函数就会间断点:

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,装饰器就是一个返回函数的一个高阶函数。

现在有一个输出当前时间的函数:

如果你还想知道输出的这个函数的名字是什么,但是又不行改变这个函数本身,那么就可以使用装饰器了。

其实就是相当于log(now)

请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。比如:int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

面向对象

类和实例

这样就定义了一个类,object代表的是从哪一个类继承的。

这个时候bart就指向一个student类的,后面的一串十六进制其实就是这个对象的地址。可以把student当成是一个模板,有什么必要的属性可以先丢进去,可以同过一个init函数把这些必要的属性先固定好。而如果对象再绑定属性那么这时候这个属性就仅仅是属于这个对象的了。

这个name就是仅仅绑定在了bart这个对象上面。

这个时候name和score就绑定在了类上面,创造的每一个对象都会存在这两个属性。

访问限制

上面所定义的变量都是公有变量,如果需要访问限制,那就是私有变量了。

如果要访问,那么就可以使用内部函数来访问:

事实上python的私有变量并不是严格意义上的私有变量,在外部是可以访问的,类名_属性名是可以访问的:

使用只能靠自觉了。 要注意一种错误写法,bart.__name = 'srgr',这样是不可以访问私有变量的,只会增加一个叫__name的新公有变量而已。

继承和多态

当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类。比如有一个动物类:

继承了animal类之后它方法也顺次继承了下来。

也可以加上属性和函数,覆盖原来的基类方法。 当我们定义了一个class类的时候实际上定义了一个数据类型:

但是如果反着来:

小的可以往大的转,大的不能往小的转。Dog可以看成Animal,但Animal不可以看成Dog。

你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思。对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。 对外扩张:可以添加animal子类。 对内封闭:不需要修改和基类有关的函数。

获取对象信息

首先判断一个数据类型可以使用

函数,基本类型都可以用

来判断。

对于函数和类都可以使用这个

函数判定:

如果是直接判断类型,是可以直接写int和str,但是如果判断函数,就需要用到

了。

instance函数。isinstance()就可以告诉我们,一个对象是否是某种类型。

isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。

可以判断有没有这个属性:

由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self变量:

这个时候的属性是绑定在了实例变量上,如果这个属性是属于这个类的,就可以直接定义:

千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

使用slots

如果想做这个类里面新添加一个方法的话那就要用到MethodType方法:

如果想要限制属性的添加,那就可以用slots方法了。

slots只是对当前的类有效果,如果要想对子类有效果,那么就要重新设置一个,这个时候子类的就等于父类的加上子类的限制了。

使用property

每一个都这样设置,烦死了,而@property就是一个把方法调用变成属性的一个装饰器:

单单设置了property就是只读而已,如果要是写,那么就要setter方法,这样非常安全。

由于resolution只是设置了property,使用只是只读而已。对于上面其他的属性设置了property和setter使用可读可写了。

多重继承

我们想要实现下列的一些动物,需要用到多重继承:

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。

这样一来就不再需要庞大的继承链了,只需要组合一些类的继承就好了。

定制类

这样输出感觉有点不好看,可以用str函数定制一下输出效果:

设置了str打印类信息是按照自定义格式,但是如果是对象就不是了。

如果要自定义类的效果,那就必须要设置repr == str:

如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个iter()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

定义一个iter把这个类转变成一个迭代器,再定义一个next迭代输出。 上面的设计虽然可以变成一个迭代器,但是是不可以按照list的类型来取的,比如[]等等。

同时,如果定义了call函数,是可以直接对实例进行调用的。

枚举类

当我们需要定义常量的时候,一个方法是用大写的变量来定义:

麻烦,可以直接使用枚举类来解决:

自动会从1开始计时:

如果需要更加准确的定义一个枚举类:

使用元类

之前讨论过type函数,type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:

type需要三个参数,类名,基础的基类,由于继承的基类可以不止是一个,使用要注意元祖的格式,通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

错误,调试和测试

错误处理

之前一般用来抛出错误的都是使用错误码,但是错误码表示错误其实行麻烦的,使用一般的语言都会内置try...catch机制,python也不例外。

改一下:

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo(),foo()调用bar(),结果bar()出错了,这时,只要main()捕获到了,就可以处理:

如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出:

如果不捕捉,那么错误就会一直往上抛,抛到最顶为止。如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。 可以使用logging模块:

logging相当于一个日志,把错误都记录下来,然后继续执行下面的,执行完了再把错误抛出来。 因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型:

单元测试

写完一个类或者是功能我们需要做一些测试,比如我们想要:

实现的这个类想要和python的数据类型dict实现的效果一样。为了做单元测试,需要实现也单元测试的类:

以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()。写完之后就可以运行单元测试了:

对于单元测试还有两个方法:setup()和setdown()。分别是在调用测试方法前后执行,假设你有数据库,那么就可以在setup打开,setdown关闭啦。

对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:

IO操作

文件读写

读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

打开一个文件对象,使用的就是open()这个函数,传入打开的文件位置和参数。

如果文件不存在,它会抛出一个IOerror的错误:

如果文件是打开成功的,就可以用read()函数一次读取全部内容了:

f.close()就是用来关闭文件:

但是对于打开文件有时候是会出现异常或者是错误:

每次都这样写,太麻烦了,所以引入了另一种写法:

这和前面的try ... finally是一样的,但是代码更佳简洁,并且不必调用f.close()方法。调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便:

StringIO 和 BytesIO

StringIO 很多时候,数据读写不一定是文件,也可以在内存中读写。StringIO顾名思义就是在内存中读写str。

把hello world存进了stringIO里面,然后从里面读取出来输出。

StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:

StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。

操作文件和目录

如果我们要操作文件、目录,可以在命令行下面输入操作系统提供的各种命令来完成。比如dir、cp等命令。如果要在Python程序中执行这些目录和文件的操作怎么办?其实操作系统提供的命令只是简单地调用了操作系统提供的接口函数,Python内置的os模块也可以直接调用操作系统提供的接口函数。

如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。也可以得到当前电脑的环境变量:

序列化

在程序中定义的变量都是存储在内存里面的,比如定义一个dict:

可以随时修改变量,比如把name改成'Bill',但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果没有把修改后的'Bill'存储到磁盘上,下次重新运行程序,变量又被初始化为'Bob'。 我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。

当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:

这样就又变回来了。Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。

如果需要在不同的编程语言之间传递参数,那么就要使用json了。比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。

这是json对应的格式,python已经提供了非常完善的python对象到json格式的转换:

要反序列化就只需要loads一下就好了:

事实上我们更喜欢把class对象序列化了:

错误的原因是Student对象不是一个可序列化为JSON的对象。

因为通常class的实例都有一个dict属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了slots的class。同样的道理,如果我们要把JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后,我们传入的object_hook函数负责把dict转换。

进程和线程

多进程

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

python已经封装了常见的系统调用,包括了fork() ,可以在程序中创建一个进程。一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。比如如下程序:

但是上面的代码是不可以运行的,因为window没有提供fork()函数。

python是一个跨平台的语言,那么自然也是存在一个跨平台的多进程支持,multiprocessing模块就是夸平台版本的多进程模块。

如果是要创建大量的进程的时候,就要用到进程池pool了。

多线程

多任务可以由多进程完成也可以由多线程完成。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

loop就是线程要执行的代码,join一下就是要等子线程执行完,如果没有等子线程执行完就结束了那么是显示不出下面的东西的:

由于一个进程在创建的时候,其实我们在运行这个程序的时候就是有一个进程的了,每一个进程创建的时候默认就会创建一个线程,那么一开始被创建的线程就是叫做主线程了。所以一开始得到的那个就是主线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。如果不起名字那么就是thrad-1 2这种的啦。 线程和进程比较大的区别应该就是在于变量共享这块,在进程里面变量是独立的,不同的变量在不同的进程之间是相互独立的,在同一个进程的线程中变量是共享的。所以在同一个进程中多个线程是很容易同时修改一个变量的。

究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

ThreadLocal

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦。每一个线程都是都是处理不同的对象,如果用一个 全局的变量dict存储所有的student对象,然后用自身也就是thread来获得线程对应的student对象。

这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。于是就用上了ThreadLocal,不用你自己查找的。

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小樱的经验随笔

C/C++中peek函数的原理及应用

C++中的peek函数   该调用形式为cin.peek() 其返回值是一个char型的字符,其返回值是指针指向的当前字符,但它只是观测,指针仍停留在当前位置,...

2955
来自专栏我的博客

php使用elasticsearch

1.引入包 composer require elasticsearch/elasticsearch 2.DEMO参考 <?php require_once ...

4237
来自专栏C/C++基础

将模板申明为友元

严格来说,函数模板(类模板)是不能作为一个类的友元的,就像类模板之间不能发生继承关系一样。只有当函数模板(或类模板)被实例化之后生成模板函数(或模板类),该函数...

1011
来自专栏debugeeker的专栏

《coredump问题原理探究》Linux x86版4.3节函数的逆向之条件结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

1152
来自专栏上善若水

P002PHP开发之变量定义

在函数体内定义的global变量,函数体外可以使用,在函数体外定义的global变量不能在函数体内使用,在全局范围内访问变量可以用特殊的 PHP 自定义 $GL...

1063
来自专栏Java技术栈

深度历险:Redis 内存模型详解

Redis 是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说 Redis 是实现网站高并发不可或缺的一部分。

1902
来自专栏我的博客

TP入门第十二天

1、模板技术 为何使用模板这里就不罗嗦了,直接介绍模板技术 变量传递和显示: 例如在action里面可以这样写 $title=”变量”; $arr=array(...

3366
来自专栏python3

Python语句-if.....else......

似乎所有的条件语句都使用if.....else.....,它的作用可以简单地概括为非此即彼,满足条件A则执行A的语句,否则执行B语句,python的if.......

1002
来自专栏恰童鞋骚年

你必须知道的指针基础-8.栈空间与堆空间

一个由C/C++编译的程序占用的内存分为以下几个部分:  1、栈区(stack):又编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于...

592
来自专栏赵俊的Java专栏

关于 Java finally 执行顺序 -- 修改版

2014

扫码关注云+社区

领取腾讯云代金券