首页 Python 学习笔记
文章
取消

Python 学习笔记

基础

数据类型和变量

整数:

对于很大的数,可以使用_来分割,10_000_000_00010000000000是一样的

python整数没有大小限制,浮点数也没有,超出一定范围直接直接表示为inf

字符串:

  • 使用 r'' 表示字符串默认不转义

  • 使用 '''...'''的格式表示多行内容

常量:

用全部大写的变量来表示常量(本质还是变量,Python没有)

空值:

python中 None 表示空值

编码

通用的字符编码工作方式(utf-8更节省空间,utf-8英文字符是1字节,unicode通常是2字节)

rw-file-utf-8

浏览网页的时候

web-utf-8

字符串

编码

python字符串类型是 str 使用Unicode编码

  • ord() 获取字符的整数表示

  • chr() 将编码转为对应的字符

字符串在内存中以Unicode表示,如果要在网上传输,需要转为字节格式,bytes类型在Python中用 b''表示

b'ABC''ABC'显示内容和前者一样,但bytes类型的每个字符只占一个字节

以Unicode表示的str用encode('编码')来编码

1
2
3
4
5
6
7
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>>

解码

解码使用 decode('编码')

len()函数

如果里面的参数是字符串,计算的是字符数,如果里面的参数是字节类型的,就是计算的是字节数

1
2
3
4
>>> len('中文abc'.encode('utf-8'))
9
>>> len('中文abc')
5

编辑器编码问题

1
2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

第一行注释是为了告诉linux系统 这个文件是Python可执行程序,windows会忽略

第二行注释告诉解释器按utf-8读取源代码,防止中文输出有问题

但还需要确保文本编辑器也在使用utf-8编码

格式化输出

%d %s 字符串 %f 浮点数 %x 16进制数

整数和浮点数可以指定是否补0和位数

主要三种 普通的 format 和 fstring

1
2
3
4
5
6
7
8
9
10
>>> print('%d %s'%(123,'哈哈'))
123 哈哈
>>> print('%d'%123)
123
>>> print('{0:03d} {1:.2f}'.format(1,3.1415))
001 3.14
>>> a=1
>>> b=3.1415
>>> print(f'{a:03d} {b:.2f}')
001 3.14

list

初始化用[]

  • a[-1]表示倒数第一个元素

  • a.append(...) 末尾追加

  • a.insert(1,'123') 索引1位置插入123

  • a.pop(i) 删除指定索引位置的元素,如果不指定i,即末尾弹出

tuple

元组,一旦初始化不能修改 更安全

a=() 空元组

a=(1) a会被赋值为1 a=(1,) 加逗号消除歧义

dict

其他语言中的map

d['haha'] 如果key不存在会报错,d.get('haha',1) 表示取key为haha的,如果不存在,使用指定的value 1,后面的1可省

dict通过计算key的哈希只来得到存放的内存位置,为了保证key的正确性,key应该是不可变对象,比如list可变,就不可以作为key

set

集合 使用{}表示

函数

空函数

如果想定义空函数,可以用pass语句

pass语句可以用来作为占位符,也可以写在一般的语句中,比如:

if age >= 18:

        pass

先让代码运行起来

类型检查

数据的类型检查 可以使用 isinstance 来实现

比如:

1
2
3
4
5
6
7
def my_abs(x):
    if not isinstance(x,(int,float)):
        raise TypeError('wrong type')
    if x>= 0:
        return x
    else:
        return -x

返回多值

python返回多个值是返回一个tuple,本质上也是一个单一值

参数

默认参数

默认参数不能是可变的

比如:

1
2
3
def add_end(L=[]):
    L.append('END')
    return L

正常调用,不会出错,但是多次默认参数调用会出错

1
2
3
4
5
6
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

因为定义函数时,该默认值就会被定义出来,应改成

1
2
3
4
5
def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

可变参数

传入的参数个数是可变的,初步的想法是,定义个参数,传入的参数是list或者tuple,但是这样需要组装,即加上()[];改进方法是,定义函数时,在参数前加上* 来修饰,即可传入任意个参数,包括0个,不用再用()[]修饰。如果此时要输入的是list或者tuple,在调用函数时,变量前加上*即可。

关键字参数

**修饰参数名,*本质上是调用时自动组成一个tuple,而关键字参数本质上是将任意个含参数名的参数自动组装成一个dict,关键字参数不是必选的,同可变参数类似,如果调用时输入的参数是dict,在变量名前加上**即可

命名关键字

1
2
def person(name, age, *, city, job):
    print(name, age, city, job)

即用特殊分隔符*来隔开,后面的参数会被视为关键字参数,如果已经有可变参数了,就不需要*分割了

1
2
def person(name, age, *args, city, job):
    print(name, age, args, city, job)

后面的可变参数可以添加缺省值,从而简化调用

参数定义顺序:必选参数、默认参数、可变参数、命名关键字参数和关键字参数

高级特性

切片

切片 slice

list dict str都可以进行切片

迭代

dict的迭代:

1
>>> d = {'a': 1, 'b': 2, 'c': 3}

对key迭代 对值迭 对键值迭代

1
2
3
>>> for key in d:
...     print(key)
...

for value in d.values()for k, v in d.items()

判断是否可迭代

1
2
>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable)

如果需要索引,可以使用 enumerate 函数

1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C

列表生成式

list comprehensions

几个简单示例:

1
2
3
4
5
6
>>> [x*x for x in range(1,11) if x%2==0]
[4, 16, 36, 64, 100]
>>> [x+y for x in 'ABC' for y in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
>>> [x if x%2==0 else -x for x in range(1,6)]
[-1, 2, -3, 4, -5]

for后面的if是过滤条件不能带else,for前面的if是表达式,必须带上else来确定x

生成器 generator

在python中,一边循环一边计算的机制,称为生成器。

与列表生成式类似,将[]改成()即可,通过 next() 获取下一个返回值,但一般不适用next方法,而是通过for循环,比如:

1
2
3
4
5
6
7
8
9
>>> g=(x*x for x in range(1,6))
>>> for n in g:
...     print(n)
...
1
4
9
16
25

关键字 yield

以斐波那契数列为例,

1
2
3
4
5
6
7
8
9
def fib(x):
    n,a,b=0,0,1
    while n<x:
        print(b)
        a,b=b,a+b
        n+=1
    return 'done'

print(fib(5))

输出是 1 1 2 3 5 8 done,如果将print(b)改成yield b,fib函数就变成了generator函数(函数定义中包含 yield 关键字)

普通函数遇到return语句 或者 最后一行函数语句就返回,而generator函数,每次调用next()时执行,遇到yield语句返回,再次执行时,会从返回的yield语句处继续执行

for x in fib(5)可以循环得到1 1 2 3 5 8 但无法获得return的done值,需要捕获 StopIteration错误,返回值包含在该错误的value

1
2
3
4
5
6
7
8
g=fib(5)
while True:
    try:
        x=next(g)
        print(x)
    except StopIteration as e:
        print(e.value)
        break

即可获取返回值了

迭代器 iterator

可以直接作用于 for 循环的对象统称为可迭代对象 Iterable

如果可以被 next()函数调用并不断返回下一个值的对象称为迭代器 Iterator

list dict str 等是 Iterable 但不是 Iterator,但可以通过 iter()函数获得一个Iterator对象

python中的for循环本质上就是通过不断调用next()函数实现的

1
2
for x in [1, 2, 3, 4, 5]:
    pass

本质为:

1
2
3
4
5
6
7
8
9
10
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

函数式编程

纯粹的函数式编程语言编写的函数没有变量,即输入是确定的,输出是确定的。(python函数允许变量,不是纯函数式编程语言)

函数式编程的特点之一就是 允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

高阶函数

函数名本质就是一个变量,该变量可以指向函数,而函数的参数可以接收变量,所以函数也可以接收一个函数作为参数,这种函数称之为高阶函数,比如:

1
2
3
4
def add(a,b,f):
    return f(a)+f(b)

add(-1,2,abs)

map/reduce

python 内建了 map() 和 reduce() 函数

map()的两个参数 一个是函数,一个是 Iterable 对象,map将传入的函数一次作用到序列的每个元素,并把结果作为新的Iterator对象返回。

1
2
3
4
5
def f(x):
    return x*x

r=map(f,list(range(1,5)))
list(r)

输出[1,4,9,16]

map返回的结果是iterator 是 惰性序列,需要list()函数将整个序列计算出来返回

reduce()的用法,同样是两个参数,reduce的效果相当于把结果和下一个元素进行某种运算,比如:

1
2
3
4
5
6
from functools import reduce

def reduce_ff(x,y):
    return x+y

reduce(reduce_ff,list(range(1,5)))

输出10

类似的 还可以写一些 类似字符转数字的功能:

1
2
3
4
5
6
7
8
9
def char2num(c):
    digits={'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,}
    return digits[c]

def ff(x,y):
    return x*10+y

# reduce(ff,map(char2num,'12345'))
reduce(lambda x,y:x*10+y,map(char2num,'12345'))

filter

与map类似,也是接受一个函数和一个序列,根据函数的返回值是True还是False来选择保留还是丢弃

简单举个输出素数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 输出素数 运用埃氏筛法 从2开始 2的倍数不是素数 所以可以从3开始构造奇数序列 
# 再过滤,3的倍数不是素数...
def get_list_num():
    n=3
    while True:
        yield(n)
        n+=2

def prime():
    yield 2
    it = get_list_num()
    while True:
        n=next(it)
        yield n
        it=filter(lambda x:x%n>0,it)

i=1
ls=[]
for the_prime in prime():
    if(i<20):
        ls.append(the_prime)
        i+=1
    else:
        break
ls

输出:[2, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37]

sorted

sorted() 也是高阶函数,前一个接收序列,第二个参数key函数进行自定义排序,还可以添加第三个参数 reverse

举个简单例子

1
2
3
4
5
6
7
8
# sorted 排序
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 66)]

# 按分数降序
L1=sorted(L,key=lambda x:x[1],reverse=True)
print(L1)
# 按名字升序
sorted(L,key=lambda x:str(x[0]).lower())

返回函数

高阶函数除了可以接收函数作为参数外,还可以将函数作为返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 返回函数测试
def createCounter():
    i=0
    def counter():
        nonlocal i
        i=i+1
        return i
    return counter

# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('测试通过!')
else:
    print('测试失败!')
  • nonlocal 声明了 counter函数中的变量i不是临时变量,而是createCounter的i

  • counterA是一个函数,通过counterA()进行函数调用,第一次调用时i一开始时0,调用counter 得到的是1,第二次调用时,因为返回的值是一个函数(该函数引用了外部函数createCounter的变量,相关参数和变量都会保存在返回的函数中,这种程序结构称为闭包Closure),而i已经变成了1,此时再调用,得到的结果就是1+1为2

  • counterB得到的是另一个函数,所以之前counterA的变化不会对其产生影响

装饰器

函数对象有一个__name__属性来获取函数的名字

如果有一个函数

1
2
def now():
    print(time.time())

如果需要加强该函数的功能,比如调用前打印当前调用的函数名,但now函数不进行修改,则在代码运行期间动态增加功能的方式 称为 装饰器Decorator

装饰器本质是一个返回函数的高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
import time

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

@log
def now():
    print(time.time())

now()
  • log 函数返回 wrapper函数,wrapper()函数可以接收一切参数

  • 使用@修饰符,本质是执行了一句now = log(now),本来now函数还在,但now这个变量指向了wrapper这个新函数

如果修饰器本身要输入参数,会更加复杂:

1
2
3
4
5
6
7
8
9
10
11
def log(text):
    def log2(func):
        def wrapper(*args,**kw):
            print('%s %s()'%(text, func.__name__))
            return func(*args,**kw)
        return wrapper
    return log2

@log('execute')
def now():
    print(time.time())

输出 execute now() 1664445736.5886693

三层的嵌套效果本质是 now = log('execute')(now)

但还有一个问题,此时now函数的name就变成了wrapper,解决方式,用@functools.wraps(func)来修饰:

1
2
3
4
5
6
7
8
def log(text):
    def log2(func):
        @functools.wraps(func)
        def wrapper(*args,**kw):
            print('%s %s()'%(text, func.__name__))
            return func(*args,**kw)
        return wrapper
    return log2

偏函数

使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

具体暂略

模块

模块module:避免函数名和变量名冲突,提高代码可维护性,可重用性。abc.pyabc就称之为模块,模块名可能冲突,又引入了包package,比如mycompany文件夹下有个abc.py 则模块名就成了mycompany.abc

每个包目录下应该存在一个 __init__.py,python才会将其当作一个包,而不是普通目录,该文件可以是空文件,该文件本身就一个模块,而模块名就是 mycompany

使用模块

注意:

  • __name__ __author__ 这种称为特殊变量,可以被直接引用,但都有特殊用途

  • _xxx / __xxx 下划线开头的变量或函数一般被称之为private,不应该被引用(但还是可以引用,python没有限制)

面向对象编程

类和实例

class Student(object):所有类最终都会继承object类,python3可以不写object,会默认继承

  • 可以自由地给实例变量绑定属性

    1
    2
    3
    
    xxx=Student()
    xxx.name='123'
    print(xxx.name)
    

    一般的做法是,将必须绑定的属性填写进去 定义一个 __init__方法(初始化时必须带上相关参数)

访问限制

内部属性不被房补访问,可以再属性的名称前加上两个下划线 __

如果外界要获取或者修改,一般增加 get_name set_name 这样的方法,

如果 强制 xxx.__name赋值,虽然可以调用,但本身并不是类中的属性,实际上private限制本质是python把类中的私有属性名自动改成了类名加私有属性名 _Student__name,因此赋值__name本质上是给实例增加了一个属性而已

继承和多态

简单例子:定义一个父类Animal,子类 Cat Dog,每个类都有一个run函数(子类重写了父类的run) ,一个接收Animal类型的函数,该函数执行该实例的run函数,此时不管传入cat还是dog类型的,都会正常运行,这就是多态。

开闭原则

  • 对扩展开放(比如再来个Turtoise子类)

  • 对修改封闭(不需要修改依赖Animal类型的函数)

静态语言(如java),传入的对象必须是Animal类型或者它的子类,对于动态语言(如python),传入的对象没有那么严格,只要该对象有一个run方法即可。

获取对象信息

判断类型 函数 或者 类 都可以使用 tpye()函数

对于函数:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True

对于class的继承关系来说,一般使用 isinstance(),可以判断当前实例是否是某一个类或者该类的父类等

实例属性和类属性

实例属性用self.xxx来绑定,类属性直接在class中定义,所有实例都能访问到

面向对象高级编程

使用__slots__

类中使用__slots__=('name','age') 用tuple定义了允许绑定的属性名称,绑定其他属性名会报错

仅对当前类实例起作用,对继承的子类是不起作用的,但如果也定义了 __slots__,则还会继承父类的__slots__

使用@property

对于类的方法,python内置的@property装饰器将方法变成属性调用,比如说score属性,该装饰器本身又创建了另一个@score.setter装饰器,property相当于是getter。

属性的方法名不能和实例变量重名

多重继承

python允许多重设计

MixIn设计:不需要考虑复杂的层次关系,只需要考虑单一继承链,再添加继承额外的功能类,例如:男人类,单一继承链上他属于人类,如果还要为这个男人类添加其它功能,比如抽烟类,那就用MixIn的写法加进来,但是无需你考虑抽烟类和人类有什么关系。

class Man(Person,ChouyanMixIn)

定制类

__str____repr__

print类,定制化输出,需定义 __str__函数,但此后 如果直接敲类变量,还是和之前一样,因为不会调用__str__而是调用__repr__ 解决方法是再定义一个,通常 两者函数代码是一样的,偷懒方法:

1
2
3
4
5
6
class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

__iter__

如果一个类像被用于for ... in ... 需定义__iter__(该方法返回一个迭代对象)和__next__(获取迭代对象的值)

__getitem__

取任意下标元素

__getattr__

调用不存在的属性或方法时,如果不存在会报错,当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值,也可以尝试返回一个函数:

1
2
3
4
5
6
7
8
9
10
class Student(object):

    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr=='score':
            return 99
        if attr=='age':
            return lambda: 25

getattr 默认返回时None,如果只想响应特定的几个属性,要抛出,比如:

1
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

链式调用

1
2
3
4
5
6
7
8
9
10
11
12
class Chain(object):

    def __init__(self, path=''):
        self._path = path

    def __getattr__(self, path):
        return Chain('%s/%s' % (self._path, path))

    def __str__(self):
        return self._path

    __repr__ = __str__
1
2
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'

__call__

调用实例本身,通过callable()函数来判断一个对象是否是可调用对象

使用枚举类

使用常量时,常使用大写,但是其仍然是变量

使用枚举类型 enum.Enum

1
2
3
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

使用 Month.Jan或者 Month['Jan'] 来引用常量,.value来获取常量值,默认从0开始,如果需要更精确地控制枚举类型,可从Enum派生出自定义类

1
2
3
4
5
6
7
8
9
10
11
from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

unique 确保了无重复值

!使用元类

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比如说 定义一个 Hello 类,定义在hello.py模块中:

1
2
3
class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)

当python解释器加载hello.py模块时,会依次执行该模块的所有语句,结果就是动态创建了一个Hello的class对象

type()

type函数可以查看一个类型或变量的类型,

1
2
3
4
from hello import Hello
h=Hello()
print(type(Hello))
print(type(h))

输出

1
2
<class 'type'>
<class 'hello.Hello'>

class类本身类型就是 type,而h是一个实例,所以类型是class Hello。说Hello类是运行时创建的,而创建的方法就是 type,该函数既可以返回类型,也可以创建新的类型,比如 创建Hello2类

1
2
3
4
5
6
7
8
def func(self,name="world"):
    print("hello,%s"%name)

Hello2 = type('Hello2',(object,),dict(hello=func))
h=Hello2()
h.hello()
print(type(Hello2))
print(type(h))
1
2
3
hello,world
<class 'type'>
<class '__main__.Hello2'>

type三个参数:

  • class名称

  • 继承的父类集合(python支持多重继承,如果只有一个父类,别忘了tuple的单元数写法)

  • 方法名称与函数绑定

python解释器遇到class定义时,就是调用type函数创建class

metaclass

type动态创建类,如果要控制类的创建行为,需要使用metaclass

略费劲….暂略

错误、调试和测试

错误处理

程序出错,返回错误码十分不便(正常结果和错误码混在一起,需要大量判断),并且出错还要一级级上报

try-except-finally

所有的错误类型都继承自BaseException 继承层级

  1. 使用except时,有父类的前提下,子类的不会再被捕获

  2. 跨越多层调用,即可以在合适的层次捕获错误即可

如果不捕获错误,python解释器也会打印出错误堆栈,但程序也终止了,可以使用logging模块,将错误信息记录在日志文件中,同时让程序继续运行

错误是一种class,捕获一个错误就是捕获一个该类的实例,可以只定义错误类,选择好继承关系,用 raise语句抛出错误实例

调试

暴力点的 print() 所有可能出错的地方,但可能有很多垃圾信息

assert

使用assert断言,用法如下:

1
2
3
4
5
6
7
def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')
1
2
3
4
$ python err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

可以通过类似 python -O err.py来关闭assert,相当于被替换成了pass

logging

1
2
import logging
logging.basicConfig(level=logging.INFO)

debug info warning error

IDE

单元测试

  • 编写单元测试,需要引入python自带的unittest模块

  • 测试类需要继承 unittest.TestCase

  • 测试方法必须以 test_作为前缀

  • 常用的是断言函数

    • self.assertEqual(v1,v2)
    • self.assertRaises(ErrorName)
  • 运行单元测试:

    • python -m unittest xxx_test 推荐 因为可以批量运行

    • 也可以写个main函数,当作正常脚本来运行

      1
      2
      
      if __name__ == '__main__':
          unittest.main()
      
    • 特殊方法:

      • setUp() 每次测试方法前执行
      • teardown() 每次测试方法后执行

文档测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# mydict2.py
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__':
    import doctest
    doctest.testmod()

doctest 会提取注释中的代码进行测试 测试异常可以用…来省略错误

IO编程

文件读写

操作系统打开文件对象(通称为文件描述符),程序通过操作系统提供的接口进行读写

读文件

读文本文件

1
2
3
4
5
6
7
8
9
f=open('./test.txt','r')
# 读取所有文件内容
f.read()
# 读取1行内容
f.readline()
# 读取所有内容,按行化为列表
f.readlines()
# 执行完读操作 文件描述符必须关闭 因为占资源
f.close()

读写文件 可能会产生 IOError 可以使用 try...finally但是较为繁琐,通常是使用 with语句,会自动调用f.close()

1
2
with open('./test.txt', 'r') as f:
    print(f.read())

大文件直接 read() 读取所有内容,内存可能溢出 可以使用 read(size) 比较保险,配置文件,可以使用 readlines

file-like Object 有个read()方法的对象,除了file,还可以是 字节流 网络流 自定义流等,不要求特定类的继承。

  • 二进制文件 r改成rb
  • 非utf-8的文本文件,可以传入 encoding参数 指定编码
  • errors 参数,收到编码错误后如何处理 简单处理是忽略 errors='ignore'

写文件

必须调用close 有时候内容不会直接写入磁盘,而是在内存中缓存起来,空闲时写入,只有调用close方法,才能保证没有写入的数据全部写入磁盘

标识符:

  • w
  • wb 二进制
  • a 追加

通过反复调用write()来进行写入

StringIO和BytesIOP

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

操作文件和目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看当前目录的绝对路径:
>>> os.path.abspath('.')
'/Users/michael'
# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:
>>> os.path.join('/Users/michael', 'testdir')
'/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.rename('test.txt', 'test.py')
# 删掉文件:
>>> os.remove('test.py')

序列化

变量是存在内存中的,如果需要存储或者传输, 需要进行序列化, 在python中被称为 pickling, 反过来 读到内存中去 称之为 反序列化 unpickling

python 自带的 pickle

1
2
3
import pickle
d = dict(name='Bob', age=20, score=88)
pickle.dumps(d)
1
b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\x03Bob\x94\x8c\x03age\x94K\x14\x8c\x05score\x94KXu.'

dumps 将对象序列化成 bytes, dump 则是将结果写入到file-like object中

1
2
with open('./test.txt', 'wb') as f:
    pickle.dump(d,f)

通过loads方法将bytes反序列化出对象, 通过 load 从file-like Object中读出对象

1
2
3
with open('./test.txt', 'rb') as f:
    d= pickle.load(f)
d

缺点: 不同语言之间不兼容 甚至 不同python版本都不兼容

JSON

JSON的兼容性更好,用法与pickle类似

dict可以直接序列化JSON的{} 如果是自定义的class对象, 需要定义一些函数, 来帮助序列化和反序列化

dumpsdefault参数定义 序列化的方法

loadsobject_hook 定义反序列化的方法

比如定义了一个 Student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(object):
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
        
def student2dict(std):
    return {
        'name': std.name,
        'age': std.age,
        'score': std.score
    }

def dict2student(d):
    return Student(d['name'], d['age'], d['score'])

s = Student('Bob', 20, 88)

使用方法:

1
2
3
4
5
print(json.dumps(s, default=student2dict))
# 对于绝大部分类的示例来说,都会有一个 __dict__ 属性, 用来存储实例变量 可以将任意 class 实例变为 dict
print(json.dumps(s, default=lambda obj: obj.__dict__))

print(json.loads(json_str, object_hook=dict2student))

进程和线程

多任务分成

  • 多进程
  • 多线程
  • 多进程+多线程

多进程

Unix/Linux系统 提供了 fork()系统调用, 该函数调用一次,返回两次,操作系统把当前进程复制一份(子进程),分别在父子进程中返回,父进程返回的是子进程的ID,子进程中返回的是0,通过python的os模块可以进行该系统调用 os.fork()

windows 平台可以使用 multiprocessing模块 该模块提供了一个 Process类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.')

如果需要大量的子进程,可以使用进程池 Pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.')

Pool的默认大小是系统CPU的核数

子进程

有时候子进程是外部进程,需要使用subprocess模块

进程间通信

主要使用 QueuePipes 等实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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()

主要使用的是Queue的put和get方法

多线程

python 提供了 _threadthreading,threading 是对 _thread 的封装,通常使用 threading模块即可

1
2
3
t = threading.Thread(target=func, name='funcname')
t.start()
t.join()

任何进程都会默认启动一个线程,该线程被称之为 主线程,主线程可以启动其他新的线程

Lock

多进程同一个变量会有各自的拷贝数据,但多线程是共享数据

1
2
3
4
5
6
7
8
9
10
11
12
13
balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release(

使用 try...finally来防止死线程导致锁不被释放

如果有多个锁,可能造成死锁,全部挂起

对于每一个进程而言,解释器有一个 GIL锁 global interpreter lock,每个线程执行前,都需要先获取该锁,每执行100条字节码,解释器就会自动释放该锁,让别的线程有机会执行,所以多线程在python中智能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

所以如果想要实现多核任务,需要使用多进程来实现,因为每个进程有各自独立的GIL锁,互不影响

本文由作者按照 CC BY 4.0 进行授权

Linux命令行和shell脚本学习

Django 简单入门