[TOC]

变量和结构基础

  1. #注释;缩进视为代码块;句末无分号;

  2. 浮点数,当需要用科学计数时,1.2*10^6表示为1.2e6;0.00012表示为1.2e-4;

  3. 字符串用’或者”,多行使用”’语法

    print('''one
    two
    three''')
    
  4. 布尔值,True或False,注意大小写

  5. 空值,None

  6. 变量赋值

    a=123
    b='test'
    

    同一个变量可以被赋值不同类型的值。这儿和静态语言不同。

  7. 常量,Python中一般使用大写表示常量,和变量赋值一样,只是约定的常量,不是真不能改变的。

  8. 算术操作符

    1/2 #整数除法默认返回地板除,为0 1.0/2.0 #浮点除法执行真正的除法,为0.5 1//2 #地板除,为0.这是为了兼容新版把传统除法默认替换为真正的除法

    10%3 #取余,为1

    3**2 #幂运算,为9

    其余的加减乘除和位运算都与C语言相通

  9. 编码

    计算机中8bit表示1byte,所以1byte能表示256个值,在ASCII编码中,1byte表示一个字符。

    在中文中1byte表示一个字符是不够的,所以使用2byte,有65536个值。使用了GB2312编码。

    为了兼容大多语言,有了Unicode编码,通常用2byte表示。大多语言和操作系统支持Unicode。

    但是这样英文在前面补零就浪费IO,所以出现了可变长编码utf-8。可以使用1~6byte表示,英文为1byte,汉字通常3byte。

    在计算机内存中,统一使用Unicode编码,当需要保存或者传输时转换成utf-8

  10. 序列包括字符串、列表和元组。

  11. 序列操作符

    seq[ind] #获取下标为ind的元素
    seq[ind1:ind2] #获取两下标之间的元素
    seq*expr #重复expr次
    seq1+seq2 #连接两序列
    obj in seq #判断是否包含,返回True或False
    obj not in seq #判断obj是否不包含
    
  12. Python的字符串类型str,在内存中用Unicode表示。如果需要在网络传输或者保存到本地,就需要把str转为以字节为单位的bytes

    x=b'ABC'
    
  13. 格式化

    'hello,%s' % 'world'
    'hello,%s and %s' % ('a', 'b')
    'rate %d %%' 7 #rate 7 %
    
  14. 列表是一种可变的有序的集合。

    l=[]
    l=[1,2,3,4,5]
    l1=[1,2,[3,4],5]
    l.pop()
    l[-1]
    l[:2]
    l1[2][1]
    
  15. 元组是一种不可变的有序集合

    t=()
    t1=(1,) #为了避免被理解成运算的()
    t2=(1,2,3,[4,5]) #这是列表中的值是可变的
    
  16. 条件判断

    a=100
    if a<100:
        print('less')
    elif a=100:
        print('proper')
    else:
        print('more')
    
  17. for循环

    l=[1,2,3,4,5,6,7,8,9]
    sum=0
    for i in l:
        sum = sum + i
    print(sum)
    
  18. while循环

    sum=0
    n=1
    while n<10:
        sum = sum + n
        n = n + 1
    print(sum)
    
  19. 三元操作符 类似c?x:

    y在Python中如下

    smaller = x if x>y else y
    

    这儿等效于smaller = x>y ? x : y

  20. 映射类型:字典

    d={'a':1,'b':2,'c':3}
    d['a']
    'a' in d #判断key是否存在
    d.pop('c')
    
  21. Python里的映射类型只有dict。一个字典对象是可变的,容器类型,无序,存储任意多个。映射类型通常被叫做hash表。

    序列类型用有序的数字键做索引将数据以数组的形式存储,查找和插入随着元素增加而增加。

    字典类型使用键名的哈希函数值决定值的地址,查找和插入的时间复杂度恒定。

    更多序列容器和关联式容器参见STL容器

  22. 集合set

    Python的set没有特定的语法格式,一般使用set()和frozenset()(不可变集合)创建

    s=set([1,2,3,4,5,6,7]) #{1,2,3,4,5,6,7}
    s<t #s是t的严格子集
    s>=t #s是t的超集
    s-t #差集,只属于s不属于t
    s^t #对称差分,只属于s或只属于t,而不能是交集
    
    集合操作符
    in
    
    # not in
    
    !=
    < #是严格子集
    <= #是子集
    
    > #是严格超集
    > = #是超集
    > &
    > |
    
    - #差集
    ^ #对称差分函数
    
  23. 列表生成式 对于要生成一个列表,可以使用for in语法,然后在里面append元素

    不过这种方法太low太冗长了,Python里提供了一种更优雅的写法(其实TM就是数不清的语法糖),这儿最前面的就相当于for循环内的表达式

    [x * x for x in range(1, 11)] #生成1~11中所有数的平方的列表
    
    [x * x for x in range(1, 11) if x % 2 == 0] #有条件的生成
    
    [m + n for m in 'ABC' for n in 'XYZ'] #双层生成
    
    d = {'x': 'A', 'y': 'B', 'z': 'C' }
    [k + '=' + v for k, v in d.iteritems()] #对字典遍历
    ['y=B', 'x=A', 'z=C']
    

函数

  1. 定义函数

    def nop():
        pass #定义空函数
    
    def func(x):
        if x>10:
            return True
        else:
            return False
    
    import math
    def move(x, y, step, angle=0):
        nx = x + step * math.cos(angle)
        ny = y - step * math.sin(angle)
        return nx, ny #这儿其实返回的是个tuple,省略了括号
    
  2. 函数返回0个值时返回的对象为None,1个的时候为object,大于1时为tuple

  3. 位置参数、默认参数、关键字参数

    位置参数必须以准确顺序来传递,且数目必须一致。

    默认参数可以缺省,缺省时为默认值。默认参数要必需的参数之后。

    如果命名了参数,则可以不按照次序来给出参数。

    def net_conn(host, port=80, stype='tcp'):
    
    net_conn_suite
    
    anet_conn('phaze', 8000, 'udp') # no def args used
    bnet_conn('kappa') # both def args used
    cnet_conn('chino', stype='icmp') # use port def arg
    dnet_conn(stype='udp', host='solo') # use port def arg
    enet_conn('deli', 8080) # use stype def arg
    fnet_conn(port=81, host='chino') # use stype def arg
    
  4. 可变长度的参数

    函数调用中可使用*和**符号来指定元组和字典的元素作为非关键字以及关键字参数的方法。定义时也这样表示。

    可变长的参数元组必须在位置和默认参数之后。

    def tupleVarArgs(arg1, arg2='defaultB', *theRest): 
    ...     'display regular args and non-keyword variable args'
    ...     print 'formal arg 1:', arg1
    ...     print 'formal arg 2:', arg2
    ...     for eachXtrArg in theRest:
    ...         print 'another arg:', eachXtrArg
    ... 
    >>> tupleVarArgs('abc')
    formal arg 1: abc
    formal arg 2: defaultB
    >>> tupleVarArgs(23, 4.56)
    formal arg 1: 23
    formal arg 2: 4.56
    >>> tupleVarArgs('abc', 123, 'xyz', 456.789)
    formal arg 1: abc
    formal arg 2: 123
    another arg: xyz
    another arg: 456.789
    

    可变长的参数字典要是最后一个参数,并且非关键字参数元组于它出现。

    def dictVarArgs(arg1, arg2='defaultB', **theRest):
    ... 'display 2 regular args and keyword variable args'
    ... print 'formal arg1:', arg1
    ... print 'formal arg2:', arg2
    ... for eachXtrArg in theRest.keys():
    ... print 'Xtra arg %s: %s' % (eachXtrArg, str(theRest[eachXtrArg]))
    ... 
    >>> dictVarArgs(1220, 740.0, c='grail')
    formal arg1: 1220
    formal arg2: 740.0
    Xtra arg c: grail
    >>> dictVarArgs(arg2='tales', c=123, d='poe', arg1='mystery')
    formal arg1: mystery
    formal arg2: tales
    Xtra arg c: 123
    Xtra arg d: poe
    >>> dictVarArgs('one', d=10, e='zoo', men=('freud', 'gaudi'))
    formal arg1: one
    formal arg2: defaultB
    Xtra arg men: ('freud', 'gaudi')
    Xtra arg e: zoo
    Xtra arg d: 10
    

    同时存在关键字和非关键字的可变长参数的情况:

    >>> def newfoo(arg1, arg2, *nkw, **kw):
    ...     print 'arg1 is:', arg1
    ...     print 'arg2 is:', arg2
    ...     for eachNKW in nkw:
    ...         print 'additional non-keyword arg:', eachNKW
    ...     for eachKW in kw.keys():
    ...         print "additional keyword arg '%s': %s" % (eachKW, kw[eachKW])
    ... 
    >>> newfoo('wolf', 3, 'projects', freud=90, gamble=96)
    arg1 is: wolf
    arg2 is: 3
    additional non-keyword arg: projects
    additional keyword arg 'gamble': 96
    additional keyword arg 'freud': 90
    
  5. 调用带有可变长参数对象函数

    先匹配位置和默认参数,匹配完再看非关键词参数,最后看关键词参数

    在调用的时候也可以用如下的方式:

    >>> newfoo(2, 4, *(6, 8), **{'foo': 10, 'bar': 12})
    
    arg1 is: 2
    arg2 is: 4
    additional non-keyword arg: 6
    additional non-keyword arg: 8
    additional keyword arg 'foo': 10
    additional keyword arg 'bar': 12
    
    >>> aTuple = (6, 7, 8)
    >>> aDict = {'z': 9}
    >>> newfoo(1, 2, 3, x=4, y=5, *aTuple, **aDict)
    arg1 is: 1
    arg2 is: 2
    additional non-keyword arg: 3
    additional non-keyword arg: 6
    additional non-keyword arg: 7
    additional non-keyword arg: 8
    additional keyword arg 'y': 5
    additional keyword arg 'x': 4
    additional keyword arg 'z': 9
    
  6. 内部/内嵌函数,函数中定义的函数,只能在函数体内调用。

  7. 函数装饰器(Decorator)

    装饰器是一种设计模式,AOP(Aspect Oriented Programming,面向方面编程)

    装饰器其实是一个函数,一个用来包装函数的函数。返回一个修改后的函数对象,将其重新赋值原来的标识符,并永久丧失对原始函数对象的访问。

    比如我们定义了一个func,不希望对func进行任何修改的增强func的功能,增加对一类func的日志打印。

    使用语法糖@来实现装饰器

    def log(func):
        def wrapper(*args, **kw):
            print 'call %s():' % func.__name__
            return func(*args, **kw)
        return wrapper
    
    @log
    def now():
        print "2016"
    
    now()
    

    如上面代码所示,调用now的时候,其实now已经发生了改变,now=log(now)
    在这儿还能定义多个装饰器,会一层一层包裹起来
    当定义装饰器带参数时,情况稍复杂…

  8. 传递函数

    所有的对象都是通过引用来传递的,函数也不例外。当对一个变量赋值时,实际上是将相同对象的引用复制给这个变量。如果对象是函数时,这个对象所有的别名都是可调用的。

    def foo():
        print 'in foo()'
    
    bar = foo
    bar() # in foo()
    
  9. 匿名函数与lambda

    Python允许使用lambda来创造匿名函数。我们创建了一个匿名函数,但是没有在任何地方保存它,调用它。这样的函数对象在创建时引用计数被设置为True,但是因为没有引用保存下来,计数又回到零,然后被垃圾回收掉。为了保留住这个对象,我们将它保存到一个变量中,以后可以随时调用。

    def true(): return True #假设可以这样写,则等效于下面的
    lambda :True
    
    def add(x, y=2): return x+y #等效于下面
    lambda x, y=2 : x+y
    
    a =lambda x, y=2 : x+y
    a(5)
    
  10. 偏函数

    当调用对于大量同一个函数的情况,大部分参数相同,少量一两个参数不同时,我们会写出很冗余的代码。这时可以使用偏函数来解决,将其中部分参数固化,就只需要改变其他少量参数。

    from functools import partial #使用partial函数来创建PFA
    add1 = partial(add, 1) #等效 add1(x) == add(1, x)
    
    baseTwo = partial(int, base=2)
    
  11. 变量作用域

    全局变量全局可见,在函数内部也可见。

    局部变量只在函数内部可见。在函数内,Python先从局部作用域 开始搜索,然后才去全局域寻找,找到为止。

    同名全局和局部变量不冲突,只是由于寻找次序不同而看上去函数内部像是覆盖,其实不是。

    可以使用golbal关键字在函数内部建立全局域的引用,这样函数内部同名变量就能修改全局变量。

  12. 闭包

    当在一个函数中定义一个函数,当外部函数返回内部函数,调用时内部函数会带着外部函数的作用域。这样内部函数就被认为是闭包。

    def line_conf():
     b = 15
     def line(x):
         return 2*x+b
     return line # return a function object
    
    b = 5
    my_line = line_conf()
    print(my_line.__closure__)
    print(my_line.__closure__[0].cell_contents)
    

    从这个例子中可以看到,my_line在调用的时候,引用了line_conf的作用域中的b
    相当于使用line带着一个流浪的作用域
    看这个例子,能更深理解调用闭包时带着外层函数的作用域到处跑

     def counter(start_at = 0):
         count = [ start_at ]
         def incr():
             count[0] += 1
             return count[0]
         return incr
    
    count = counter(5)
    
    count()
    6
    count()
    7
    

    这儿可以看到每次调用count方法,其引用的count值是内部函数附带的流浪的作用域中的变量,他在这次调用中始终存在。
    这样就能创建一种具有广泛含义的函数,有点类似偏函数,但是闭包是关于使用定义在其他作用域的变量,而PFA更像是currying,与函数调用有关。看这个例子,创建了一种直线的类型

     def line_conf(a, b):
         def line(x):
             return ax + b
         return line
    
     line1 = line_conf(1, 1)
     line2 = line_conf(4, 5)
     print(line1(5), line2(5))
    

  1. 生成器

    当需要遍历一个巨大的list的时候,一般做法是先生成列表,再遍历

    但是我们不能不考虑内存的使用情况

    这时候应当考虑使用生成器,通过yield语法挂起获取其中一个的返回值,当调用回到程序中时,仍能从我们上次离开的地方继续。

    g = (x * x for x in range(10))
    

    前面我们看到了列表生成式,这儿[]变成了(),就变成了生成器,前面的xx等效于yield xx
    这里如果调用g()会返回一个迭代器对象,其实可以使用迭代器的接口来返回每一个值,g.next(),这样依次迭代
    在执行中每次遇到yield其实就中断了,需要使用迭代器的接口来处理,然后再次调用

    在迭代器中还有个send()方法,每次把值传给yield后的变量,看例子

     def counter(start_at=0):
            count=start_at
            while True:
                   val=(yield count)
                   if val is not None
                          count=val
                   else count+=1
    
     count=counter(5)
     count.next()
     5
     count.next()
     6
     count.send(9)
     9
     count.next()
     10
    

模块

  1. import.一般按照标准库模块、第三方模块、自定义模块来导入

    import module
    import module1,module2,module3
    
  2. 如果模块在全局中导入,他的作用域是全局的。如果在函数中导入,作用域是局部的。

    如果模块是第一次被导入,它会被加载并执行

  3. from-import导入指定的模块属性。这个时候不需要使用属性/句点属性标识来访问模块的标识符。但是赋值的时候,却只能通过属性/句点属性标识来操作,否则只会赋值到本模块内的变量中

    from module import name1,name2,name3
    
  4. as别名

    import Tkinter as tk
    from cgi import FieldStorage as form
    
  5. 一个模块只被加载一次,无论它被导入多少次

  6. 可以使用__import__()来导入模块

    sys = __import__('sys')
    
  7. reload()内建函数可以重新导入一个已经导入的模块。首先使用的时候只能全部导入,不能是from-import。然后导入语法,reload(sys)而不是reload(‘sys’)

  8. 有时候模块和模块可能会名称冲突,所以引入了包

    文件夹名为包名

    里面可以多层包结构,通过.引用

    一个文件夹目录必须有一个__init__.py文件,否则不会被当做包。这个文件可以为空,也可以不为空,不为空时代表包的属性和方法

  9. 相对导入和绝对导入//TODO

  10. 两个下划线的方法和变量一般是特殊用途的,我们不要命名成这样

    一个下划线一般指非公开的,_var,当from module import *时就不会被引入

  11. 当运行当前脚本时,解释器会赋予当前脚本一个特殊值__name__为__main__

    if __name__ == '__main__':
        test()
    
  12. 使用pip命令来安装第三方模块

    pip install Pillow
    

类和对象

  1. 定义一个类

    class Student(object): #类名通常是大写开头,Object表示继承的类
    
    def __init__(self, name, score): #构造函数,self固定不变
        self.name = name
        self.score = score
    
    def print_score(self): 
        print('%s: %s' % (self.name, self.score))
    
    bart = Student('Bart Simpson', 59)
    bart.print_score()
    
  2. 访问限制

    上面的例子中,可以在类外直接赋值,bart.name=’luca’

    所以可以通过修改属性名来限制,限制把name前加上__,两个下划线

    self.__name = name

    这样外面调用bart.__name就会报错,这样就变成了私有变量

    而其实在Python解释器中,属性变成了_student.__name,外部还是能通过bart._student__name来访问

  3. 继承

    class MiddleStudent(Student):
        pass
    

    这样就继承了Student类,并继承了Student中的方法
    当存在同名方法时,子类的方法会覆盖父类的方法

  4. 用type(fn)来获取类型,用isinstance(instance, class)来判断是否是某个对象的实例,用dir(‘abc’)来获取一个对象的所有属性和方法。

  5. 当定义一个class,创建一个实例后,我们可以给绑定任意方法和属性。

    s = Student()
    s.name = 'luca'
    
    def set_score(self, score):
         self.score = score
    
    Student.set_score = set_score #给所有实例绑定方法
    

    如果我们要限制不能动态绑定属性,可以使用__slots__

    class Student(object):
        __slots__ = ('name', 'age')
    

    如果这时候再动态绑定属性,会提示不存在,因为新属性不会被放到__slots__中。但是这个对子类不生效。

  6. 把属性变成方法调用:使用@property,使用装饰器的方法

    class Student(object):
    
    @property
    def score(self):
        return self._score
    
    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
    

    把一个getter方法变属性,使用@property
    这时@property又创造了一个@score.setter方法,对score进行属性赋值。
    这样可以对属性score的赋值进行控制。

    s = Student()
    s.score = 60
    s.score 
    60
    
    s.score = 9999
    Traceback (most recent call last):
      ...
    ValueError: score must between 0 ~ 100!
    
  7. Python支持多重继承

    class Bat(Mammal, Flyable):
        pass
    
  8. 使用元类,type,metaclass

  9. 错误处理

    try:
        print('try...')
        r = 10 / int('2')
        print('result:', r)
    except ValueError as e:
        print('ValueError:', e)
    except ZeroDivisionError as e:
        print('ZeroDivisionError:', e)
    else:
        print('no error!')
    finally:
        print('finally...')
    print('END')
    

    在try中执行代码时,遇到错误会不再执行,而是进入except块中,执行完后,如果有finally块,还会进入finally块,然后结束。
    不同类型的错误可以进入不同类型的except块。
    所有的错误类型都是继承自BaseException类。
    调用堆栈,如果错误没有被捕获,会根据调用栈一直往上抛。
    抛出异常

    class FooError(ValueError):
        pass
    
    def foo(s):
        n = int(s)
        if n==0:
            raise FooError('invalid value: %s' % s)
        return 10 / n
    
    foo('0')
    
    #other
    def bar():
        try:
            foo('0')
        except ValueError as e:
            print('ValueError!')
            raise
    
    bar()
    

    使用raise抛出异常,先定义好一个错误类。
    在except中还能使用raise,这儿虽然有except,但只是print一下并不处理,把错误继续抛给上一级调用。

IO编程

  1. 读写文件

    try:

    f = open('/path/to/file', 'r')
        print(f.read())
    finally:
        if f:
            f.close()
    

    这个代码块可以用with来替代

    with open('/path/to/file', 'r') as f:
        print(f.read())
    

    这儿表示只有存在f的时候才执行下面的代码块。
    read每次读完整个文件,可以使用read(size)循环调用来完成。
    或者可以使用readlines来读取

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

    写文件的时候要调用close(),才能保证把内容写进磁盘。为了避免忘记调用,使用下面的方式来写

    with open('/Users/michael/test.txt', 'w') as f:
        f.write('Hello, world!')
    
  2. 往内存里读写字符串StringIO,读写二进制文件BytesIO。

  3. 我们把变量从内存中变成可存储或者可传输的过程叫做序列化,pickling,其他语言中叫serialize等。

  4. 我们在不同语言中传递对象,需要把对象序列化成标准格式,比如JSON。

进程和线程

  1. Linux提供一个fork的系统调用,它调用一次返回两次,因为它会把当前的进程复制一份,分别在父进程和子进程返回。子进程永远返回0,父进程返回子进程的id。

  2. 可以使用os.fork()创建子进程,但是Windows平台不可用,所以使用另外一个模块multiprocessing

    from multiprocessing import Process
    import os
    
    # 子进程要执行的代码
    def run_proc(name):
        print('Run child process %s (%s)...' % (name, os.getpid()))
    
    if __name__=='__main__':
        print('Parent process %s.' % os.getpid())
        p = Process(target=run_proc, args=('test',))
        print('Child process will start.')
        p.start()
        p.join()
        print('Child process end.')
    
  3. 如果要开多个进程,可以使用Pool方法

    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()
        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.
    

    看代码知道,进程池创建了4个进程,每个进程都跑long_time_task这个任务。
    调用的时候调了五次,这时task 4在其中task 2结束后才启动。
    一般pool的参数是默认CPU的核数。

  4. 进程间通信,使用queue或者pipes

    from multiprocessing import Process, Queue
    import os, time, random
    
    # 写数据进程执行的代码:
    def write(q):
        print('Process to write: %s' % os.getpid())
        for value in ['A', 'B', 'C']:
            print('Put %s to queue...' % value)
            q.put(value)
            time.sleep(random.random())
    
    # 读数据进程执行的代码:
    def read(q):
        print('Process to read: %s' % os.getpid())
        while True:
            value = q.get(True)
            print('Get %s from queue.' % value)
    
    if __name__=='__main__':
        # 父进程创建Queue,并传给各个子进程:
        q = Queue()
        pw = Process(target=write, args=(q,))
        pr = Process(target=read, args=(q,))
        # 启动子进程pw,写入:
        pw.start()
        # 启动子进程pr,读取:
        pr.start()
        # 等待pw结束:
        pw.join()
        # pr进程里是死循环,无法等待其结束,只能强行终止:
        pr.terminate()
    
    Process to write: 50563
    Put A to queue...
    Process to read: 50564
    Get A from queue.
    Put B to queue...
    Get B from queue.
    Put C to queue...
    Get C from queue.
    

    这里可以看到pw和pr两个子进程,他们的数据都是来自queue,通过queue共享数据。
    这儿两个子进程差不多是同时启动的,pw进程慢慢地往队列里压数据,而pr进程则需要等待有数据了才执行,否则就被挂起。

  5. 任何进程启动时会默认启动一个线程,也叫主线程。主线程又可以启动新的线程。在多进程中,同一个变量,每个进程会有一份自己的拷贝,互不影响。而在一个进程中的多线程却共享同一个变量,多个线程如果同时修改一个变量,可能把内容改乱。

    这个时候需要考虑加锁,确保关键运算的时候只有一个线程运行。

    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)
    

    这个例子中,change_it先加后减,按理来说应该为0,但是两个线程同时跑,由操作系统调度,很可能出现交叉情况,导致最后的结果出错。但是上面加了锁,其实就是把线程串行化了,其实还是相当于单线程。

  6. 在多线程中,一个线程跑满,会100%占用一个CPU。如果两个线程跑满,则占用200%个CPU。在N核CPU中,可以启动N个死循环线程把CPU全部跑满。

    在C等语言中可以这样做,但是在Python中这样做却只能跑满一个CPU,因为Python有一个GIL锁,任何Python线程启动前都会先获得GIL锁,然后每跑完100个字节码,解释器就会释放GIL锁,让其他线程有机会跑。所以即使有多核,也最多只能用到一个核。

    所以在Python中如果要利用多核,可以使用多进程。

  7. 在多线程中,每个线程都有自己的数据。一个线程使用自己的局部变量比全局变量好,因为局部变量只有线程内部可见,不会影响其他线程。而全局变量的修改必须加锁。

    ThreadLocal(TODO)

  8. 多任务一般采用master-worker模式,master负责分配任务,worker负责执行任务。多线程或者多进程都可以使用master-worker模式。

    多进程模式稳定性高但是创建新进程代价高。

    多任务一般要考虑核心数,否则资源都消耗在了任务切换了。

    计算密集型耗CPU多,比较适合c等语言,而Python等效率不行。

    IO密集型,涉及到网络、磁盘IO,这类适合用Python等开发,而c开发无法提升效率。

  9. 在IO密集型任务中,大部分时间用来等待IO,这时候不太适合单进程单线程方式,最好使用多进程多线程模式。

    而现代操作系统支持异步IO,可以在单进程单线程模型执行多任务(协程),这叫做事件驱动模型,比如nginx。

  10. process可以分不到不同机器上,而thread只能分布到同一台机器的几个CPU上。