51Testing软件测试论坛
标题: 【转】Python教程 [打印本页]
作者: 八戒你干嘛 时间: 2017-7-6 09:20
标题: 【转】Python教程
本帖最后由 八戒你干嘛 于 2017-7-6 09:28 编辑
获取对象信息
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
使用type()首先,我们来判断对象类型,使用type()函数:
基本类型都可以用type()判断:
>>> type(123)<class 'int'>>>> type('str')<class 'str'>>>> type(None)<type(None) 'NoneType'>如果一个变量指向函数或者类,也可以用type()判断:
>>> type(abs)<class 'builtin_function_or_method'>>>> type(a)<class '__main__.Animal'>但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
>>> type(123)==type(456)True>>> type(123)==intTrue>>> type('abc')==type('123')True>>> type('abc')==strTrue>>> type('abc')==type(123)False判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
>>> import types>>> def fn():... pass...>>> type(fn)==types.FunctionTypeTrue>>> type(abs)==types.BuiltinFunctionTypeTrue>>> type(lambda x: x)==types.LambdaTypeTrue>>> type((x for x in range(10)))==types.GeneratorTypeTrue
作者: 八戒你干嘛 时间: 2017-7-6 09:32
使用isinstance()
对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
我们回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky
那么,isinstance()就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
然后,判断:
>>> isinstance(h, Husky)
True
没有问题,因为h变量指向的就是Husky对象。
再判断:
>>> isinstance(h, Dog)
True
h虽然自身是Husky类型,但由于Husky是从Dog继承下来的,所以,h也还是Dog类型。换句话说,isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
因此,我们可以确信,h还是Animal类型:
>>> isinstance(h, Animal)
True
同理,实际类型是Dog的d也是Animal类型:
>>> isinstance(d, Dog) and isinstance(d, Animal)
True
但是,d不是Husky类型:
>>> isinstance(d, Husky)
False
能用type()判断的基本类型也可以用isinstance()判断:
>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance((1, 2, 3), (list, tuple))
True
作者: 八戒你干嘛 时间: 2017-7-6 09:37
使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100
剩下的都是普通属性或方法,比如lower()返回小写的字符串:
>>> 'ABC'.lower()
'abc'
仅仅把属性和方法列出来是不够的,配合getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
紧接着,可以测试该对象的属性:
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19
如果试图获取不存在的属性,会抛出AttributeError的错误:
>>> getattr(obj, 'z') # 获取属性'z'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404
也可以获得对象的方法:
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81
小结
通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。如果可以直接写:
sum = obj.x + obj.y
就不要写:
sum = getattr(obj, 'x') + getattr(obj, 'y')
一个正确的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。
作者: 乐哈哈yoyo 时间: 2017-7-6 09:41
实例属性和类属性【转】
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
class Student(object):
name = 'Student'
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
从上面的例子可以看出,在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
作者: 悠悠小仙仙 时间: 2017-7-6 09:46
面向对象高级编程【转】
数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。
我们会讨论多重继承、定制类、元类等概念。
使用__slots__
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义class:
class Student(object):
pass
然后,尝试给实例绑定一个属性:
>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
还可以尝试给实例绑定一个方法:
>>> def set_age(self, age): # 定义一个函数作为实例方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
>>> s2 = Student() # 创建新的实例
>>> s2.set_age(25) # 尝试调用方法
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
为了给所有实例都绑定方法,可以给class绑定方法:
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
给class绑定方法后,所有实例均可调用:
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99
通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。
使用__slots__
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
然后,我们试试:
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
>>> class GraduateStudent(Student):
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
作者: 巴黎的灯光下 时间: 2017-7-6 09:48
使用@property
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改:
s = Student()
s.score = 9999
这显然不合逻辑。为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数:
class Student(object):
def get_score(self):
return self._score
def set_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
现在,对任意的Student实例进行操作,就不能随心所欲地设置score了:
>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。
有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!
还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@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
@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2015 - self._birth
上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
小结
@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
练习
请利用@property给一个Screen对象加上width和height属性,以及一个只读属性resolution:
# -*- coding: utf-8 -*-
class Screen(object):
pass
# test:
s = Screen()
s.width = 1024
s.height = 768
print(s.resolution)
assert s.resolution == 786432, '1024 * 768 = %d ?' % s.resolution
Run
作者: 草帽路飞UU 时间: 2017-7-6 09:59
本帖最后由 草帽路飞UU 于 2017-7-6 10:28 编辑
多重继承【转】
继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能。
回忆一下Animal类层次的设计,假设我们要实现以下4种动物:
Dog - 狗狗;
Bat - 蝙蝠;
Parrot - 鹦鹉;
Ostrich - 鸵鸟。
如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次:[attach]107021[/attach]
C:\Users\Administrator\Desktop
但是如果按照“能跑”和“能飞”来归类,我们就应该设计出这样的类的层次:
[attach]107019[/attach]
C:\Users\Administrator\Desktop
如果要把上面的两种分类都包含进来,我们就得设计更多的层次:
哺乳类:能跑的哺乳类,能飞的哺乳类;
鸟类:能跑的鸟类,能飞的鸟类。
这么一来,类的层次就复杂了:
[attach]107020[/attach]
C:\Users\Administrator\Desktop
如果要再增加“宠物类”和“非宠物类”,这么搞下去,类的数量会呈指数增长,很明显这样设计是不行的。
正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:
class Animal(object):
pass
# 大类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
# 各种动物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
现在,我们要给动物再加上Runnable和Flyable的功能,只需要先定义好Runnable和Flyable的类:
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:
class Dog(Mammal, Runnable):
pass
对于需要Flyable功能的动物,就多继承一个Flyable,例如Bat:
class Bat(Mammal, Flyable):
pass
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
MixIn
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。
为了更好地看出继承关系,我们把Runnable和Flyable改为RunnableMixIn和FlyableMixIn。类似的,你还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServer和UDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn和ThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。
比如,编写一个多进程模式的TCP服务,定义如下:
class MyTCPServer(TCPServer, ForkingMixIn):
pass
编写一个多线程模式的UDP服务,定义如下:
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
小结
由于Python允许使用多重继承,因此,MixIn就是一种常见的设计。
只允许单一继承的语言(如Java)不能使用MixIn的设计。
作者: 八戒你干嘛 时间: 2017-7-6 10:32
定制类[转]看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。
__slots__我们已经知道怎么用了,__len__()方法我们也知道是为了能让class作用于len()函数。
除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。
__str__我们先定义一个Student类,打印一个实例:
>>> class Student(object):... def __init__(self, name):... self.name = name...>>> print(Student('Michael'))<__main__.Student object at 0x109afb190>打印出一堆<__main__.Student object at 0x109afb190>,不好看。
怎么才能打印得好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了:
>>> class Student(object):... def __init__(self, name):... self.name = name... def __str__(self):... return 'Student object (name: %s)' % self.name...>>> print(Student('Michael'))Student object (name: Michael)这样打印出来的实例,不但好看,而且容易看出实例内部重要的数据。
但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:
>>> s = Student('Michael')>>> s<__main__.Student object at 0x109afb310>这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以,有个偷懒的写法:
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循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
class Fib(object): def __init__(self): self.a, self.b = 0, 1 # 初始化两个计数器a,b def __iter__(self): return self # 实例本身就是迭代对象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b # 计算下一个值 if self.a > 100000: # 退出循环的条件 raise StopIteration() return self.a # 返回下一个值现在,试试把Fib实例作用于for循环:
>>> for n in Fib():... print(n)...11235...4636875025
作者: 八戒你干嘛 时间: 2017-7-6 10:35
__getitem__Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:
>>> Fib()[5]Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: 'Fib' object does not support indexing要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:
class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a现在,就可以按下标访问数列的任意一项了:
>>> f = Fib()>>> f[0]1>>> f[1]1>>> f[2]2>>> f[3]3>>> f[10]89>>> f[100]573147844013817084101但是list有个神奇的切片方法:
>>> list(range(100))[5:10][5, 6, 7, 8, 9]对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:
class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L现在试试Fib的切片:
>>> f = Fib()>>> f[0:5][1, 1, 2, 3, 5]>>> f[:10][1, 1, 2, 3, 5, 8, 13, 21, 34, 55]但是没有对step参数作处理:
>>> f[:10:2][1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。
此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,例如str。
与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
__getattr__正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:
class Student(object): def __init__(self): self.name = 'Michael'调用name属性,没问题,但是,调用不存在的score属性,就有问题了:
>>> s = Student()>>> print(s.name)Michael>>> print(s.score)Traceback (most recent call last): ...AttributeError: 'Student' object has no attribute 'score'错误信息很清楚地告诉我们,没有找到score这个attribute。
要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:
class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:
>>> s = Student()>>> s.name'Michael'>>> s.score99返回函数也是完全可以的:
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25只是调用方式要变为:
>>> s.age()25注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。
此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25 raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。
举个例子:
现在很多网站都搞REST API,比如新浪微博、豆瓣啥的。
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的__getattr__,我们可以写出一个链式调用:
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__试试:
>>> Chain().status.user.timeline.list'/status/user/timeline/list'这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!
还有些REST API会把参数放到URL中,比如GitHub的API:
GET /users/:user/repos调用时,需要把:user替换为实际用户名。如果我们能写出这样的链式调用:
Chain().users('michael').repos就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。
作者: 八戒你干嘛 时间: 2017-7-6 10:37
__call__一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。
任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。请看示例:
class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name)调用方式如下:
>>> s = Student('Michael')>>> s() # self参数不要传入My name is Michael.__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。
那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:
>>> callable(Student())True>>> callable(max)True>>> callable([1, 2, 3])False>>> callable(None)False>>> callable('str')False通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
小结Python的class允许定义许多定制方法,可以让我们非常方便地生成特定的类。
本节介绍的是最常用的几个定制方法,还有很多可定制的方法
作者: 乐哈哈yoyo 时间: 2017-7-6 10:39
使用枚举类[转]
当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份:
JAN = 1FEB = 2MAR = 3...NOV = 11DEC = 12好处是简单,缺点是类型是int,并且仍然是变量。
更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:
from enum import EnumMonth = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:
for name, member in Month.__members__.items(): print(name, '=>', member, ',', member.value)value属性则是自动赋给成员的int常量,默认从1开始计数。
如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:
from enum import Enum, unique@uniqueclass Weekday(Enum): Sun = 0 # Sun的value被设定为0 Mon = 1 Tue = 2 Wed = 3 Thu = 4 Fri = 5 Sat = 6@unique装饰器可以帮助我们检查保证没有重复值。
访问这些枚举类型可以有若干种方法:
>>> day1 = Weekday.Mon>>> print(day1)Weekday.Mon>>> print(Weekday.Tue)Weekday.Tue>>> print(Weekday['Tue'])Weekday.Tue>>> print(Weekday.Tue.value)2>>> print(day1 == Weekday.Mon)True>>> print(day1 == Weekday.Tue)False>>> print(Weekday(1))Weekday.Mon>>> print(day1 == Weekday(1))True>>> Weekday(7)Traceback (most recent call last): ...ValueError: 7 is not a valid Weekday>>> for name, member in Weekday.__members__.items():... print(name, '=>', member)...Sun => Weekday.SunMon => Weekday.MonTue => Weekday.TueWed => Weekday.WedThu => Weekday.ThuFri => Weekday.FriSat => Weekday.Sat可见,既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。
小结Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。
作者: 悠悠小仙仙 时间: 2017-7-6 10:44
使用元类[转]type()动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个Hello的class,就写一个hello.py模块:
class Hello(object): def hello(self, name='world'): print('Hello, %s.' % name)当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:
>>> from hello import Hello>>> h = Hello()>>> h.hello()Hello, world.>>> print(type(Hello))<class 'type'>>>> print(type(h))<class 'hello.Hello'>type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。
我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。
type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:
>>> def fn(self, name='world'): # 先定义函数... print('Hello, %s.' % name)...>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class>>> h = Hello()>>> h.hello()Hello, world.>>> print(type(Hello))<class 'type'>>>> print(type(h))<class '__main__.Hello'>要创建一个class对象,type()函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
metaclass除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。
metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。
我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:
定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
# metaclass是类的模板,所以必须从`type`类型派生:class ListMetaclass(type): def __new__(cls, name, bases, attrs): attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name, bases, attrs)有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:
class MyList(list, metaclass=ListMetaclass): pass当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()方法接收到的参数依次是:
当前准备创建的类的对象;
类的名字;
类继承的父类集合;
类的方法集合。
测试一下MyList是否可以调用add()方法:
>>> L = MyList()>>> L.add(1)>> L[1]而普通的list没有add()方法:
>>> L2 = list()>>> L2.add(1)Traceback (most recent call last): File "<stdin>", line 1, in <module>AttributeError: 'list' object has no attribute 'add'动态修改有什么意义?直接在MyList定义中写上add()方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。
但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:
class User(Model): # 定义类的属性到列的映射: id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('password')# 创建一个实例:u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')# 保存到数据库:u.save()其中,父类Model和属性类型StringField、IntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。
作者: 悠悠小仙仙 时间: 2017-7-6 10:45
现在,我们就按上面的接口来实现该ORM。
首先来定义Field类,它负责保存数据库表的字段名和字段类型:
class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return '<%s:%s>' % (self.__class__.__name__, self.name)在Field的基础上,进一步定义各种类型的Field,比如StringField,IntegerField等等:
class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name, 'varchar(100)')class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, 'bigint')下一步,就是编写最复杂的ModelMetaclass了:
class ModelMetaclass(type): def __new__(cls, name, bases, attrs): if name=='Model': return type.__new__(cls, name, bases, attrs) print('Found model: %s' % name) mappings = dict() for k, v in attrs.items(): if isinstance(v, Field): print('Found mapping: %s ==> %s' % (k, v)) mappings[k] = v for k in mappings.keys(): attrs.pop(k) attrs['__mappings__'] = mappings # 保存属性和列的映射关系 attrs['__table__'] = name # 假设表名和类名一致 return type.__new__(cls, name, bases, attrs)以及基类Model:
class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kw): super(Model, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Model' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value def save(self): fields = [] params = [] args = [] for k, v in self.__mappings__.items(): fields.append(v.name) params.append('?') args.append(getattr(self, k, None)) sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params)) print('SQL: %s' % sql) print('ARGS: %s' % str(args))当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找metaclass,如果没有找到,就继续在父类Model中查找metaclass,找到了,就使用Model中定义的metaclass的ModelMetaclass来创建User类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass中,一共做了几件事情:
在Model类中,就可以定义各种操作数据库的方法,比如save(),delete(),find(),update等等。
我们实现了save()方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT语句。
编写代码试试:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')u.save()输出如下:
Found model: UserFound mapping: email ==> <StringField:email>Found mapping: password ==> <StringField:password>Found mapping: id ==> <IntegerField:uid>Found mapping: name ==> <StringField:username>SQL: insert into User (password,email,username,id) values (?,?,?,?)ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]可以看到,save()方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。
不到100行代码,我们就通过metaclass实现了一个精简的ORM框架。
小结metaclass是Python中非常具有魔术性的对象,它可以改变类创建时的行为。这种强大的功能使用起来务必小心。
作者: 八戒你干嘛 时间: 2017-7-6 10:59
错误、调试和测试[转]
在程序运行过程中,总会遇到各种各样的错误。
有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。
有的错误是用户输入造成的,比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理。
还有一类错误是完全无法在程序运行过程中预测的,比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。
Python内置了一套异常处理机制,来帮助我们进行错误处理。
此外,我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。
最后,编写测试也很重要。有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们编写的测试。
作者: 八戒你干嘛 时间: 2017-7-6 11:04
错误处理
在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就可以知道是否有错,以及出错的原因。在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1。
用错误码来表示是否出错十分不便,因为函数本身应该返回的正常结果和错误码混在一起,造成调用者必须用大量的代码来判断是否出错:
def foo(): r = some_function() if r==(-1): return (-1) # do something return rdef bar(): r = foo() if r==(-1): print('Error') else: pass一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。
所以高级语言通常都内置了一套try...except...finally...的错误处理机制,Python也不例外。
try让我们用一个例子来看看try的机制:
try: print('try...') r = 10 / 0 print('result:', r)except ZeroDivisionError as e: print('except:', e)finally: print('finally...')print('END')当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。
上面的代码在计算10 / 0时会产生一个除法运算错误:
try...except: division by zerofinally...END从输出可以看到,当错误发生时,后续语句print('result:', r)不会被执行,except由于捕获到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后,程序继续按照流程往下走。
如果把除数0改成2,则执行结果如下:
try...result: 5finally...END由于没有错误发生,所以except语句块不会被执行,但是finally如果有,则一定会被执行(可以没有finally语句)。
你还可以猜测,错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。没错,可以有多个except来捕获不同类型的错误:
try: print('try...') r = 10 / int('a') print('result:', r)except ValueError as e: print('ValueError:', e)except ZeroDivisionError as e: print('ZeroDivisionError:', e)finally: print('finally...')print('END')int()函数可能会抛出ValueError,所以我们用一个except捕获ValueError,用另一个except捕获ZeroDivisionError。
此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:
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')Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
try: foo()except ValueError as e: print('ValueError')except UnicodeError as e: print('UnicodeError')第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。
Python所有的错误都是从BaseException类派生的。
使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo(),foo()调用bar(),结果bar()出错了,这时,只要main()捕获到了,就可以处理:
def foo(s): return 10 / int(s)def bar(s): return foo(s) * 2def main(): try: bar('0') except Exception as e: print('Error:', e) finally: print('finally...')也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try...except...finally的麻烦。
调用堆栈如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py:
# err.py:def foo(s): return 10 / int(s)def bar(s): return foo(s) * 2def main(): bar('0')main()执行,结果如下:
$ python3 err.pyTraceback (most recent call last): File "err.py", line 11, in <module> main() File "err.py", line 9, in main bar('0') File "err.py", line 6, in bar return foo(s) * 2 File "err.py", line 3, in foo return 10 / int(s)ZeroDivisionError: division by zero出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
错误信息第1行:
Traceback (most recent call last):告诉我们这是错误的跟踪信息。
第2~3行:
File "err.py", line 11, in <module> main()调用main()出错了,在代码文件err.py的第11行代码,但原因是第9行:
File "err.py", line 9, in main bar('0')调用bar('0')出错了,在代码文件err.py的第9行代码,但原因是第6行:
File "err.py", line 6, in bar return foo(s) * 2原因是return foo(s) * 2这个语句出错了,但这还不是最终原因,继续往下看:
File "err.py", line 3, in foo return 10 / int(s)原因是return 10 / int(s)这个语句出错了,这是错误产生的源头,因为下面打印了:
ZeroDivisionError: integer division or modulo by zero根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。
作者: 八戒你干嘛 时间: 2017-7-6 11:05
记录错误如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
Python内置的logging模块可以非常容易地记录错误信息:
# err_logging.pyimport loggingdef foo(s): return 10 / int(s)def bar(s): return foo(s) * 2def main(): try: bar('0') except Exception as e: logging.exception(e)main()print('END')同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
$ python3 err_logging.pyERROR:root:division by zeroTraceback (most recent call last): File "err_logging.py", line 13, in main bar('0') File "err_logging.py", line 9, in bar return foo(s) * 2 File "err_logging.py", line 6, in foo return 10 / int(s)ZeroDivisionError: division by zeroEND通过配置,logging还可以把错误记录到日志文件里,方便事后排查。
抛出错误因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:
# err_raise.pyclass FooError(ValueError): passdef foo(s): n = int(s) if n==0: raise FooError('invalid value: %s' % s) return 10 / nfoo('0')执行,可以最后跟踪到我们自己定义的错误:
$ python3 err_raise.py Traceback (most recent call last): File "err_throw.py", line 11, in <module> foo('0') File "err_throw.py", line 8, in foo raise FooError('invalid value: %s' % s)__main__.FooError: invalid value: 0只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
最后,我们来看另一种错误处理的方式:
# err_reraise.pydef foo(s): n = int(s) if n==0: raise ValueError('invalid value: %s' % s) return 10 / ndef bar(): try: foo('0') except ValueError as e: print('ValueError!') raisebar()在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?
其实这种错误处理方式不但没病,而且相当常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。
raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型:
try: 10 / 0except ZeroDivisionError: raise ValueError('input error!')只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError。
小结Python内置的try...except...finally用来处理错误十分方便。出错时,会分析错误信息并定位错误发生的代码位置才是最关键的。
程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。
作者: 八戒你干嘛 时间: 2017-7-6 11:09
本帖最后由 八戒你干嘛 于 2017-7-6 11:20 编辑
调试
程序能一次写完并正常运行的概率很小,基本不超过1%。总会有各种各样的bug需要修正。有的bug很简单,看看错误信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是错误的,因此,需要一整套调试程序的手段来修复bug。
第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看:
def foo(s): n = int(s) print('>>> n = %d' % n) return 10 / ndef main(): foo('0')main()执行后在输出中查找打印的变量值:
$ python3 err.py>>> n = 0Traceback (most recent call last): ...ZeroDivisionError: integer division or modulo by zero用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。
断言凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:
def foo(s): n = int(s) assert n != 0, 'n is zero!' return 10 / ndef main(): foo('0')assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
$ python3 err.pyTraceback (most recent call last): ...AssertionError: n is zero!程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
$ python3 -O err.pyTraceback (most recent call last): ...ZeroDivisionError: division by zero关闭后,你可以把所有的assert语句当成pass来看。
logging把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:
import loggings = '0'n = int(s)logging.info('n = %d' % n)print(10 / n)logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?
别急,在import logging之后添加一行配置再试试:
import logginglogging.basicConfig(level=logging.INFO)看到输出了:
$ python3 err.pyINFO:root:n = 0Traceback (most recent call last): File "err.py", line 8, in <module> print(10 / n)ZeroDivisionError: division by zero这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
pdb第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。我们先准备好程序:
# err.pys = '0'n = int(s)print(10 / n)然后启动:
$ python3 -m pdb err.py> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()-> s = '0'以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = '0'。输入命令l来查看代码:
(Pdb) l 1 # err.py 2 -> s = '0' 3 n = int(s) 4 print(10 / n)输入命令n可以单步执行代码:
(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()-> n = int(s)(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()-> print(10 / n)任何时候都可以输入命令p 变量名来查看变量:
(Pdb) p s'0'(Pdb) p n0输入命令q结束调试,退出程序:
(Pdb) q这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。
pdb.set_trace()这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:
# err.pyimport pdbs = '0'n = int(s)pdb.set_trace() # 运行到这里会自动暂停print(10 / n)运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行:
$ python3 err.py > /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()-> print(10 / n)(Pdb) p n0(Pdb) cTraceback (most recent call last): File "err.py", line 7, in <module> print(10 / n)ZeroDivisionError: division by zero这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪去。
IDE如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。目前比较好的Python IDE有PyCharm:
另外,Eclipes加上pydev插件也可以调试Python程序。
小结写程序最痛苦的事情莫过于调试,程序往往会以你意想不到的流程来运行,你期待执行的语句其实根本没有执行,这时候,就需要调试了。
虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。
作者: 八戒你干嘛 时间: 2017-7-6 11:11
单元测试
如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs(),我们可以编写出以下几个测试用例:
输入正数,比如1、1.2、0.99,期待返回值与输入相同;
输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
输入0,期待返回0;
输入非数值类型,比如None、[]、{},期待抛出TypeError。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:
>>> d = Dict(a=1, b=2)>>> d['a']1>>> d.a1mydict.py代码如下:
class Dict(dict): def __init__(self, **kw): super().__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为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:
import unittestfrom mydict import Dictclass TestDict(unittest.TestCase): def test_init(self): d = Dict(a=1, b='test') self.assertEqual(d.a, 1) self.assertEqual(d.b, 'test') self.assertTrue(isinstance(d, dict)) def test_key(self): d = Dict() d['key'] = 'value' self.assertEqual(d.key, 'value') def test_attr(self): d = Dict() d.key = 'value' self.assertTrue('key' in d) self.assertEqual(d['key'], 'value') def test_keyerror(self): d = Dict() with self.assertRaises(KeyError): value = d['empty'] def test_attrerror(self): d = Dict() with self.assertRaises(AttributeError): value = d.empty编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。
以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。
对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual():
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError:
with self.assertRaises(KeyError): value = d['empty']而通过d.empty访问不存在的key时,我们期待抛出AttributeError:
with self.assertRaises(AttributeError): value = d.empty运行单元测试一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:
if __name__ == '__main__': unittest.main()这样就可以把mydict_test.py当做正常的python脚本运行:
$ python3 mydict_test.py另一种方法是在命令行通过参数-m unittest直接运行单元测试:
$ python3 -m unittest mydict_test.....----------------------------------------------------------------------Ran 5 tests in 0.000sOK这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
setUp与tearDown可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()和tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase): def setUp(self): print('setUp...') def tearDown(self): print('tearDown...')可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...和tearDown...。
小结单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。
作者: 八戒你干嘛 时间: 2017-7-6 11:14
文档测试
如果你经常阅读Python的官方文档,可以看到很多文档都有示例代码。比如
re模块就带了很多示例代码:
>>> import re>>> m = re.search('(?<=abc)def', 'abcdef')>>> m.group(0)'def'可以把这些示例代码在Python的交互式环境下输入并执行,结果与文档中的示例代码显示的一致。
这些代码与其他说明可以写在注释中,然后,由一些工具来自动生成文档。既然这些代码本身就可以粘贴出来直接运行,那么,可不可以自动执行写在注释中的这些代码呢?
答案是肯定的。
当我们编写注释时,如果写上这样的注释:
def abs(n): ''' Function to get absolute value of number. Example: >>> abs(1) 1 >>> abs(-1) 1 >>> abs(0) 0 ''' return n if n >= 0 else (-n)无疑更明确地告诉函数的调用者该函数的期望输入和输出。
并且,Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...表示中间一大段烦人的输出。
让我们用doctest来测试上次编写的Dict类:
# mydict2.pyclass 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] = valueif __name__=='__main__': import doctest doctest.testmod()运行python3 mydict2.py:
$ python3 mydict2.py什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()方法注释掉,再运行就会报错:
$ python3 mydict2.py**********************************************************************File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.DictFailed example: d1.xException raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'x'**********************************************************************File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.DictFailed example: d2.cException raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'c'**********************************************************************1 items had failures: 2 of 9 in __main__.Dict***Test Failed*** 2 failures.注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。
练习对函数fact(n)编写doctest并执行:
# -*- coding: utf-8 -*-def fact(n): ''' ''' if n < 1: raise ValueError() if n == 1: return 1 return n * fact(n - 1)if __name__ == '__main__': import doctest doctest.testmod() Run小结doctest非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest。
作者: 八戒你干嘛 时间: 2017-7-6 11:29
IO编程[转]
IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。
IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。
由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。
同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。
你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO。
很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。我们后面会详细讨论Python的IO编程接口。
注意,本章的IO编程都是同步模式,异步IO由于复杂度太高,后续涉及到服务器端程序开发时我们再讨论。
作者: 八戒你干嘛 时间: 2017-7-6 11:33
文件读写
读写文件是最常见的IO操作。Python内置了读写文件的函数,用法和C是兼容的。
读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。
读文件要以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和标示符:
>>> f = open('/Users/michael/test.txt', 'r')标示符'r'表示读,这样,我们就成功地打开了一个文件。
如果文件不存在,open()函数就会抛出一个IOError的错误,并且给出错误码和详细的信息告诉你文件不存在:
>>> f=open('/Users/michael/notfound.txt', 'r')Traceback (most recent call last): File "<stdin>", line 1, in <module>FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'如果文件打开成功,接下来,调用read()方法可以一次读取文件的全部内容,Python把内容读到内存,用一个str对象表示:
>>> f.read()'Hello, world!'最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的:
>>> f.close()由于文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,我们可以使用try ... finally来实现:
try: f = open('/path/to/file', 'r') print(f.read())finally: if f: f.close()但是每次都这么写实在太繁琐,所以,Python引入了with语句来自动帮我们调用close()方法:
with open('/path/to/file', 'r') as f: print(f.read())这和前面的try ... finally是一样的,但是代码更佳简洁,并且不必调用f.close()方法。
调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便:
for line in f.readlines(): print(line.strip()) # 把末尾的'\n'删掉file-like Object像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。
StringIO就是在内存中创建的file-like Object,常用作临时缓冲。
二进制文件前面讲的默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:
>>> f = open('/Users/michael/test.jpg', 'rb')>>> f.read()b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六进制表示的字节字符编码要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')>>> f.read()'测试'遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')写文件写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符'w'或者'wb'表示写文本文件或写二进制文件:
>>> f = open('/Users/michael/test.txt', 'w')>>> f.write('Hello, world!')>>> f.close()你可以反复调用write()来写入文件,但是务必要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用with语句来得保险:
with open('/Users/michael/test.txt', 'w') as f: f.write('Hello, world!')要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。
小结在Python中,文件读写是通过open()函数打开的文件对象完成的。使用with语句操作文件IO是个好习惯。
作者: 八戒你干嘛 时间: 2017-7-6 11:43
StringIO和BytesIO
StringIO很多时候,数据读写不一定是文件,也可以在内存中读写。
StringIO顾名思义就是在内存中读写str。
要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:
>>> from io import StringIO>>> f = StringIO()>>> f.write('hello')5>>> f.write(' ')1>>> f.write('world!')6>>> print(f.getvalue())hello world!getvalue()方法用于获得写入后的str。
要读取StringIO,可以用一个str初始化StringIO,然后,像读文件一样读取:
>>> from io import StringIO>>> f = StringIO('Hello!\nHi!\nGoodbye!')>>> while True:... s = f.readline()... if s == '':... break... print(s.strip())...Hello!Hi!Goodbye!BytesIOStringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:
>>> from io import BytesIO>>> f = BytesIO()>>> f.write('中文'.encode('utf-8'))6>>> print(f.getvalue())b'\xe4\xb8\xad\xe6\x96\x87'请注意,写入的不是str,而是经过UTF-8编码的bytes。
和StringIO类似,可以用一个bytes初始化BytesIO,然后,像读文件一样读取:
>>> from io import BytesIO>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')>>> f.read()b'\xe4\xb8\xad\xe6\x96\x87'小结StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。
作者: 八戒你干嘛 时间: 2017-7-6 11:47
操作文件和目录
如果我们要操作文件、目录,可以在命令行下面输入操作系统提供的各种命令来完成。比如dir、cp等命令。
如果要在Python程序中执行这些目录和文件的操作怎么办?其实操作系统提供的命令只是简单地调用了操作系统提供的接口函数,Python内置的os模块也可以直接调用操作系统提供的接口函数。
打开Python交互式命令行,我们来看看如何使用os模块的基本功能:
>>> import os>>> os.name # 操作系统类型'posix'如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。
要获取详细的系统信息,可以调用uname()函数:
>>> os.uname()posix.uname_result(sysname='Darwin', nodename='MichaelMacPro.local', release='14.3.0', version='Darwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64', machine='x86_64')注意uname()函数在Windows上不提供,也就是说,os模块的某些函数是跟操作系统相关的。
环境变量在操作系统中定义的环境变量,全部保存在os.environ这个变量中,可以直接查看:
>>> os.environenviron({'VERSIONER_PYTHON_PREFER_32_BIT': 'no', 'TERM_PROGRAM_VERSION': '326', 'LOGNAME': 'michael', 'USER': 'michael', 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin', ...})要获取某个环境变量的值,可以调用os.environ.get('key'):
>>> os.environ.get('PATH')'/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin'>>> os.environ.get('x', 'default')'default'操作文件和目录操作文件和目录的函数一部分放在os模块中,一部分放在os.path模块中,这一点要注意一下。查看、创建和删除目录可以这么调用:
# 查看当前目录的绝对路径:>>> 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.join()函数,这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下,os.path.join()返回这样的字符串:
part-1/part-2而Windows下会返回这样的字符串:
part-1\part-2同样的道理,要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名:
>>> os.path.split('/Users/michael/testdir/file.txt')('/Users/michael/testdir', 'file.txt')os.path.splitext()可以直接让你得到文件扩展名,很多时候非常方便:
>>> os.path.splitext('/path/to/file.txt')('/path/to/file', '.txt')这些合并、拆分路径的函数并不要求目录和文件要真实存在,它们只对字符串进行操作。
文件操作使用下面的函数。假定当前目录下有一个test.txt文件:
# 对文件重命名:>>> os.rename('test.txt', 'test.py')# 删掉文件:>>> os.remove('test.py')但是复制文件的函数居然在os模块中不存在!原因是复制文件并非由操作系统提供的系统调用。理论上讲,我们通过上一节的读写文件可以完成文件复制,只不过要多写很多代码。
幸运的是shutil模块提供了copyfile()的函数,你还可以在shutil模块中找到很多实用函数,它们可以看做是os模块的补充。
最后看看如何利用Python的特性来过滤文件。比如我们要列出当前目录下的所有目录,只需要一行代码:
>>> [x for x in os.listdir('.') if os.path.isdir(x)]['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]要列出所有的.py文件,也只需一行代码:
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']是不是非常简洁?
小结Python的os模块封装了操作系统的目录和文件操作,要注意这些函数有的在os模块中,有的在os.path模块中。
练习
作者: 八戒你干嘛 时间: 2017-7-6 11:49
序列化
在程序运行的过程中,所有的变量都是在内存中,比如,定义一个dict:
d = dict(name='Bob', age=20, score=88)可以随时修改变量,比如把name改成'Bill',但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果没有把修改后的'Bill'存储到磁盘上,下次重新运行程序,变量又被初始化为'Bob'。
我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
Python提供了pickle模块来实现序列化。
首先,我们尝试把一个对象序列化并写入文件:
>>> import pickle>>> d = dict(name='Bob', age=20, score=88)>>> pickle.dumps(d)b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:
>>> f = open('dump.txt', 'wb')>>> pickle.dump(d, f)>>> f.close()看看写入的dump.txt文件,一堆乱七八糟的内容,这些都是Python保存的对象内部信息。
当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:
>>> f = open('dump.txt', 'rb')>>> d = pickle.load(f)>>> f.close()>>> d{'age': 20, 'score': 88, 'name': 'Bob'}变量的内容又回来了!
当然,这个变量和原来的变量是完全不相干的对象,它们只是内容相同而已。
Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。
JSON如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。
JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
JSON类型 | Python类型 |
{} | dict |
[] | list |
"string" | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON:
>>> import json>>> d = dict(name='Bob', age=20, score=88)>>> json.dumps(d)'{"age": 20, "score": 88, "name": "Bob"}'dumps()方法返回一个str,内容就是标准的JSON。类似的,dump()方法可以直接把JSON写入一个file-like Object。
要把JSON反序列化为Python对象,用loads()或者对应的load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'>>> json.loads(json_str){'age': 20, 'score': 88, 'name': 'Bob'}由于JSON标准规定JSON编码是UTF-8,所以我们总是能正确地在Python的str与JSON的字符串之间转换。
JSON进阶Python的dict对象可以直接序列化为JSON的{},不过,很多时候,我们更喜欢用class表示对象,比如定义Student类,然后序列化:
import jsonclass Student(object): def __init__(self, name, age, score): self.name = name self.age = age self.score = scores = Student('Bob', 20, 88)print(json.dumps(s))运行代码,毫不留情地得到一个TypeError:
Traceback (most recent call last): ...TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable错误的原因是Student对象不是一个可序列化为JSON的对象。
如果连class的实例对象都无法序列化为JSON,这肯定不合理!
别急,我们仔细看看dumps()方法的参数列表,可以发现,除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数。
这些可选参数就是让我们来定制JSON序列化。前面的代码之所以无法把Student类实例序列化为JSON,是因为默认情况下,dumps()方法不知道如何将Student实例变为一个JSON的{}对象。
可选参数default就是把任意一个对象变成一个可序列为JSON的对象,我们只需要为Student专门写一个转换函数,再把函数传进去即可:
def student2dict(std): return { 'name': std.name, 'age': std.age, 'score': std.score }这样,Student实例首先被student2dict()函数转换成dict,然后再被顺利序列化为JSON:
>>> print(json.dumps(s, default=student2dict)){"age": 20, "name": "Bob", "score": 88}不过,下次如果遇到一个Teacher类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class的实例变为dict:
print(json.dumps(s, default=lambda obj: obj.__dict__))因为通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slots__的class。
同样的道理,如果我们要把JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后,我们传入的object_hook函数负责把dict转换为Student实例:
def dict2student(d): return Student(d['name'], d['age'], d['score'])运行结果如下:
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'>>> print(json.loads(json_str, object_hook=dict2student))<__main__.Student object at 0x10cd3c190>打印出的是反序列化的Student实例对象。
小结Python语言特定的序列化模块是pickle,但如果要把序列化搞得更通用、更符合Web标准,就可以使用json模块。
json模块的dumps()和loads()函数是定义得非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是,当默认的序列化或反序列机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化或反序列化的规则,既做到了接口简单易用,又做到了充分的扩展性和灵活性。
作者: 八戒你干嘛 时间: 2017-7-6 11:51
进程和线程【转】
很多同学都听说过,现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。
我们前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
有两种解决方案:
一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
总结一下就是,多任务的实现有3种方式:
同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。
因为复杂度高,调试困难,所以,不是迫不得已,我们也不想编写多任务。但是,有很多时候,没有多任务还真不行。想想在电脑上看电影,就必须由一个线程播放视频,另一个线程播放音频,否则,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。
Python既支持多进程,又支持多线程,我们会讨论如何编写这两种多任务程序。
小结线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。
作者: 八戒你干嘛 时间: 2017-7-6 11:56
多进程[转]
要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识。
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
import osprint('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))运行结果如下:
Process (876) start...I (876) just created a child process (877).I am child process (877) and my parent is 876.由于Windows没有fork调用,上面的代码在Windows上无法运行。由于Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的,推荐大家用Mac学Python!
有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
multiprocessing如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
from multiprocessing import Processimport 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.')执行结果如下:
Parent process 928.Process will start.Run child process test (929)...Process end.创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。
join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
Pool如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Poolimport os, time, randomdef 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.代码解读:
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:
p = Pool(5)就可以同时跑5个进程。
由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
作者: 八戒你干嘛 时间: 2017-7-6 11:58
子进程很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
import subprocessprint('$ nslookup www.python.org')r = subprocess.call(['nslookup', 'www.python.org'])print('Exit code:', r)运行结果:
$ nslookup www.python.orgServer: 192.168.19.4Address: 192.168.19.4#53Non-authoritative answer:www.python.org canonical name = python.map.fastly.net.Name: python.map.fastly.netAddress: 199.27.79.223Exit code: 0如果子进程还需要输入,则可以通过communicate()方法输入:
import subprocessprint('$ nslookup')p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)output, err = p.communicate(b'set q=mx\npython.org\nexit\n')print(output.decode('utf-8'))print('Exit code:', p.returncode)上面的代码相当于在命令行执行命令nslookup,然后手动输入:
set q=mxpython.orgexit运行结果如下:
$ nslookupServer: 192.168.19.4Address: 192.168.19.4#53Non-authoritative answer:python.org mail exchanger = 50 mail.python.org.Authoritative answers can be found from:mail.python.org internet address = 82.94.164.166mail.python.org has AAAA address 2001:888:2000:d::a6Exit code: 0进程间通信Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
from multiprocessing import Process, Queueimport 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: 50563Put A to queue...Process to read: 50564Get A from queue.Put B to queue...Get B from queue.Put C to queue...Get C from queue.在Unix/Linux下,multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节。由于Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。
小结在Unix/Linux下,可以使用fork()调用实现多进程。
要实现跨平台的多进程,可以使用multiprocessing模块。
进程间通信是通过Queue、Pipes等实现的。
作者: 八戒你干嘛 时间: 2017-7-6 12:00
多线程[转]
多任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
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 >>> 1thread LoopThread >>> 2thread LoopThread >>> 3thread LoopThread >>> 4thread LoopThread >>> 5thread LoopThread ended.thread MainThread ended.由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
Lock多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
来看看多个线程同时操作一个变量怎么把内容给改乱了:
import time, threading# 假定这是你的银行存款:balance = 0def change_it(n): # 先存后取,结果应该为0: global balance balance = balance + n balance = balance - ndef run_thread(n): for i in range(100000): change_it(n)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)我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n也分两步:
- 计算balance + n,存入临时变量中;
- 将临时变量的值赋给balance。
也就是可以看成:
x = balance + nbalance = x由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:
初始值 balance = 0t1: x1 = balance + 5 # x1 = 0 + 5 = 5t1: balance = x1 # balance = 5t1: x1 = balance - 5 # x1 = 5 - 5 = 0t1: balance = x1 # balance = 0t2: x2 = balance + 8 # x2 = 0 + 8 = 8t2: balance = x2 # balance = 8t2: x2 = balance - 8 # x2 = 8 - 8 = 0t2: balance = x2 # balance = 0结果 balance = 0但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
初始值 balance = 0t1: x1 = balance + 5 # x1 = 0 + 5 = 5t2: x2 = balance + 8 # x2 = 0 + 8 = 8t2: balance = x2 # balance = 8t1: balance = x1 # balance = 5t1: x1 = balance - 5 # x1 = 5 - 5 = 0t1: balance = x1 # balance = 0t2: x2 = balance - 8 # x2 = 0 - 8 = -8t2: balance = x2 # balance = -8结果 balance = -8究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
balance = 0lock = threading.Lock()def run_thread(n): for i in range(100000): # 先要获取锁: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了一定要释放锁: lock.release()当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
多核CPU如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。
如果写一个死循环的话,会出现什么情况呢?
打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。
我们可以监控到一个死循环线程会100%占用一个CPU。
如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。
要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。
试试用Python写个死循环:
import threading, multiprocessingdef loop(): x = 0 while True: x = x ^ 1for i in range(multiprocessing.cpu_count()): t = threading.Thread(target=loop) t.start()启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为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中就是一个美丽的梦。
作者: 八戒你干嘛 时间: 2017-7-6 13:12
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name): std = Student(name) # std是局部变量,但是每个函数都要用它,因此必须传进去: do_task_1(std) do_task_2(std)def do_task_1(std): do_subtask_1(std) do_subtask_2(std)def do_task_2(std): do_subtask_2(std) do_subtask_2(std)每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。
如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?
global_dict = {}def std_thread(name): std = Student(name) # 把std放到全局变量global_dict中: global_dict[threading.current_thread()] = std do_task_1() do_task_2()def do_task_1(): # 不传入std,而是根据当前线程查找: std = global_dict[threading.current_thread()] ...def do_task_2(): # 任何函数都可以查找出当前线程的std变量: std = global_dict[threading.current_thread()] ...这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。
有没有更简单的方式?
ThreadLocal应运而生,不用查找dict,ThreadLocal帮你自动做这件事:
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)全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
小结一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
作者: 八戒你干嘛 时间: 2017-7-6 13:14
进程 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现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs. IO密集型是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
作者: 八戒你干嘛 时间: 2017-7-6 13:17
分布式进程
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
举个例子:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。
我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:
# task_master.pyimport random, time, queuefrom multiprocessing.managers import BaseManager# 发送任务的队列:task_queue = queue.Queue()# 接收结果的队列:result_queue = queue.Queue()# 从BaseManager继承的QueueManager:class QueueManager(BaseManager): pass# 把两个Queue都注册到网络上, callable参数关联了Queue对象:QueueManager.register('get_task_queue', callable=lambda: task_queue)QueueManager.register('get_result_queue', callable=lambda: result_queue)# 绑定端口5000, 设置验证码'abc':manager = QueueManager(address=('', 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.')请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。
然后,在另一台机器上启动任务进程(本机上启动也可以):
# task_worker.pyimport time, sys, queuefrom 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.')任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。
现在,可以试试分布式进程的工作效果了。先启动task_master.py服务进程:
$ python3 task_master.py Put task 3411...Put task 1605...Put task 1398...Put task 4729...Put task 5300...Put task 7471...Put task 68...Put task 4219...Put task 339...Put task 7866...Try get results...task_master.py进程发送完任务后,开始等待result队列的结果。现在启动task_worker.py进程:
$ python3 task_worker.pyConnect to server 127.0.0.1...run task 3411 * 3411...run task 1605 * 1605...run task 1398 * 1398...run task 4729 * 4729...run task 5300 * 5300...run task 7471 * 7471...run task 68 * 68...run task 4219 * 4219...run task 339 * 339...run task 7866 * 7866...worker exit.task_worker.py进程结束,在task_master.py进程中会继续打印出结果:
Result: 3411 * 3411 = 11634921Result: 1605 * 1605 = 2576025Result: 1398 * 1398 = 1954404Result: 4729 * 4729 = 22363441Result: 5300 * 5300 = 28090000Result: 7471 * 7471 = 55815841Result: 68 * 68 = 4624Result: 4219 * 4219 = 17799961Result: 339 * 339 = 114921Result: 7866 * 7866 = 61873956这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。
Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:
[attach]107038[/attach]
而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue。
authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py的authkey和task_master.py的authkey不一致,肯定连接不上。
小结Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。
作者: 八戒你干嘛 时间: 2017-7-6 13:20
正则表达式[转]
字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址,虽然可以编程提取@前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。
正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。
所以我们判断一个字符串是否是合法的Email的方法是:
创建一个匹配Email的正则表达式;
用该正则表达式去匹配用户的输入来判断是否合法。
因为正则表达式也是用字符串表示的,所以,我们要首先了解如何用字符来描述字符。
在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:
.可以匹配任意字符,所以:
- 'py.'可以匹配'pyc'、'pyo'、'py!'等等。
要匹配变长的字符,在正则表达式中,用*表示任意个字符(包括0个),用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n-m个字符:
来看一个复杂的例子:\d{3}\s+\d{3,8}。
我们来从左到右解读一下:
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配'010-12345'这样的号码呢?由于'-'是特殊字符,在正则表达式中,要用'\'转义,所以,上面的正则是\d{3}\-\d{3,8}。
但是,仍然无法匹配'010 - 12345',因为带有空格。所以我们需要更复杂的匹配方式。
进阶要做更精确地匹配,可以用[]表示范围,比如:
[0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;
[0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','Py3000'等等;
[a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;
[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
A|B可以匹配A或B,所以(P|p)ython可以匹配'Python'或者'python'。
^表示行的开头,^\d表示必须以数字开头。
$表示行的结束,\d$表示必须以数字结束。
你可能注意到了,py也可以匹配'python',但是加上^py$就变成了整行匹配,就只能匹配'py'了。
re模块有了准备知识,我们就可以在Python中使用正则表达式了。Python提供re模块,包含所有正则表达式的功能。由于Python的字符串本身也用\转义,所以要特别注意:
s = 'ABC\\-001' # Python的字符串# 对应的正则表达式字符串变成:# 'ABC\-001'因此我们强烈建议使用Python的r前缀,就不用考虑转义的问题了:
s = r'ABC\-001' # Python的字符串# 对应的正则表达式字符串不变:# 'ABC\-001'先看看如何判断正则表达式是否匹配:
>>> 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')>>>match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。常见的判断方法就是:
test = '用户输入的字符串'if re.match(r'正则表达式', test): print('ok')else: print('failed')切分字符串用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:
>>> 'a b c'.split(' ')['a', 'b', '', '', 'c']嗯,无法识别连续的空格,用正则表达式试试:
>>> re.split(r'\s+', 'a b c')['a', 'b', 'c']无论多少个空格都可以正常分割。加入,试试:
>>> re.split(r'[\s\,]+', 'a,b, c d')['a', 'b', 'c', 'd']再加入;试试:
>>> re.split(r'[\s\,\;]+', 'a,b;; c d')['a', 'b', 'c', 'd']如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。
分组除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:
^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
>>> 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'如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。
注意到group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。
提取子串非常有用。来看一个更凶残的例子:
>>> t = '19:05:30'>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)>>> m.groups()('19', '05', '30')这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:
'^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$'对于'2-30','4-31'这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。
贪婪匹配最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:
>>> re.match(r'^(\d+)(0*)$', '102300').groups()('102300', '')由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()('1023', '00')编译当我们在Python中使用正则表达式时,re模块内部会干两件事情:
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:
>>> 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')编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。
小结正则表达式非常强大,要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容,可以写一本厚厚的书了。如果你经常遇到正则表达式的问题,你可能需要一本正则表达式的参考书。
练习请尝试写一个验证Email地址的正则表达式。版本一应该可以验证出类似的Email:
someone@gmail.combill.gates@microsoft.com版本二可以验证并提取出带名字的Email地址:
<Tom Paris> tom@voyager.org
作者: 八戒你干嘛 时间: 2017-7-6 13:24
常用内建模块【转】
Python之所以自称“batteries included”,就是因为内置了许多非常有用的模块,无需额外安装和配置,即可直接使用。
本章将介绍一些常用的内建模块。
datetime
datetime是Python处理日期和时间的标准库。
获取当前日期和时间我们先看如何获取当前日期和时间:
>>> from datetime import datetime>>> now = datetime.now() # 获取当前datetime>>> print(now)2015-05-18 16:28:07.198690>>> print(type(now))<class 'datetime.datetime'>注意到datetime是模块,datetime模块还包含一个datetime类,通过from datetime import datetime导入的才是datetime这个类。
如果仅导入import datetime,则必须引用全名datetime.datetime。
datetime.now()返回当前日期和时间,其类型是datetime。
作者: 八戒你干嘛 时间: 2017-7-6 13:26
获取指定日期和时间要指定某个日期和时间,我们直接用参数构造一个datetime:
>>> from datetime import datetime>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime>>> print(dt)2015-04-19 12:20:00datetime转换为timestamp在计算机中,时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp。
你可以认为:
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00对应的北京时间是:
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00可见timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp表示的,因为全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。
把一个datetime类型转换为timestamp只需要简单调用timestamp()方法:
>>> from datetime import datetime>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime>>> dt.timestamp() # 把datetime转换为timestamp1429417200.0注意Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。
某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。
timestamp转换为datetime要把timestamp转换为datetime,使用datetime提供的fromtimestamp()方法:
>>> from datetime import datetime>>> t = 1429417200.0>>> print(datetime.fromtimestamp(t))2015-04-19 12:20:00注意到timestamp是一个浮点数,它没有时区的概念,而datetime是有时区的。上述转换是在timestamp和本地时间做转换。
本地时间是指当前操作系统设定的时区。例如北京时区是东8区,则本地时间:
2015-04-19 12:20:00实际上就是UTC+8:00时区的时间:
2015-04-19 12:20:00 UTC+8:00而此刻的格林威治标准时间与北京时间差了8小时,也就是UTC+0:00时区的时间应该是:
2015-04-19 04:20:00 UTC+0:00timestamp也可以直接被转换到UTC标准时区的时间:
>>> from datetime import datetime>>> t = 1429417200.0>>> print(datetime.fromtimestamp(t)) # 本地时间2015-04-19 12:20:00>>> print(datetime.utcfromtimestamp(t)) # UTC时间2015-04-19 04:20:00str转换为datetime很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先必须把str转换为datetime。转换方法是通过datetime.strptime()实现,需要一个日期和时间的格式化字符串:
>>> from datetime import datetime>>> cday = datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S')>>> print(cday)2015-06-01 18:19:59字符串'%Y-%m-%d %H:%M:%S'规定了日期和时间部分的格式。详细的说明请参考Python文档。 注意转换后的datetime是没有时区信息的。
datetime转换为str如果已经有了datetime对象,要把它格式化为字符串显示给用户,就需要转换为str,转换方法是通过strftime()实现的,同样需要一个日期和时间的格式化字符串:
>>> from datetime import datetime>>> now = datetime.now()>>> print(now.strftime('%a, %b %d %H:%M'))Mon, May 05 16:28datetime加减对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+和-运算符,不过需要导入timedelta这个类:
>>> from datetime import datetime, timedelta>>> now = datetime.now()>>> nowdatetime.datetime(2015, 5, 18, 16, 57, 3, 540997)>>> now + timedelta(hours=10)datetime.datetime(2015, 5, 19, 2, 57, 3, 540997)>>> now - timedelta(days=1)datetime.datetime(2015, 5, 17, 16, 57, 3, 540997)>>> now + timedelta(days=2, hours=12)datetime.datetime(2015, 5, 21, 4, 57, 3, 540997)可见,使用timedelta你可以很容易地算出前几天和后几天的时刻。
作者: 八戒你干嘛 时间: 2017-7-6 13:26
本地时间转换为UTC时间本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。
一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区:
>>> from datetime import datetime, timedelta, timezone>>> tz_utc_8 = timezone(timedelta(hours=8)) # 创建时区UTC+8:00>>> now = datetime.now()>>> nowdatetime.datetime(2015, 5, 18, 17, 2, 10, 871012)>>> dt = now.replace(tzinfo=tz_utc_8) # 强制设置为UTC+8:00>>> dtdatetime.datetime(2015, 5, 18, 17, 2, 10, 871012, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTC+8:00时区。
时区转换我们可以先通过utcnow()拿到当前的UTC时间,再转换为任意时区的时间:
# 拿到UTC时间,并强制设置时区为UTC+0:00:>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)>>> print(utc_dt)2015-05-18 09:05:12.377316+00:00# astimezone()将转换时区为北京时间:>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))>>> print(bj_dt)2015-05-18 17:05:12.377316+08:00# astimezone()将转换时区为东京时间:>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt)2015-05-18 18:05:12.377316+09:00# astimezone()将bj_dt转换时区为东京时间:>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt2)2015-05-18 18:05:12.377316+09:00时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。
利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。
注:不是必须从UTC+0:00时区转换到其他时区,任何带时区的datetime都可以正确转换,例如上述bj_dt到tokyo_dt的转换。
小结datetime表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。
如果要存储datetime,最佳方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关。
练习假设你获取了用户输入的日期和时间如2015-1-21 9:01:30,以及一个时区信息如UTC+5:00,均是str,请编写一个函数将其转换为timestamp:
# -*- coding:utf-8 -*-import refrom datetime import datetime, timezone, timedeltadef to_timestamp(dt_str, tz_str):# 测试:t1 = to_timestamp('2015-6-1 08:10:30', 'UTC+7:00')assert t1 == 1433121030.0, t1t2 = to_timestamp('2015-5-31 16:10:30', 'UTC-09:00')assert t2 == 1433121030.0, t2print('Pass') Run
作者: 八戒你干嘛 时间: 2017-7-6 13:30
collections[转]
collections是Python内建的一个集合模块,提供了许多有用的集合类。
namedtuple我们知道tuple可以表示不变集合,例如,一个点的二维坐标就可以表示成:
>>> p = (1, 2)但是,看到(1, 2),很难看出这个tuple是用来表示一个坐标的。
定义一个class又小题大做了,这时,namedtuple就派上了用场:
>>> from collections import namedtuple>>> Point = namedtuple('Point', ['x', 'y'])>>> p = Point(1, 2)>>> p.x1>>> p.y2namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。
这样一来,我们用namedtuple可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用,使用十分方便。
可以验证创建的Point对象是tuple的一种子类:
>>> isinstance(p, Point)True>>> isinstance(p, tuple)True类似的,如果要用坐标和半径表示一个圆,也可以用namedtuple定义:
# namedtuple('名称', [属性list]):Circle = namedtuple('Circle', ['x', 'y', 'r'])deque使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。
deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:
>>> from collections import deque>>> q = deque(['a', 'b', 'c'])>>> q.append('x')>>> q.appendleft('y')>>> qdeque(['y', 'a', 'b', 'c', 'x'])deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。
defaultdict使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict:
>>> from collections import defaultdict>>> dd = defaultdict(lambda: 'N/A')>>> dd['key1'] = 'abc'>>> dd['key1'] # key1存在'abc'>>> dd['key2'] # key2不存在,返回默认值'N/A'注意默认值是调用函数返回的,而函数在创建defaultdict对象时传入。
除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的。
OrderedDict使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。
如果要保持Key的顺序,可以用OrderedDict:
>>> from collections import OrderedDict>>> d = dict([('a', 1), ('b', 2), ('c', 3)])>>> d # dict的Key是无序的{'a': 1, 'c': 3, 'b': 2}>>> od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])>>> od # OrderedDict的Key是有序的OrderedDict([('a', 1), ('b', 2), ('c', 3)])注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序:
>>> od = OrderedDict()>>> od['z'] = 1>>> od['y'] = 2>>> od['x'] = 3>>> list(od.keys()) # 按照插入的Key的顺序返回['z', 'y', 'x']OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key:
from collections import OrderedDictclass LastUpdatedOrderedDict(OrderedDict): def __init__(self, capacity): super(LastUpdatedOrderedDict, self).__init__() self._capacity = capacity def __setitem__(self, key, value): containsKey = 1 if key in self else 0 if len(self) - containsKey >= self._capacity: last = self.popitem(last=False) print('remove:', last) if containsKey: del self[key] print('set:', (key, value)) else: print('add:', (key, value)) OrderedDict.__setitem__(self, key, value)CounterCounter是一个简单的计数器,例如,统计字符出现的个数:
>>> from collections import Counter>>> c = Counter()>>> for ch in 'programming':... c[ch] = c[ch] + 1...>>> cCounter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1})Counter实际上也是dict的一个子类,上面的结果可以看出,字符'g'、'm'、'r'各出现了两次,其他字符各出现了一次。
小结collections模块提供了一些有用的集合类,可以根据需要选用。
作者: 八戒你干嘛 时间: 2017-7-6 13:33
base64[转]
Base64是一种用64个字符来表示任意二进制数据的方法。
用记事本打开exe、jpg、pdf这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。
Base64的原理很简单,首先,准备一个包含64个字符的数组:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit:
[attach]107040[/attach]
这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。
Python内置的base64可以直接进行base64的编解码:
>>> import base64>>> base64.b64encode(b'binary\x00string')b'YmluYXJ5AHN0cmluZw=='>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')b'binary\x00string'由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+和/分别变成-和_:
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')b'abcd++//'>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')b'abcd--__'>>> base64.urlsafe_b64decode('abcd--__')b'i\xb7\x1d\xfb\xef\xff'还可以自己定义64个字符的排列顺序,这样就可以自定义Base64编码,不过,通常情况下完全没有必要。
Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉:
# 标准Base64:'abcd' -> 'YWJjZA=='# 自动去掉=:'abcd' -> 'YWJjZA'去掉=后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了。
小结Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。
练习请写一个能处理去掉=的base64解码函数:
# -*- coding: utf-8 -*-import base64def safe_base64_decode(s):# 测试:assert b'abcd' == safe_base64_decode(b'YWJjZA=='), safe_base64_decode('YWJjZA==')assert b'abcd' == safe_base64_decode(b'YWJjZA'), safe_base64_decode('YWJjZA')print('Pass') Run
作者: 八戒你干嘛 时间: 2017-7-6 13:34
struct[转]
准确地讲,Python没有专门处理字节的数据类型。但由于b'str'可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便地用struct、union来处理字节,以及字节和int,float的转换。
在Python中,比方说要把一个32位无符号整数变成字节,也就是4个长度的bytes,你得配合位运算符这么写:
>>> n = 10240099>>> b1 = (n & 0xff000000) >> 24>>> b2 = (n & 0xff0000) >> 16>>> b3 = (n & 0xff00) >> 8>>> b4 = n & 0xff>>> bs = bytes([b1, b2, b3, b4])>>> bsb'\x00\x9c@c'非常麻烦。如果换成浮点数就无能为力了。
好在Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。
struct的pack函数把任意数据类型变成bytes:
>>> import struct>>> struct.pack('>I', 10240099)b'\x00\x9c@c'pack的第一个参数是处理指令,'>I'的意思是:
>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。
后面的参数个数要和处理指令一致。
unpack把bytes变成相应的数据类型:
>>> struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')(4042322160, 32896)根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数。
所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct就方便多了。
struct模块定义的数据类型可以参考Python官方文档。
Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。
首先找一个bmp文件,没有的话用“画图”画一个。
读入前30个字节来分析:
>>> s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'BMP格式采用小端方式存储数据,文件头的结构按顺序如下:
两个字节:'BM'表示Windows位图,'BA'表示OS/2位图;一个4字节整数:表示位图大小;一个4字节整数:保留位,始终为0;一个4字节整数:实际图像的偏移量;一个4字节整数:Header的字节数;一个4字节整数:图像宽度;一个4字节整数:图像高度;一个2字节整数:始终为1;一个2字节整数:颜色数。
所以,组合起来用unpack读取:
>>> struct.unpack('<ccIIIIIIHH', s)(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)结果显示,b'B'、b'M'说明是Windows位图,位图大小为640x360,颜色数为24。
请编写一个bmpinfo.py,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数。
作者: 八戒你干嘛 时间: 2017-7-6 13:36
hashlib[转]
摘要算法简介Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。
什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
举个例子,你写了一篇文章,内容是一个字符串'how to use python hashlib - by Michael',并附上这篇文章的摘要是'2d73d4f15c0db7f5ecb321b6a65e5d6d'。如果有人篡改了你的文章,并发表为'how to use python hashlib - by Bob',你可以一下子指出Bob篡改了你的文章,因为根据'how to use python hashlib - by Bob'计算出的摘要不同于原始文章的摘要。
可见,摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。
摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值:
import hashlibmd5 = hashlib.md5()md5.update('how to use md5 in python hashlib?'.encode('utf-8'))print(md5.hexdigest())计算结果如下:
d26a53750bc40b38b65a520292f69306如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的:
import hashlibmd5 = hashlib.md5()md5.update('how to use md5 in '.encode('utf-8'))md5.update('python hashlib?'.encode('utf-8'))print(md5.hexdigest())试试改动一个字母,看看计算的结果是否完全不同。
MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。
另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:
import hashlibsha1 = hashlib.sha1()sha1.update('how to use sha1 in '.encode('utf-8'))sha1.update('python hashlib?'.encode('utf-8'))print(sha1.hexdigest())SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要?完全有可能,因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞,比如Bob试图根据你的摘要反推出一篇文章'how to learn hashlib in python - by Bob',并且这篇文章的摘要恰好和你的文章完全一致,这种情况也并非不可能出现,但是非常非常困难。
摘要算法应用摘要算法能应用到什么地方?举个常用例子:
任何允许用户登录的网站都会存储用户登录的用户名和口令。如何存储用户名和口令呢?方法是存到数据库表中:
name | password--------+----------michael | 123456bob | abc999alice | alice2008如果以明文保存用户口令,如果数据库泄露,所有用户的口令就落入黑客的手里。此外,网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。
正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:
username | password---------+---------------------------------michael | e10adc3949ba59abbe56e057f20f883ebob | 878ef96e86145580c38c87f0410ad153alice | 99b1c2188db85afee403b1536010c2c9当用户登录时,首先计算用户输入的明文口令的MD5,然后和数据库存储的MD5对比,如果一致,说明口令输入正确,如果不一致,口令肯定错误。
练习根据用户输入的口令,计算出存储在数据库中的MD5口令:
def calc_md5(password): pass存储MD5的好处是即使运维人员能访问数据库,也无法获知用户的明文口令。
设计一个验证用户登录的函数,根据用户输入的口令是否正确,返回True或False:
db = { 'michael': 'e10adc3949ba59abbe56e057f20f883e', 'bob': '878ef96e86145580c38c87f0410ad153', 'alice': '99b1c2188db85afee403b1536010c2c9'}def login(user, password): pass采用MD5存储口令是否就一定安全呢?也不一定。假设你是一个黑客,已经拿到了存储MD5口令的数据库,如何通过MD5反推用户的明文口令呢?暴力破解费事费力,真正的黑客不会这么干。
考虑这么个情况,很多用户喜欢用123456,888888,password这些简单的口令,于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表:
'e10adc3949ba59abbe56e057f20f883e': '123456''21218cca77804d2ba1922c33e0151105': '888888''5f4dcc3b5aa765d61d8327deb882cf99': 'password'这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。
对于用户来讲,当然不要使用过于简单的口令。但是,我们能否在程序设计上对简单口令加强保护呢?
由于常用口令的MD5值很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”:
def calc_md5(password): return get_md5(password + 'the-Salt')经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。
但是如果有两个用户都使用了相同的简单口令比如123456,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?
如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。
练习根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5:
db = {}def register(username, password): db[username] = get_md5(password + username + 'the-Salt')然后,根据修改后的MD5算法实现用户登录的验证:
def login(username, password): pass小结摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
作者: 八戒你干嘛 时间: 2017-7-6 13:38
itertools[转]
Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
首先,我们看看itertools提供的几个“无限”迭代器:
>>> import itertools>>> natuals = itertools.count(1)>>> for n in natuals:... print(n)...123...因为count()会创建一个无限的迭代器,所以上述代码会打印出自然数序列,根本停不下来,只能按Ctrl+C退出。
cycle()会把传入的一个序列无限重复下去:
>>> import itertools>>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一种>>> for c in cs:... print(c)...'A''B''C''A''B''C'...同样停不下来。
repeat()负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:
>>> ns = itertools.repeat('A', 3)>>> for n in ns:... print(n)...AAA无限序列只有在for迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。
无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列:
>>> natuals = itertools.count(1)>>> ns = itertools.takewhile(lambda x: x <= 10, natuals)>>> list(ns)[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]itertools提供的几个迭代器操作函数更加有用:
chain()chain()可以把一组迭代对象串联起来,形成一个更大的迭代器:
>>> for c in itertools.chain('ABC', 'XYZ'):... print(c)# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'groupby()groupby()把迭代器中相邻的重复元素挑出来放在一起:
>>> for key, group in itertools.groupby('AAABBBCCAAA'):... print(key, list(group))...A ['A', 'A', 'A']B ['B', 'B', 'B']C ['C', 'C']A ['A', 'A', 'A']实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素'A'和'a'都返回相同的key:
>>> for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):... print(key, list(group))...A ['A', 'a', 'a']B ['B', 'B', 'b']C ['c', 'C']A ['A', 'A', 'a']小结itertools模块提供的全部是处理迭代功能的函数,它们的返回值不是list,而是Iterator,只有用for循环迭代的时候才真正计算。
作者: 八戒你干嘛 时间: 2017-7-6 13:39
contextlib[转]
在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。正确关闭文件资源的一个方法是使用try...finally:
try: f = open('/path/to/file', 'r') f.read()finally: if f: f.close()写try...finally非常繁琐。Python的with语句允许我们非常方便地使用资源,而不必担心资源没有关闭,所以上面的代码可以简化为:
with open('/path/to/file', 'r') as f: f.read()并不是只有open()函数返回的fp对象才能使用with语句。实际上,任何对象,只要正确实现了上下文管理,就可以用于with语句。
实现上下文管理是通过__enter__和__exit__这两个方法实现的。例如,下面的class实现了这两个方法:
class Query(object): def __init__(self, name): self.name = name def __enter__(self): print('Begin') return self def __exit__(self, exc_type, exc_value, traceback): if exc_type: print('Error') else: print('End') def query(self): print('Query info about %s...' % self.name)这样我们就可以把自己写的资源对象用于with语句:
with Query('Bob') as q: q.query()@contextmanager编写__enter__和__exit__仍然很繁琐,因此Python的标准库contextlib提供了更简单的写法,上面的代码可以改写如下:
from contextlib import contextmanagerclass Query(object): def __init__(self, name): self.name = name def query(self): print('Query info about %s...' % self.name)@contextmanagerdef create_query(name): print('Begin') q = Query(name) yield q print('End')@contextmanager这个decorator接受一个generator,用yield语句把with ... as var把变量输出出去,然后,with语句就可以正常地工作了:
with create_query('Bob') as q: q.query()很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现。例如:
@contextmanagerdef tag(name): print("<%s>" % name) yield print("</%s>" % name)with tag("h1"): print("hello") print("world")上述代码执行结果为:
<h1>helloworld</h1>代码的执行顺序是:
- with语句首先执行yield之前的语句,因此打印出<h1>;
- yield调用会执行with语句内部的所有语句,因此打印出hello和world;
- 最后执行yield之后的语句,打印出</h1>。
因此,@contextmanager让我们通过编写generator来简化上下文管理。
@closing如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。例如,用with语句使用urlopen():
from contextlib import closingfrom urllib.request import urlopenwith closing(urlopen('https://www.python.org')) as page: for line in page: print(line)closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:
@contextmanagerdef closing(thing): try: yield thing finally: thing.close()它的作用就是把任意对象变为上下文对象,并支持with语句。
@contextlib还有一些其他decorator,便于我们编写更简洁的代码。
作者: 八戒你干嘛 时间: 2017-7-6 13:42
XML[转]
XML虽然比JSON复杂,在Web中应用也不如以前多了,不过仍有很多地方在用,所以,有必要了解如何操作XML。
DOM vs SAX操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element,end_element和char_data,准备好这3个函数,然后就可以解析xml了。
举个例子,当SAX解析器读到一个节点时:
<a href="/">python</a>会产生3个事件:
用代码实验一下:
from xml.parsers.expat import ParserCreateclass DefaultSaxHandler(object): def start_element(self, name, attrs): print('sax:start_element: %s, attrs: %s' % (name, str(attrs))) def end_element(self, name): print('sax:end_element: %s' % name) def char_data(self, text): print('sax:char_data: %s' % text)xml = r'''<?xml version="1.0"?><ol> <li><a href="/python">Python</a></li> <li><a href="/ruby">Ruby</a></li></ol>'''handler = DefaultSaxHandler()parser = ParserCreate()parser.StartElementHandler = handler.start_elementparser.EndElementHandler = handler.end_elementparser.CharacterDataHandler = handler.char_dataparser.Parse(xml)需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面再合并。
除了解析XML外,如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串:
L = []L.append(r'<?xml version="1.0"?>')L.append(r'<root>')L.append(encode('some & data'))L.append(r'</root>')return ''.join(L)如果要生成复杂的XML呢?建议你不要用XML,改成JSON。
小结解析XML时,注意找出自己感兴趣的节点,响应事件时,把节点数据保存起来。解析完毕后,就可以处理数据。
练习请利用SAX编写程序解析Yahoo的XML格式的天气预报,获取当天和第二天的天气。
参数w是城市代码,要查询某个城市代码,可以在网站上搜索城市,浏览器地址栏的URL就包含城市代码。
# -*- coding:utf-8 -*-from xml.parsers.expat import ParserCreate# 测试:data = r'''<?xml version="1.0" encoding="UTF-8" standalone="yes" ?><rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"> <channel> <title>Yahoo! Weather - Beijing, CN</title> <lastBuildDate>Wed, 27 May 2015 11:00 am CST</lastBuildDate> <yweather:location city="Beijing" region="" country="China"/> <yweather:units temperature="C" distance="km" pressure="mb" speed="km/h"/> <yweather:wind chill="28" direction="180" speed="14.48" /> <yweather:atmosphere humidity="53" visibility="2.61" pressure="1006.1" rising="0" /> <yweather:astronomy sunrise="4:51 am" sunset="7:32 pm"/> <item> <geo:lat>39.91</geo:lat> <geo:long>116.39</geo:long> <pubDate>Wed, 27 May 2015 11:00 am CST</pubDate> <yweather:condition text="Haze" code="21" temp="28" date="Wed, 27 May 2015 11:00 am CST" /> <yweather:forecast day="Wed" date="27 May 2015" low="20" high="33" text="Partly Cloudy" code="30" /> <yweather:forecast day="Thu" date="28 May 2015" low="21" high="34" text="Sunny" code="32" /> <yweather:forecast day="Fri" date="29 May 2015" low="18" high="25" text="AM Showers" code="39" /> <yweather:forecast day="Sat" date="30 May 2015" low="18" high="32" text="Sunny" code="32" /> <yweather:forecast day="Sun" date="31 May 2015" low="20" high="37" text="Sunny" code="32" /> </item> </channel></rss>'''weather = parse_weather(data)assert weather['city'] == 'Beijing', weather['city']assert weather['country'] == 'China', weather['country']assert weather['today']['text'] == 'Partly Cloudy', weather['today']['text']assert weather['today']['low'] == 20, weather['today']['low']assert weather['today']['high'] == 33, weather['today']['high']assert weather['tomorrow']['text'] == 'Sunny', weather['tomorrow']['text']assert weather['tomorrow']['low'] == 21, weather['tomorrow']['low']assert weather['tomorrow']['high'] == 34, weather['tomorrow']['high']print('Weather:', str(weather)) Run
作者: 八戒你干嘛 时间: 2017-7-6 13:43
HTMLParser[转]
如果我们要编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,看看里面的内容到底是新闻、图片还是视频。
假设第一步已经完成了,第二步应该如何解析HTML呢?
HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。
好在Python提供了HTMLParser来非常方便地解析HTML,只需简单几行代码:
from html.parser import HTMLParserfrom html.entities import name2codepointclass MyHTMLParser(HTMLParser): def handle_starttag(self, tag, attrs): print('<%s>' % tag) def handle_endtag(self, tag): print('</%s>' % tag) def handle_startendtag(self, tag, attrs): print('<%s/>' % tag) def handle_data(self, data): print(data) def handle_comment(self, data): print('<!--', data, '-->') def handle_entityref(self, name): print('&%s;' % name) def handle_charref(self, name): print('&#%s;' % name)parser = MyHTMLParser()parser.feed('''<html><head></head><body><!-- test html parser --> <p>Some <a href=\"#\">html</a> HTML tutorial...<br>END</p></body></html>''')feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
特殊字符有两种,一种是英文表示的 ,一种是数字表示的Ӓ,这两种字符都可以通过Parser解析出来。
小结利用HTMLParser,可以把网页中的文本、图像等解析出来。
练习找一个网页,用浏览器查看源码并复制,然后尝试解析一下HTML,输出Python官网发布的会议时间、名称和地点。
作者: 八戒你干嘛 时间: 2017-7-6 13:46
本帖最后由 八戒你干嘛 于 2017-7-6 14:10 编辑
urllib[转]
urllib提供了一系列用于操作URL的功能。
Geturllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应:
例如,对豆瓣的一个URL进行抓取,并返回响应:
from urllib import requestwith request.urlopen('https://api.douban.com/v2/book/2129650') as f: data = f.read() print('Status:', f.status, f.reason) for k, v in f.getheaders(): print('%s: %s' % (k, v)) print('Data:', data.decode('utf-8'))可以看到HTTP响应的头和JSON数据:
Status: 200 OKServer: nginxDate: Tue, 26 May 2015 10:02:27 GMTContent-Type: application/json; charset=utf-8Content-Length: 2049Connection: closeExpires: Sun, 1 Jan 2006 01:00:00 GMTPragma: no-cacheCache-Control: must-revalidate, no-cache, privateX-DAE-Node: pidl1Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6","tags":[{"count":20,"name":"spring","title":"spring"}...}如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:
from urllib import requestreq = request.Request('http://www.douban.com/')req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')with request.urlopen(req) as f: print('Status:', f.status, f.reason) for k, v in f.getheaders(): print('%s: %s' % (k, v)) print('Data:', f.read().decode('utf-8'))这样豆瓣会返回适合iPhone的移动版网页:
... <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0"> <meta name="format-detection" content="telephone=no"> <link rel="apple-touch-icon" sizes="57x57" href="http://img4.douban.com/pics/cardkit/launcher/57.png" />...Post如果要以POST发送一个请求,只需要把参数data以bytes形式传入。
我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:
from urllib import request, parseprint('Login to weibo.cn...')email = input('Email: ')passwd = input('Password: ')login_data = parse.urlencode([ ('username', email), ('password', passwd), ('entry', 'mweibo'), ('client_id', ''), ('savestate', '1'), ('ec', ''), ('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')])req = request.Request('https://passport.weibo.cn/sso/login')req.add_header('Origin', 'https://passport.weibo.cn')req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')with request.urlopen(req, data=login_data.encode('utf-8')) as f: print('Status:', f.status, f.reason) for k, v in f.getheaders(): print('%s: %s' % (k, v)) print('Data:', f.read().decode('utf-8'))如果登录成功,我们获得的响应如下:
Status: 200 OKServer: nginx/1.2.0...Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn...Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}如果登录失败,我们获得的响应如下:
...Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}Handler如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:
proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()proxy_auth_handler.add_password('realm', 'host', 'username', 'password')opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)with opener.open('http://www.example.com/login.html') as f: pass小结urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标识浏览器的。
练习利用urllib读取XML,将XML一节的数据由硬编码改为由urllib获取:
from urllib import request, parsedef fetch_xml(url):# 测试print(fetch_xml('http://weather.yahooapis.com/forecastrss?u=c&w=2151330')) Run
作者: 八戒你干嘛 时间: 2017-7-6 14:03
常用第三方模块[转]
除了内建的模块外,Python还有大量的第三方模块。
基本上,所有的第三方模块都会在PyPI - the Python Package Index上注册,只要找到对应的模块名字,即可用pip安装。
本章介绍常用的第三方模块。
PIL
PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。
由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性,因此,我们可以直接安装使用Pillow。
安装Pillow在命令行下直接通过pip安装:
$ pip install pillow如果遇到Permission denied安装失败,请加上sudo重试。
操作图像来看看最常见的图像缩放操作,只需三四行代码:
from PIL import Image# 打开一个jpg图像文件,注意是当前路径:im = Image.open('test.jpg')# 获得图像尺寸:w, h = im.sizeprint('Original image size: %sx%s' % (w, h))# 缩放到50%:im.thumbnail((w//2, h//2))print('Resize image to: %sx%s' % (w//2, h//2))# 把缩放后的图像用jpeg格式保存:im.save('thumbnail.jpg', 'jpeg')其他功能如切片、旋转、滤镜、输出文字、调色板等一应俱全。
比如,模糊效果也只需几行代码:
from PIL import Image, ImageFilter# 打开一个jpg图像文件,注意是当前路径:im = Image.open('test.jpg')# 应用模糊滤镜:im2 = im.filter(ImageFilter.BLUR)im2.save('blur.jpg', 'jpeg')效果如下:
[attach]107044[/attach]
PIL的ImageDraw提供了一系列绘图方法,让我们可以直接绘图。比如要生成字母验证码图片:
from PIL import Image, ImageDraw, ImageFont, ImageFilterimport random# 随机字母:def rndChar(): return chr(random.randint(65, 90))# 随机颜色1:def rndColor(): return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))# 随机颜色2:def rndColor2(): return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))# 240 x 60:width = 60 * 4height = 60image = Image.new('RGB', (width, height), (255, 255, 255))# 创建Font对象:font = ImageFont.truetype('Arial.ttf', 36)# 创建Draw对象:draw = ImageDraw.Draw(image)# 填充每个像素:for x in range(width): for y in range(height): draw.point((x, y), fill=rndColor())# 输出文字:for t in range(4): draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())# 模糊:image = image.filter(ImageFilter.BLUR)image.save('code.jpg', 'jpeg')我们用随机颜色填充背景,再画上文字,最后对图像进行模糊,得到验证码图片如下:
[attach]107045[/attach]
如果运行的时候报错:
IOError: cannot open resource这是因为PIL无法定位到字体文件的位置,可以根据操作系统提供绝对路径,比如:
'/Library/Fonts/Arial.ttf'要详细了解PIL的强大功能,请请参考Pillow官方文档。
小结PIL提供了操作图像的强大功能,可以通过简单的代码完成复杂的图像处理。
作者: 八戒你干嘛 时间: 2017-7-6 14:24
virtualenv【转】
在开发Python应用程序的时候,系统安装的Python3只有一个版本:3.4。所有第三方的包都会被pip安装到Python3的site-packages目录下。
如果我们要同时开发多个应用程序,那这些应用程序都会共用一个Python,就是安装在系统的Python 3。如果应用A需要jinja 2.7,而应用B需要jinja 2.6怎么办?
这种情况下,每个应用可能需要各自拥有一套“独立”的Python运行环境。virtualenv就是用来为一个应用创建一套“隔离”的Python运行环境。
首先,我们用pip安装virtualenv:
$ pip3 install virtualenv然后,假定我们要开发一个新的项目,需要一套独立的Python运行环境,可以这么做:
第一步,创建目录:
Mac:~ michael$ mkdir myprojectMac:~ michael$ cd myproject/Mac:myproject michael$第二步,创建一个独立的Python运行环境,命名为venv:
Mac:myproject michael$ virtualenv --no-site-packages venvUsing base prefix '/usr/local/.../Python.framework/Versions/3.4'New python executable in venv/bin/python3.4Also creating executable in venv/bin/pythonInstalling setuptools, pip, wheel...done.命令virtualenv就可以创建一个独立的Python运行环境,我们还加上了参数--no-site-packages,这样,已经安装到系统Python环境中的所有第三方包都不会复制过来,这样,我们就得到了一个不带任何第三方包的“干净”的Python运行环境。
新建的Python环境被放到当前目录下的venv目录。有了venv这个Python环境,可以用source进入该环境:
Mac:myproject michael$ source venv/bin/activate(venv)Mac:myproject michael$注意到命令提示符变了,有个(venv)前缀,表示当前环境是一个名为venv的Python环境。
下面正常安装各种第三方包,并运行python命令:
(venv)Mac:myproject michael$ pip install jinja2...Successfully installed jinja2-2.7.3 markupsafe-0.23(venv)Mac:myproject michael$ python myapp.py...在venv环境下,用pip安装的包都被安装到venv这个环境下,系统Python环境不受任何影响。也就是说,venv环境是专门针对myproject这个应用创建的。
退出当前的venv环境,使用deactivate命令:
(venv)Mac:myproject michael$ deactivate Mac:myproject michael$此时就回到了正常的环境,现在pip或python均是在系统Python环境下执行。
完全可以针对每个应用创建独立的Python运行环境,这样就可以对每个应用的Python环境进行隔离。
virtualenv是如何创建“独立”的Python运行环境的呢?原理很简单,就是把系统Python复制一份到virtualenv的环境,用命令source venv/bin/activate进入一个virtualenv环境时,virtualenv会修改相关环境变量,让命令python和pip均指向当前的virtualenv环境。
小结virtualenv为应用提供了隔离的Python运行环境,解决了不同应用间多版本的冲突问题。
作者: 八戒你干嘛 时间: 2017-7-6 14:31
图形界面【转】
Python支持多种图形界面的第三方库,包括:
等等。
但是Python自带的库是支持Tk的Tkinter,使用Tkinter,无需安装任何包,就可以直接使用。本章简单介绍如何使用Tkinter进行GUI编程。
Tkinter我们来梳理一下概念:
我们编写的Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;
Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;
Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。
所以,我们的代码只需要调用Tkinter提供的接口就可以了。
第一个GUI程序使用Tkinter十分简单,我们来编写一个GUI版本的“Hello, world!”。
第一步是导入Tkinter包的所有内容:
from tkinter import *第二步是从Frame派生一个Application类,这是所有Widget的父容器:
class Application(Frame): def __init__(self, master=None): Frame.__init__(self, master) self.pack() self.createWidgets() def createWidgets(self): self.helloLabel = Label(self, text='Hello, world!') self.helloLabel.pack() self.quitButton = Button(self, text='Quit', command=self.quit) self.quitButton.pack()在GUI中,每个Button、Label、输入框等,都是一个Widget。Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。
pack()方法把Widget加入到父容器中,并实现布局。pack()是最简单的布局,grid()可以实现更复杂的布局。
在createWidgets()方法中,我们创建一个Label和一个Button,当Button被点击时,触发self.quit()使程序退出。
第三步,实例化Application,并启动消息循环:
app = Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()GUI程序的主线程负责监听来自操作系统的消息,并依次处理每一条消息。因此,如果消息处理非常耗时,就需要在新线程中处理。
运行这个GUI程序,可以看到下面的窗口:
[attach]107048[/attach]
点击“Quit”按钮或者窗口的“x”结束程序。
输入文本我们再对这个GUI程序改进一下,加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。
from tkinter import *import tkinter.messagebox as messageboxclass Application(Frame): def __init__(self, master=None): Frame.__init__(self, master) self.pack() self.createWidgets() def createWidgets(self): self.nameInput = Entry(self) self.nameInput.pack() self.alertButton = Button(self, text='Hello', command=self.hello) self.alertButton.pack() def hello(self): name = self.nameInput.get() or 'world' messagebox.showinfo('Message', 'Hello, %s' % name)app = Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()当用户点击按钮时,触发hello(),通过self.nameInput.get()获得用户输入的文本后,使用tkMessageBox.showinfo()可以弹出消息对话框。
程序运行结果如下:
[attach]107047[/attach]
小结Python内置的Tkinter可以满足基本的GUI程序的要求,如果是非常复杂的GUI程序,建议用操作系统原生支持的语言和库来编写。
作者: 八戒你干嘛 时间: 2017-7-6 14:34
本帖最后由 八戒你干嘛 于 2017-7-6 16:39 编辑
网络编程【转】
自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。
计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。
举个例子,当你使用浏览器访问新浪网时,你的计算机就和新浪的某台服务器通过互联网连接起来了,然后,新浪的服务器把网页内容作为数据通过互联网传输到你的电脑上。
由于你的电脑上可能不止浏览器,还有QQ、Skype、Dropbox、邮件客户端等,不同的程序连接的别的计算机也会不同,所以,更确切地说,网络通信是两台计算机上的两个进程之间的通信。比如,浏览器进程和新浪服务器上的某个Web服务进程在通信,而QQ进程是和腾讯的某个服务器上的某个进程在通信。
[attach]107071[/attach]
网络编程对所有开发语言都是一样的,Python也不例外。用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。
本章我们将详细介绍Python网络编程的概念和最主要的两种网络类型的编程。
作者: 八戒你干嘛 时间: 2017-7-6 14:38
本帖最后由 八戒你干嘛 于 2017-7-6 16:40 编辑
TCP/IP简介[转]
虽然大家现在对互联网很熟悉,但是计算机网络的出现比互联网要早很多。
计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。
为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。Internet是由inter和net两个单词组合起来的,原意就是连接“网络”的网络,有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。
通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
[attach]107072[/attach]
IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
端口有什么作用?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。
一个进程也可能同时与多个计算机建立链接,因此它会申请很多端口。
了解了TCP/IP协议的基本概念,IP地址和端口的概念,我们就可以开始进行网络编程了。
作者: 八戒你干嘛 时间: 2017-7-6 14:44
本帖最后由 八戒你干嘛 于 2017-7-6 16:42 编辑
TCP编程[转]
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
客户端大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。
所以,我们要创建一个基于TCP连接的Socket,可以这样做:
# 导入socket库:import socket# 创建一个socket:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:s.connect(('www.sina.com.cn', 80))创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。
客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名
www.sina.com.cn自动转换到IP地址,但是怎么知道新浪服务器的端口号呢?
答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。
因此,我们连接新浪服务器的代码如下:
s.connect(('www.sina.com.cn', 80))注意参数是一个tuple,包含地址和端口号。
建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:
# 发送数据:s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。
发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:
# 接收数据:buffer = []while True: # 每次最多接收1k字节: d = s.recv(1024) if d: buffer.append(d) else: breakdata = b''.join(buffer)接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。
当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:
# 关闭连接:s.close()接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:
header, html = data.split(b'\r\n\r\n', 1)print(header.decode('utf-8'))# 把接收的数据写入文件:with open('sina.html', 'wb') as f: f.write(html)现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。
服务器和客户端编程相比,服务器编程就要复杂一些。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。
首先,创建一个基于IPv4和TCP协议的Socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。
端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:
# 监听端口:s.bind(('127.0.0.1', 9999))紧接着,调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量:
s.listen(5)print('Waiting for connection...')接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:
while True: # 接受一个新连接: sock, addr = s.accept() # 创建新线程来处理TCP连接: t = threading.Thread(target=tcplink, args=(sock, addr)) t.start()每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:
def tcplink(sock, addr): print('Accept new connection from %s:%s...' % addr) sock.send(b'Welcome!') while True: data = sock.recv(1024) time.sleep(1) if not data or data.decode('utf-8') == 'exit': break sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8')) sock.close() print('Connection from %s:%s closed.' % addr)连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。
要测试这个服务器程序,我们还需要编写一个客户端程序:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立连接:s.connect(('127.0.0.1', 9999))# 接收欢迎消息:print(s.recv(1024).decode('utf-8'))for data in [b'Michael', b'Tracy', b'Sarah']: # 发送数据: s.send(data) print(s.recv(1024).decode('utf-8'))s.send(b'exit')s.close()我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了:
[attach]107073[/attach]
需要注意的是,客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C退出程序。
小结用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。
同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
作者: 八戒你干嘛 时间: 2017-7-6 14:49
本帖最后由 八戒你干嘛 于 2017-7-6 16:43 编辑
UDP编程[转]
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。
使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。
虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
我们来看看如何通过UDP协议传输数据。和TCP类似,使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 绑定端口:s.bind(('127.0.0.1', 9999))创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据:
print('Bind UDP on 9999...')while True: # 接收数据: data, addr = s.recvfrom(1024) print('Received from %s:%s.' % addr) s.sendto(b'Hello, %s!' % data, addr)recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。
注意这里省掉了多线程,因为这个例子很简单。
客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect(),直接通过sendto()给服务器发数据:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)for data in [b'Michael', b'Tracy', b'Sarah']: # 发送数据: s.sendto(data, ('127.0.0.1', 9999)) # 接收数据: print(s.recv(1024).decode('utf-8'))s.close()从服务器接收数据仍然调用recv()方法。
仍然用两个命令行分别启动服务器和客户端测试,结果如下:
[attach]107074[/attach]
小结UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。
作者: 八戒你干嘛 时间: 2017-7-6 14:53
本帖最后由 八戒你干嘛 于 2017-7-6 16:45 编辑
电子邮件【转】
Email的历史比Web还要久远,直到现在,Email也是互联网上应用非常广泛的服务。
几乎所有的编程语言都支持发送和接收电子邮件,但是,先等等,在我们开始编写代码之前,有必要搞清楚电子邮件是如何在互联网上运作的。
我们来看看传统邮件是如何运作的。假设你现在在北京,要给一个香港的朋友发一封信,怎么做呢?
首先你得写好信,装进信封,写上地址,贴上邮票,然后就近找个邮局,把信仍进去。
信件会从就近的小邮局转运到大邮局,再从大邮局往别的城市发,比如先发到天津,再走海运到达香港,也可能走京九线到香港,但是你不用关心具体路线,你只需要知道一件事,就是信件走得很慢,至少要几天时间。
信件到达香港的某个邮局,也不会直接送到朋友的家里,因为邮局的叔叔是很聪明的,他怕你的朋友不在家,一趟一趟地白跑,所以,信件会投递到你的朋友的邮箱里,邮箱可能在公寓的一层,或者家门口,直到你的朋友回家的时候检查邮箱,发现信件后,就可以取到邮件了。
电子邮件的流程基本上也是按上面的方式运作的,只不过速度不是按天算,而是按秒算。
现在我们回到电子邮件,假设我们自己的电子邮件地址是
me@163.com,对方的电子邮件地址是
friend@sina.com(注意地址都是虚构的哈),现在我们用Outlook或者Foxmail之类的软件写好邮件,填上对方的Email地址,点“发送”,电子邮件就发出去了。这些电子邮件软件被称为
MUA:Mail User Agent——邮件用户代理。
Email从MUA发出去,不是直接到达对方电脑,而是发到MTA:Mail Transfer Agent——邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。由于我们自己的电子邮件是163.com,所以,Email首先被投递到网易提供的MTA,再由网易的MTA发到对方服务商,也就是新浪的MTA。这个过程中间可能还会经过别的MTA,但是我们不关心具体路线,我们只关心速度。
Email到达新浪的MTA后,由于对方使用的是@sina.com的邮箱,因此,新浪的MTA会把Email投递到邮件的最终目的地MDA:Mail Delivery Agent——邮件投递代理。Email到达MDA后,就静静地躺在新浪的某个服务器上,存放在某个文件或特殊的数据库里,我们将这个长期保存邮件的地方称之为电子邮箱。
同普通邮件类似,Email不会直接到达对方的电脑,因为对方电脑不一定开机,开机也不一定联网。对方要取到邮件,必须通过MUA从MDA上把邮件取到自己的电脑上。
所以,一封电子邮件的旅程就是:
发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人有了上述基本概念,要编写程序来发送和接收邮件,本质上就是:
编写MUA把邮件发到MTA;
编写MUA从MDA上收邮件。
发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。
收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。
邮件客户端软件在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。假设你正在使用163的邮箱,你就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,你得填163提供的SMTP服务器地址:smtp.163.com,为了证明你是163的用户,SMTP服务器还要求你填写邮箱地址和邮箱口令,这样,MUA才能正常地把Email通过SMTP协议发送到MTA。
类似的,从MDA收邮件时,MDA服务器也要求验证你的邮箱口令,确保不会有人冒充你收取你的邮件,所以,Outlook之类的邮件客户端会要求你填写POP3或IMAP服务器地址、邮箱地址和口令,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。
最后特别注意,目前大多数邮件服务商都需要手动打开SMTP发信和POP收信的功能,否则只允许在网页登录:
[attach]107075[/attach]
作者: 八戒你干嘛 时间: 2017-7-6 15:07
本帖最后由 八戒你干嘛 于 2017-7-6 16:51 编辑
SMTP发送邮件[转]
SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。
Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
首先,我们来构造一个最简单的纯文本邮件:
from email.mime.text import MIMETextmsg = MIMEText('hello, send by Python...', 'plain', 'utf-8')注意到构造MIMEText对象时,第一个参数就是邮件正文,第二个参数是MIME的subtype,传入'plain'表示纯文本,最终的MIME就是'text/plain',最后一定要用utf-8编码保证多语言兼容性。
然后,通过SMTP发出去:
# 输入Email地址和口令:from_addr = input('From: ')password = input('Password: ')# 输入收件人地址:to_addr = input('To: ')# 输入SMTP服务器地址:smtp_server = input('SMTP server: ')import smtplibserver = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25server.set_debuglevel(1)server.login(from_addr, password)server.sendmail(from_addr, [to_addr], msg.as_string())server.quit()我们用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和响应。login()方法用来登录SMTP服务器,sendmail()方法就是发邮件,由于可以一次发给多个人,所以传入一个list,邮件正文是一个str,as_string()把MIMEText对象变成str。
如果一切顺利,就可以在收件人信箱中收到我们刚发送的Email:
[attach]107076[/attach]
仔细观察,发现如下问题:
这是因为邮件主题、如何显示发件人、收件人等信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,所以,我们必须把From、To和Subject添加到MIMEText中,才是一封完整的邮件:
from email import encodersfrom email.header import Headerfrom email.mime.text import MIMETextfrom email.utils import parseaddr, formataddrimport smtplibdef _format_addr(s): name, addr = parseaddr(s) return formataddr((Header(name, 'utf-8').encode(), addr))from_addr = input('From: ')password = input('Password: ')to_addr = input('To: ')smtp_server = input('SMTP server: ')msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)msg['To'] = _format_addr('管理员 <%s>' % to_addr)msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()server = smtplib.SMTP(smtp_server, 25)server.set_debuglevel(1)server.login(from_addr, password)server.sendmail(from_addr, [to_addr], msg.as_string())server.quit()我们编写了一个函数_format_addr()来格式化一个邮件地址。注意不能简单地传入name <
addr@example.com>,因为如果包含中文,需要通过Header对象进行编码。
msg['To']接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。
再发送一遍邮件,就可以在收件人邮箱中看到正确的标题、发件人和收件人:
[attach]107077[/attach]
你看到的收件人的名字很可能不是我们传入的管理员,因为很多邮件服务商在显示邮件时,会把收件人名字自动替换为用户注册的名字,但是其他收件人名字的显示不受影响。
如果我们查看Email的原始内容,可以看到如下经过编码的邮件头:
From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= <xxxxxx@163.com>To: =?utf-8?b?566h55CG5ZGY?= <xxxxxx@qq.com>Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=这就是经过Header对象编码的文本,包含utf-8编码信息和Base64编码的文本。如果我们自己来手动构造这样的编码文本,显然比较复杂。
发送HTML邮件如果我们要发送HTML邮件,而不是普通的纯文本文件怎么办?方法很简单,在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain变为html就可以了:
msg = MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://www.python.org">Python</a>...</p>' + '</body></html>', 'html', 'utf-8')再发送一遍邮件,你将看到以HTML显示的邮件:
[attach]107078[/attach]
发送附件如果Email中要加上附件怎么办?带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:
# 邮件对象:msg = MIMEMultipart()msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)msg['To'] = _format_addr('管理员 <%s>' % to_addr)msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()# 邮件正文是MIMEText:msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))# 添加附件就是加上一个MIMEBase,从本地读取一个图片:with open('/Users/michael/Downloads/test.png', 'rb') as f: # 设置附件的MIME和文件名,这里是png类型: mime = MIMEBase('image', 'png', filename='test.png') # 加上必要的头信息: mime.add_header('Content-Disposition', 'attachment', filename='test.png') mime.add_header('Content-ID', '<0>') mime.add_header('X-Attachment-Id', '0') # 把附件的内容读进来: mime.set_payload(f.read()) # 用Base64编码: encoders.encode_base64(mime) # 添加到MIMEMultipart: msg.attach(mime)然后,按正常发送流程把msg(注意类型已变为MIMEMultipart)发送出去,就可以收到如下带附件的邮件:
[attach]107079[/attach]
发送图片如果要把一个图片嵌入到邮件正文中怎么做?直接在HTML邮件中链接图片地址行不行?答案是,大部分邮件服务商都会自动屏蔽带有外链的图片,因为不知道这些链接是否指向恶意网站。
要把图片嵌入到邮件正文中,我们只需按照发送附件的方式,先把邮件作为附件添加进去,然后,在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入了。如果有多个图片,给它们依次编号,然后引用不同的cid:x即可。
把上面代码加入MIMEMultipart的MIMEText从plain改为html,然后在适当的位置引用图片:
msg.attach(MIMEText('<html><body><h1>Hello</h1>' + '<p><img src="cid:0"></p>' + '</body></html>', 'html', 'utf-8'))再次发送,就可以看到图片直接嵌入到邮件正文的效果:
同时支持HTML和Plain格式如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了HTML邮件怎么办?
办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。
利用MIMEMultipart就可以组合一个HTML和Plain,要注意指定subtype是alternative:
msg = MIMEMultipart('alternative')msg['From'] = ...msg['To'] = ...msg['Subject'] = ...msg.attach(MIMEText('hello', 'plain', 'utf-8'))msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))# 正常发送msg对象...
作者: 八戒你干嘛 时间: 2017-7-6 15:08
加密SMTP使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。
某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。
必须知道,Gmail的SMTP端口是587,因此,修改代码如下:
smtp_server = 'smtp.gmail.com'smtp_port = 587server = smtplib.SMTP(smtp_server, smtp_port)server.starttls()# 剩下的代码和前面的一模一样:server.set_debuglevel(1)...只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全连接。后面的代码和前面的发送邮件代码完全一样。
如果因为网络问题无法连接Gmail的SMTP服务器,请相信我们的代码是没有问题的,你需要对你的网络设置做必要的调整。
小结使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。
构造一个邮件对象就是一个Messag对象,如果构造一个MIMEText对象,就表示一个文本邮件对象,如果构造一个MIMEImage对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象。它们的继承关系如下:
Message+- MIMEBase +- MIMEMultipart +- MIMENonMultipart +- MIMEMessage +- MIMEText +- MIMEImage这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。
作者: 八戒你干嘛 时间: 2017-7-6 15:09
本帖最后由 八戒你干嘛 于 2017-7-6 16:52 编辑
POP3收取邮件[转]
SMTP用于发送邮件,如果要收取邮件呢?
收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。
Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。
注意到POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。
要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。
所以,收取邮件分两步:
第一步:用poplib把邮件的原始文本下载到本地;
第二部:用email解析原始文本,还原为邮件对象。
通过POP3下载邮件POP3协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:
import poplib# 输入邮件地址, 口令和POP3服务器地址:email = input('Email: ')password = input('Password: ')pop3_server = input('POP3 server: ')# 连接到POP3服务器:server = poplib.POP3(pop3_server)# 可以打开或关闭调试信息:server.set_debuglevel(1)# 可选:打印POP3服务器的欢迎文字:print(server.getwelcome().decode('utf-8'))# 身份认证:server.user(email)server.pass_(password)# stat()返回邮件数量和占用空间:print('Messages: %s. Size: %s' % server.stat())# list()返回所有邮件的编号:resp, mails, octets = server.list()# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]print(mails)# 获取最新一封邮件, 注意索引号从1开始:index = len(mails)resp, lines, octets = server.retr(index)# lines存储了邮件的原始文本的每一行,# 可以获得整个邮件的原始文本:msg_content = b'\r\n'.join(lines).decode('utf-8')# 稍后解析出邮件:msg = Parser().parsestr(msg_content)# 可以根据邮件索引号直接从服务器删除邮件:# server.dele(index)# 关闭连接:server.quit()用POP3获取邮件其实很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。
解析邮件解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块:
from email.parser import Parserfrom email.header import decode_headerfrom email.utils import parseaddrimport poplib只需要一行代码就可以把邮件内容解析为Message对象:
msg = Parser().parsestr(msg_content)但是这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。
所以我们要递归地打印出Message对象的层次结构:
# indent用于缩进显示:def print_info(msg, indent=0): if indent == 0: for header in ['From', 'To', 'Subject']: value = msg.get(header, '') if value: if header=='Subject': value = decode_str(value) else: hdr, addr = parseaddr(value) name = decode_str(hdr) value = u'%s <%s>' % (name, addr) print('%s%s: %s' % (' ' * indent, header, value)) if (msg.is_multipart()): parts = msg.get_payload() for n, part in enumerate(parts): print('%spart %s' % (' ' * indent, n)) print('%s--------------------' % (' ' * indent)) print_info(part, indent + 1) else: content_type = msg.get_content_type() if content_type=='text/plain' or content_type=='text/html': content = msg.get_payload(decode=True) charset = guess_charset(msg) if charset: content = content.decode(charset) print('%sText: %s' % (' ' * indent, content + '...')) else: print('%sAttachment: %s' % (' ' * indent, content_type))邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:
def decode_str(s): value, charset = decode_header(s)[0] if charset: value = value.decode(charset) return valuedecode_header()返回一个list,因为像Cc、Bcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们偷了个懒,只取了第一个元素。
文本邮件的内容也是str,还需要检测编码,否则,非UTF-8编码的邮件都无法正常显示:
def guess_charset(msg): charset = msg.get_charset() if charset is None: content_type = msg.get('Content-Type', '').lower() pos = content_type.find('charset=') if pos >= 0: charset = content_type[pos + 8:].strip() return charset把上面的代码整理好,我们就可以来试试收取一封邮件。先往自己的邮箱发一封邮件,然后用浏览器登录邮箱,看看邮件收到没,如果收到了,我们就来用Python程序把它收到本地:
[attach]107081[/attach]
运行程序,结果如下:
+OK Welcome to coremail Mail Pop3 Server (163coms[...])Messages: 126. Size: 27228317From: Test <xxxxxx@qq.com>To: Python爱好者 <xxxxxx@163.com>Subject: 用POP3收取邮件part 0-------------------- part 0 -------------------- Text: Python可以使用POP3收取邮件……... part 1 -------------------- Text: Python可以<a href="...">使用POP3</a>收取邮件……...part 1-------------------- Attachment: application/octet-stream我们从打印的结构可以看出,这封邮件是一个MIMEMultipart,它包含两部分:第一部分又是一个MIMEMultipart,第二部分是一个附件。而内嵌的MIMEMultipart是一个alternative类型,它包含一个纯文本格式的MIMEText和一个HTML格式的MIMEText。
小结用Python的poplib模块收取邮件分两步:第一步是用POP3协议把邮件获取到本地,第二步是用email模块把原始邮件解析为Message对象,然后,用适当的形式把邮件内容展示给用户即可。
作者: 八戒你干嘛 时间: 2017-7-6 15:11
本帖最后由 八戒你干嘛 于 2017-7-6 16:54 编辑
访问数据库【转】
程序运行的时候,数据都是在内存中的。当程序终止的时候,通常都需要将数据保存到磁盘上,无论是保存到本地磁盘,还是通过网络保存到服务器上,最终都会将数据写入磁盘文件。
而如何定义数据的存储格式就是一个大问题。如果我们自己来定义存储格式,比如保存一个班级所有学生的成绩单:
名字 | 成绩 |
Michael | 99 |
Bob | 85 |
Bart | 59 |
Lisa | 87 |
你可以用一个文本文件保存,一行保存一个学生,用,隔开:
Michael,99Bob,85Bart,59Lisa,87你还可以用JSON格式保存,也是文本文件:
[ {"name":"Michael","score":99}, {"name":"Bob","score":85}, {"name":"Bart","score":59}, {"name":"Lisa","score":87}]你还可以定义各种保存格式,但是问题来了:
存储和读取需要自己实现,JSON还是标准,自己定义的格式就各式各样了;
不能做快速查询,只有把数据全部读到内存中才能自己遍历,但有时候数据的大小远远超过了内存(比如蓝光电影,40GB的数据),根本无法全部读入内存。
为了便于程序保存和读取数据,而且,能直接通过条件快速查询到指定的数据,就出现了数据库(Database)这种专门用于集中存储和查询的软件。
数据库软件诞生的历史非常久远,早在1950年数据库就诞生了。经历了网状数据库,层次数据库,我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。
关系模型有一套复杂的数学理论,但是从概念上是十分容易理解的。举个学校的例子:
假设某个XX省YY市ZZ县第一实验小学有3个年级,要表示出这3个年级,可以在Excel中用一个表格画出来:
[attach]107084[/attach]
每个年级又有若干个班级,要把所有班级表示出来,可以在Excel中再画一个表格:
[attach]107082[/attach]
这两个表格有个映射关系,就是根据Grade_ID可以在班级表中查找到对应的所有班级:
[attach]107083[/attach]
也就是Grade表的每一行对应Class表的多行,在关系数据库中,这种基于表(Table)的一对多的关系就是关系数据库的基础。
根据某个年级的ID就可以查找所有班级的行,这种查询语句在关系数据库中称为SQL语句,可以写成:
SELECT * FROM classes WHERE grade_id = '1';结果也是一个表:
---------+----------+----------grade_id | class_id | name---------+----------+----------1 | 11 | 一年级一班---------+----------+----------1 | 12 | 一年级二班---------+----------+----------1 | 13 | 一年级三班---------+----------+----------类似的,Class表的一行记录又可以关联到Student表的多行记录:
[attach]107085[/attach]
由于本教程不涉及到关系数据库的详细内容,如果你想从零学习关系数据库和基本的SQL语句,如果你想从零学习关系数据库和基本的SQL语句,请自行搜索相关课程。
NoSQL你也许还听说过NoSQL数据库,很多NoSQL宣传其速度和规模远远超过关系数据库,所以很多同学觉得有了NoSQL是否就不需要SQL了呢?千万不要被他们忽悠了,连SQL都不明白怎么可能搞明白NoSQL呢?
数据库类别既然我们要使用关系数据库,就必须选择一个关系数据库。目前广泛使用的关系数据库也就这么几种:
付费的商用数据库:
这些数据库都是不开源而且付费的,最大的好处是花了钱出了问题可以找厂家解决,不过在Web的世界里,常常需要部署成千上万的数据库服务器,当然不能把大把大把的银子扔给厂家,所以,无论是Google、Facebook,还是国内的BAT,无一例外都选择了免费的开源数据库:
作为Python开发工程师,选择哪个免费数据库呢?当然是MySQL。因为MySQL普及率最高,出了错,可以很容易找到解决方法。而且,围绕MySQL有一大堆监控和运维的工具,安装和使用很方便。
为了能继续后面的学习,你需要从MySQL官方网站下载并安装MySQL Community Server 5.6,这个版本是免费的,其他高级版本是要收钱的(请放心,收钱的功能我们用不上)。
作者: 八戒你干嘛 时间: 2017-7-6 15:13
使用SQLite[转]
SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。
在使用SQLite前,我们先要搞清楚几个概念:
表是数据库中存放关系数据的集合,一个数据库里面通常都包含多个表,比如学生的表,班级的表,学校的表,等等。表和表之间通过外键关联。
要操作关系数据库,首先需要连接到数据库,一个数据库连接称为Connection;
连接到数据库后,需要打开游标,称之为Cursor,通过Cursor执行SQL语句,然后,获得执行结果。
Python定义了一套操作数据库的API接口,任何数据库要连接到Python,只需要提供符合Python标准的数据库驱动即可。
由于SQLite的驱动内置在Python标准库中,所以我们可以直接来操作SQLite数据库。
我们在Python交互式命令行实践一下:
# 导入SQLite驱动:>>> import sqlite3# 连接到SQLite数据库# 数据库文件是test.db# 如果文件不存在,会自动在当前目录创建:>>> conn = sqlite3.connect('test.db')# 创建一个Cursor:>>> cursor = conn.cursor()# 执行一条SQL语句,创建user表:>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')<sqlite3.Cursor object at 0x10f8aa260># 继续执行一条SQL语句,插入一条记录:>>> cursor.execute('insert into user (id, name) values (\'1\', \'Michael\')')<sqlite3.Cursor object at 0x10f8aa260># 通过rowcount获得插入的行数:>>> cursor.rowcount1# 关闭Cursor:>>> cursor.close()# 提交事务:>>> conn.commit()# 关闭Connection:>>> conn.close()我们再试试查询记录:
>>> conn = sqlite3.connect('test.db')>>> cursor = conn.cursor()# 执行查询语句:>>> cursor.execute('select * from user where id=?', ('1',))<sqlite3.Cursor object at 0x10f8aa340># 获得查询结果集:>>> values = cursor.fetchall()>>> values[('1', 'Michael')]>>> cursor.close()>>> conn.close()使用Python的DB-API时,只要搞清楚Connection和Cursor对象,打开后一定记得关闭,就可以放心地使用。
使用Cursor对象执行insert,update,delete语句时,执行结果由rowcount返回影响的行数,就可以拿到执行结果。
使用Cursor对象执行select语句时,通过featchall()可以拿到结果集。结果集是一个list,每个元素都是一个tuple,对应一行记录。
如果SQL语句带有参数,那么需要把参数按照位置传递给execute()方法,有几个?占位符就必须对应几个参数,例如:
cursor.execute('select * from user where name=? and pwd=?', ('abc', 'password'))SQLite支持常见的标准SQL语句以及几种常见的数据类型。具体文档请参阅SQLite官方网站。
小结在Python中操作数据库时,要先导入数据库对应的驱动,然后,通过Connection对象和Cursor对象操作数据。
要确保打开的Connection对象和Cursor对象都正确地被关闭,否则,资源就会泄露。
如何才能确保出错的情况下也关闭掉Connection对象和Cursor对象呢?请回忆try:...except:...finally:...的用法。
练习请编写函数,在Sqlite中根据分数段查找指定的名字:
# -*- coding: utf-8 -*-import os, sqlite3db_file = os.path.join(os.path.dirname(__file__), 'test.db')if os.path.isfile(db_file): os.remove(db_file)# 初始数据:conn = sqlite3.connect(db_file)cursor = conn.cursor()cursor.execute('create table user(id varchar(20) primary key, name varchar(20), score int)')cursor.execute(r"insert into user values ('A-001', 'Adam', 95)")cursor.execute(r"insert into user values ('A-002', 'Bart', 62)")cursor.execute(r"insert into user values ('A-003', 'Lisa', 78)")cursor.close()conn.commit()conn.close()def get_score_in(low, high): ' 返回指定分数区间的名字,按分数从低到高排序 '# 测试:assert get_score_in(80, 95) == ['Adam'], get_score_in(80, 95)assert get_score_in(60, 80) == ['Bart', 'Lisa'], get_score_in(60, 80)assert get_score_in(60, 100) == ['Bart', 'Lisa', 'Adam'], get_score_in(60, 100)print('Pass') Run
作者: 八戒你干嘛 时间: 2017-7-6 15:21
使用MySQL[转]
MySQL是Web世界中使用最广泛的数据库服务器。SQLite的特点是轻量级、可嵌入,但不能承受高并发访问,适合桌面和移动应用。而MySQL是为服务器端设计的数据库,能承受高并发访问,同时占用的内存也远远大于SQLite。
此外,MySQL内部有多种数据库引擎,最常用的引擎是支持数据库事务的InnoDB。
安装MySQL可以直接从MySQL官方网站下载最新的Community Server 5.6.x版本。MySQL是跨平台的,选择对应的平台下载安装文件,安装即可。
安装时,MySQL会提示输入root用户的口令,请务必记清楚。如果怕记不住,就把口令设置为password。
在Windows上,安装时请选择UTF-8编码,以便正确地处理中文。
在Mac或Linux上,需要编辑MySQL的配置文件,把数据库默认的编码全部改为UTF-8。MySQL的配置文件默认存放在/etc/my.cnf或者/etc/mysql/my.cnf:
[client]default-character-set = utf8[mysqld]default-storage-engine = INNODBcharacter-set-server = utf8collation-server = utf8_general_ci重启MySQL后,可以通过MySQL的客户端命令行检查编码:
$ mysql -u root -pEnter password: Welcome to the MySQL monitor......mysql> show variables like '%char%';+--------------------------+--------------------------------------------------------+| Variable_name | Value |+--------------------------+--------------------------------------------------------+| character_set_client | utf8 || character_set_connection | utf8 || character_set_database | utf8 || character_set_filesystem | binary || character_set_results | utf8 || character_set_server | utf8 || character_set_system | utf8 || character_sets_dir | /usr/local/mysql-5.1.65-osx10.6-x86_64/share/charsets/ |+--------------------------+--------------------------------------------------------+8 rows in set (0.00 sec)看到utf8字样就表示编码设置正确。
注:如果MySQL的版本≥5.5.3,可以把编码设置为utf8mb4,utf8mb4和utf8完全兼容,但它支持最新的Unicode标准,可以显示emoji字符。
安装MySQL驱动由于MySQL服务器以独立的进程运行,并通过网络对外服务,所以,需要支持Python的MySQL驱动来连接到MySQL服务器。MySQL官方提供了mysql-connector-python驱动,但是安装的时候需要给pip命令加上参数--allow-external:
$ pip install mysql-connector-python --allow-external mysql-connector-python如果上面的命令安装失败,可以试试另一个驱动:
$ pip install mysql-connector我们演示如何连接到MySQL服务器的test数据库:
# 导入MySQL驱动:>>> import mysql.connector# 注意把password设为你的root口令:>>> conn = mysql.connector.connect(user='root', password='password', database='test')>>> cursor = conn.cursor()# 创建user表:>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')# 插入一行记录,注意MySQL的占位符是%s:>>> cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])>>> cursor.rowcount1# 提交事务:>>> conn.commit()>>> cursor.close()# 运行查询:>>> cursor = conn.cursor()>>> cursor.execute('select * from user where id = %s', ('1',))>>> values = cursor.fetchall()>>> values[('1', 'Michael')]# 关闭Cursor和Connection:>>> cursor.close()True>>> conn.close()由于Python的DB-API定义都是通用的,所以,操作MySQL的数据库代码和SQLite类似。
小结
作者: 八戒你干嘛 时间: 2017-7-6 15:22
使用SQLAlchemy[转]
数据库表是一个二维表,包含多行多列。把一个表的内容用Python的数据结构表示出来的话,可以用一个list表示多行,list的每一个元素是tuple,表示一行记录,比如,包含id和name的user表:
[ ('1', 'Michael'), ('2', 'Bob'), ('3', 'Adam')]Python的DB-API返回的数据结构就是像上面这样表示的。
但是用tuple表示一行很难看出表的结构。如果把一个tuple用class实例来表示,就可以更容易地看出表的结构来:
class User(object): def __init__(self, id, name): self.id = id self.name = name[ User('1', 'Michael'), User('2', 'Bob'), User('3', 'Adam')]这就是传说中的ORM技术:Object-Relational Mapping,把关系数据库的表结构映射到对象上。是不是很简单?
但是由谁来做这个转换呢?所以ORM框架应运而生。
在Python中,最有名的ORM框架是SQLAlchemy。我们来看看SQLAlchemy的用法。
首先通过pip安装SQLAlchemy:
$ pip install sqlalchemy然后,利用上次我们在MySQL的test数据库中创建的user表,用SQLAlchemy来试试:
第一步,导入SQLAlchemy,并初始化DBSession:
# 导入:from sqlalchemy import Column, String, create_enginefrom sqlalchemy.orm import sessionmakerfrom sqlalchemy.ext.declarative import declarative_base# 创建对象的基类:Base = declarative_base()# 定义User对象:class User(Base): # 表的名字: __tablename__ = 'user' # 表的结构: id = Column(String(20), primary_key=True) name = Column(String(20))# 初始化数据库连接:engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/test')# 创建DBSession类型:DBSession = sessionmaker(bind=engine)以上代码完成SQLAlchemy的初始化和具体每个表的class定义。如果有多个表,就继续定义其他class,例如School:
class School(Base): __tablename__ = 'school' id = ... name = ...create_engine()用来初始化数据库连接。SQLAlchemy用一个字符串表示连接信息:
'数据库类型+数据库驱动名称://用户名:口令@机器地址:端口号/数据库名'你只需要根据需要替换掉用户名、口令等信息即可。
下面,我们看看如何向数据库表中添加一行记录。
由于有了ORM,我们向数据库表中添加一行记录,可以视为添加一个User对象:
# 创建session对象:session = DBSession()# 创建新User对象:new_user = User(id='5', name='Bob')# 添加到session:session.add(new_user)# 提交即保存到数据库:session.commit()# 关闭session:session.close()可见,关键是获取session,然后把对象添加到session,最后提交并关闭。DBSession对象可视为当前数据库连接。
如何从数据库表中查询数据呢?有了ORM,查询出来的可以不再是tuple,而是User对象。SQLAlchemy提供的查询接口如下:
# 创建Session:session = DBSession()# 创建Query查询,filter是where条件,最后调用one()返回唯一行,如果调用all()则返回所有行:user = session.query(User).filter(User.id=='5').one()# 打印类型和对象的name属性:print('type:', type(user))print('name:', user.name)# 关闭Session:session.close()运行结果如下:
type: <class '__main__.User'>name: Bob可见,ORM就是把数据库表的行与相应的对象建立关联,互相转换。
由于关系数据库的多个表还可以用外键实现一对多、多对多等关联,相应地,ORM框架也可以提供两个对象之间的一对多、多对多等功能。
例如,如果一个User拥有多个Book,就可以定义一对多关系如下:
class User(Base): __tablename__ = 'user' id = Column(String(20), primary_key=True) name = Column(String(20)) # 一对多: books = relationship('Book')class Book(Base): __tablename__ = 'book' id = Column(String(20), primary_key=True) name = Column(String(20)) # “多”的一方的book表是通过外键关联到user表的: user_id = Column(String(20), ForeignKey('user.id'))当我们查询一个User对象时,该对象的books属性将返回一个包含若干个Book对象的list。
小结ORM框架的作用就是把数据库表的一行记录与一个对象互相做自动转换。
正确使用ORM的前提是了解关系数据库的原理。
作者: 八戒你干嘛 时间: 2017-7-6 15:24
Web开发【转】
最早的软件都是运行在大型机上的,软件使用者通过“哑终端”登陆到大型机上去运行软件。后来随着PC机的兴起,软件开始主要运行在桌面上,而数据库这样的软件运行在服务器端,这种Client/Server模式简称CS架构。
随着互联网的兴起,人们发现,CS架构不适合Web,最大的原因是Web应用程序的修改和升级非常迅速,而CS架构需要每个客户端逐个升级桌面App,因此,Browser/Server模式开始流行,简称BS架构。
在BS架构下,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。
当然,Web页面也具有极强的交互性。由于Web页面是用HTML编写的,而HTML具备超强的表现力,并且,服务器端升级后,客户端无需任何部署就可以使用到新的版本,因此,BS架构迅速流行起来。
今天,除了重量级的软件如Office,Photoshop等,大部分软件都以Web形式提供。比如,新浪提供的新闻、博客、微博等服务,均是Web应用。
Web应用开发可以说是目前软件开发中最重要的部分。Web开发也经历了好几个阶段:
静态Web页面:由文本编辑器直接编辑并生成静态的HTML页面,如果要修改Web页面的内容,就需要再次编辑HTML源文件,早期的互联网Web页面就是静态的;
CGI:由于静态Web页面无法与用户交互,比如用户填写了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了Common Gateway Interface,简称CGI,用C/C++编写。
ASP/JSP/PHP:由于Web应用特点是修改频繁,用C/C++这样的低级语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。
MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了Model-View-Controller的模式,来简化Web开发。ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。
目前,Web开发技术仍在快速发展中,异步开发、新的MVVM前端技术层出不穷。
Python的诞生历史比Web还要早,由于Python是一种解释型的脚本语言,开发效率高,所以非常适合用来做Web开发。
Python有上百种Web开发框架,有很多成熟的模板技术,选择Python开发Web应用,不但开发效率高,而且运行速度快。
本章我们会详细讨论Python Web开发技术。
作者: 八戒你干嘛 时间: 2017-7-6 15:27
本帖最后由 八戒你干嘛 于 2017-7-6 16:24 编辑
HTTP协议简介[转]
在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器,让浏览器显示出来。而浏览器和服务器之间的传输协议是HTTP,所以:
在举例子之前,我们需要安装Google的Chrome浏览器。
为什么要使用Chrome浏览器而不是IE呢?因为IE实在是太慢了,并且,IE对于开发和调试Web应用程序完全是一点用也没有。
我们需要在浏览器很方便地调试我们的Web应用,而Chrome提供了一套完整地调试工具,非常适合Web开发。
安装好Chrome浏览器后,打开Chrome,在菜单中选择“视图”,“开发者”,“开发者工具”,就可以显示开发者工具:
[attach]107050[/attach]
Elements显示网页的结构,Network显示浏览器和服务器的通信。我们点Network,确保第一个小红灯亮着,Chrome就会记录所有浏览器和服务器之间的通信:
[attach]107056[/attach]
当我们在地址栏输入www.sina.com.cn时,浏览器将显示新浪的首页。在这个过程中,浏览器都干了哪些事情呢?通过Network的记录,我们就可以知道。在Network中,定位到第一条记录,点击,右侧将显示Request Headers,点击右侧的view source,我们就可以看到浏览器发给新浪服务器的请求: [attach]107051[/attach]
最主要的头两行分析如下,第一行:
GET / HTTP/1.1GET表示一个读取请求,将从服务器获得网页数据,/表示URL的路径,URL总是以/开头,/就表示首页,最后的HTTP/1.1指示采用的HTTP协议版本是1.1。目前HTTP协议的版本就是1.1,但是大部分服务器也支持1.0版本,主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度。
从第二行开始,每一行都类似于Xxx: abcdefg:
Host: www.sina.com.cn继续往下找到Response Headers,点击view source,显示服务器返回的原始响应数据:
[attach]107052[/attach]
HTTP响应分为Header和Body两部分(Body是可选项),我们在Network中看到的Header最重要的几行如下:
200 OK200表示一个成功的响应,后面的OK是说明。失败的响应有404 Not Found:网页不存在,500 Internal Server Error:服务器内部出错,等等。
Content-Type: text/htmlContent-Type指示响应的内容,这里是text/html表示HTML网页。请注意,浏览器就是依靠Content-Type来判断响应的内容是网页还是图片,是视频还是音乐。浏览器并不靠URL来判断响应的内容,所以,即使URL是,它也不一定就是图片。 HTTP响应的Body就是HTML源码,我们在菜单栏选择“视图”,“开发者”,“查看网页源码”就可以在浏览器中直接查看HTML源码:
[attach]107055[/attach]
当浏览器读取到新浪首页的HTML源码后,它会解析HTML,显示页面,然后,根据HTML里面的各种链接,再发送HTTP请求给新浪服务器,拿到相应的图片、视频、Flash、JavaScript脚本、CSS等各种资源,最终显示出一个完整的页面。所以我们在Network下面能看到很多额外的HTTP请求。
HTTP请求跟踪了新浪的首页,我们来总结一下HTTP请求的流程:
步骤1:浏览器首先向服务器发送HTTP请求,请求包括:
方法:GET还是POST,GET仅请求资源,POST会附带用户数据;
路径:/full/url/path;
以及其他相关的Header;
如果是POST,那么请求还包括一个Body,包含用户数据。
步骤2:服务器向浏览器返回HTTP响应,响应包括:
响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;
响应类型:由Content-Type指定;
以及其他相关的Header;
通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中。
步骤3:如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤1、2。
Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。当我们编写一个页面时,我们只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等,浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源。
HTTP协议同时具备极强的扩展性,虽然浏览器请求的是http://www.sina.com.cn/首页,但是新浪在HTML中可以链入其他服务器的资源,比如<img src="http://i1.sinaimg.cn/home/2013/1008/U8455P30DT20131008135420.png">,从而将请求压力分散到各个服务器上,并且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称WWW。 HTTP格式每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。
HTTP协议是一种文本协议,所以,它的格式也非常简单。HTTP GET请求的格式:
GET /path HTTP/1.1Header1: Value1Header2: Value2Header3: Value3每个Header一行一个,换行符是\r\n。
HTTP POST请求的格式:
POST /path HTTP/1.1Header1: Value1Header2: Value2Header3: Value3body data goes here...当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。
HTTP响应的格式:
200 OKHeader1: Value1Header2: Value2Header3: Value3body data goes here...HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。
作者: 八戒你干嘛 时间: 2017-7-6 15:33
本帖最后由 八戒你干嘛 于 2017-7-6 16:26 编辑
HTML简介[转]
网页就是HTML?这么理解大概没错。因为网页中不但包含文字,还有图片、视频、Flash小游戏,有复杂的排版、动画效果,所以,HTML定义了一套语法规则,来告诉浏览器如何把一个丰富多彩的页面显示出来。
HTML长什么样?上次我们看了新浪首页的HTML源码,如果仔细数数,竟然有6000多行!
所以,学HTML,就不要指望从新浪入手了。我们来看看最简单的HTML长什么样:
<html><head> <title>Hello</title></head><body> <h1>Hello, world!</h1></body></html>可以用文本编辑器编写HTML,然后保存为hello.html,双击或者把文件拖到浏览器中,就可以看到效果:
[attach]107057[/attach]
HTML文档就是一系列的Tag组成,最外层的Tag是<html>。规范的HTML也包含<head>...</head>和<body>...</body>(注意不要和HTTP的Header、Body搞混了),由于HTML是富文档模型,所以,还有一系列的Tag用来表示链接、图片、表格、表单等等。
CSS简介CSS是Cascading Style Sheets(层叠样式表)的简称,CSS用来控制HTML里的所有元素如何展现,比如,给标题元素<h1>加一个样式,变成48号字体,灰色,带阴影:
<html><head> <title>Hello</title> <style> h1 { color: #333333; font-size: 48px; text-shadow: 3px 3px 3px #666666; } </style></head><body> <h1>Hello, world!</h1></body></html>效果如下:
[attach]107058[/attach]
JavaScript简介JavaScript虽然名称有个Java,但它和Java真的一点关系没有。JavaScript是为了让HTML具有交互性而作为脚本语言添加的,JavaScript既可以内嵌到HTML中,也可以从外部链接到HTML中。如果我们希望当用户点击标题时把标题变成红色,就必须通过JavaScript来实现:
<html><head> <title>Hello</title> <style> h1 { color: #333333; font-size: 48px; text-shadow: 3px 3px 3px #666666; } </style> <script> function change() { document.getElementsByTagName('h1')[0].style.color = '#ff0000'; } </script></head><body> <h1>Hello, world!</h1></body></html>点击标题后效果如下:
[attach]107059[/attach]
小结如果要学习Web开发,首先要对HTML、CSS和JavaScript作一定的了解。HTML定义了页面的内容,CSS来控制页面元素的样式,而JavaScript负责页面的交互逻辑。
讲解HTML、CSS和JavaScript就可以写3本书,对于优秀的Web开发人员来说,精通HTML、CSS和JavaScript是必须的。
当我们用Python或者其他语言开发Web应用时,我们就是要在服务器端动态创建出HTML,这样,浏览器就会向不同的用户显示出不同的Web页面。
作者: 八戒你干嘛 时间: 2017-7-6 15:35
本帖最后由 八戒你干嘛 于 2017-7-6 16:28 编辑
WSGI接口[转]
了解了HTTP协议和HTML文档,我们其实就明白了一个Web应用的本质就是:
所以,最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。
如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。
正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。
这个接口就是WSGI:Web Server Gateway Interface。
WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”:
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'<h1>Hello, web!</h1>']上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:
在application()函数中,调用:
start_response('200 OK', [('Content-Type', 'text/html')])就发送了HTTP响应的Header,注意Header只能发送一次,也就是只能调用一次start_response()函数。start_response()函数接收两个参数,一个是HTTP响应码,一个是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示。
通常情况下,都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。
然后,函数的返回值b'<h1>Hello, web!</h1>'将作为HTTP响应的Body发送给浏览器。
有了WSGI,我们关心的就是如何从environ这个dict对象拿到HTTP请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。
整个application()函数本身没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。
不过,等等,这个application()函数怎么调用?如果我们自己调用,两个参数environ和start_response我们没法提供,返回的bytes也没法发给浏览器。
所以application()函数必须由WSGI服务器来调用。有很多符合WSGI规范的服务器,我们可以挑选一个来用。但是现在,我们只想尽快测试一下我们编写的application()函数真的可以把HTML输出到浏览器,所以,要赶紧找一个最简单的WSGI服务器,把我们的Web应用程序跑起来。
好消息是Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。
运行WSGI服务我们先编写hello.py,实现Web应用程序的WSGI处理函数:
# hello.pydef application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'<h1>Hello, web!</h1>']然后,再编写一个server.py,负责启动WSGI服务器,加载application()函数:
# server.py# 从wsgiref模块导入:from wsgiref.simple_server import make_server# 导入我们自己编写的application函数:from hello import application# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:httpd = make_server('', 8000, application)print('Serving HTTP on port 8000...')# 开始监听HTTP请求:httpd.serve_forever()确保以上两个文件在同一个目录下,然后在命令行输入python server.py来启动WSGI服务器:
[attach]107060[/attach]
注意:如果8000端口已被其他程序占用,启动将失败,请修改成其他端口。
启动成功后,打开浏览器,输入http://localhost:8000/,就可以看到结果了:
[attach]107061[/attach]
在命令行可以看到wsgiref打印的log信息:
[attach]107062[/attach]
按Ctrl+C终止服务器。
如果你觉得这个Web应用太简单了,可以稍微改造一下,从environ里读取PATH_INFO,这样可以显示更加动态的内容:
# hello.pydef application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web') return [body.encode('utf-8')]你可以在地址栏输入用户名作为URL的一部分,将返回Hello, xxx!:
[attach]107063[/attach]
是不是有点Web App的感觉了?
小结无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。
复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化Web开发。
作者: 八戒你干嘛 时间: 2017-7-6 15:44
本帖最后由 八戒你干嘛 于 2017-7-6 16:30 编辑
使用Web框架[转]
了解了WSGI框架,我们发现:其实一个Web App,就是写一个WSGI的处理函数,针对每个HTTP请求进行响应。
但是如何处理HTTP请求不是问题,问题是如何处理100个不同的URL。
每一个URL可以对应GET和POST请求,当然还有PUT、DELETE等请求,但是我们通常只考虑最常见的GET和POST请求。
一个最简单的想法是从environ变量里取出HTTP请求的信息,然后逐个判断:
def application(environ, start_response): method = environ['REQUEST_METHOD'] path = environ['PATH_INFO'] if method=='GET' and path=='/': return handle_home(environ, start_response) if method=='POST' and path='/signin': return handle_signin(environ, start_response) ...只是这么写下去代码是肯定没法维护了。
代码这么写没法维护的原因是因为WSGI提供的接口虽然比HTTP接口高级了不少,但和Web App的处理逻辑比,还是比较低级,我们需要在WSGI接口之上能进一步抽象,让我们专注于用一个函数处理一个URL,至于URL到函数的映射,就交给Web框架来做。
由于用Python开发一个Web框架十分容易,所以Python有上百个开源的Web框架。这里我们先不讨论各种Web框架的优缺点,直接选择一个比较流行的Web框架——Flask来使用。
用Flask编写Web App比WSGI接口简单(这不是废话么,要是比WSGI还复杂,用框架干嘛?),我们先用pip安装Flask:
$ pip install flask然后写一个app.py,处理3个URL,分别是:
注意噢,同一个URL/signin分别有GET和POST两种请求,映射到两个处理函数中。
Flask通过Python的装饰器在内部自动地把URL和函数给关联起来,所以,我们写出来的代码就像这样:
from flask import Flaskfrom flask import requestapp = Flask(__name__)@app.route('/', methods=['GET', 'POST'])def home(): return '<h1>Home</h1>'@app.route('/signin', methods=['GET'])def signin_form(): return '''<form action="/signin" method="post"> <p><input name="username"></p> <p><input name="password" type="password"></p> <p><button type="submit">Sign In</button></p> </form>'''@app.route('/signin', methods=['POST'])def signin(): # 需要从request对象读取表单内容: if request.form['username']=='admin' and request.form['password']=='password': return '<h3>Hello, admin!</h3>' return '<h3>Bad username or password.</h3>'if __name__ == '__main__': app.run()运行python app.py,Flask自带的Server在端口5000上监听:
$ python app.py * Running on http://127.0.0.1:5000/打开浏览器,输入首页地址http://localhost:5000/:
[attach]107064[/attach]
首页显示正确!
再在浏览器地址栏输入http://localhost:5000/signin,会显示登录表单:
[attach]107065[/attach]
输入预设的用户名admin和口令password,登录成功:
[attach]107066[/attach]
输入其他错误的用户名和口令,登录失败:
[attach]107067[/attach]
实际的Web App应该拿到用户名和口令后,去数据库查询再比对,来判断用户是否能登录成功。
除了Flask,常见的Python Web框架还有:
当然了,因为开发Python的Web框架也不是什么难事,我们后面也会讲到开发Web框架的内容。
小结有了Web框架,我们在编写Web应用时,注意力就从WSGI处理函数转移到URL+对应的处理函数,这样,编写Web App就更加简单了。
在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。Web框架都提供了自己的API来实现这些功能。Flask通过request.form['name']来获取表单的内容。
作者: 八戒你干嘛 时间: 2017-7-6 15:52
本帖最后由 八戒你干嘛 于 2017-7-6 16:32 编辑
使用模板[转]
Web框架把我们从WSGI中拯救出来了。现在,我们只需要不断地编写函数,带上URL,就可以继续Web App的开发了。
但是,Web App不仅仅是处理逻辑,展示给用户的页面也非常重要。在函数中返回一个包含HTML的字符串,简单的页面还可以,但是,想想新浪首页的6000多行的HTML,你确信能在Python的字符串中正确地写出来么?反正我是做不到。
俗话说得好,不懂前端的Python工程师不是好的产品经理。有Web开发经验的同学都明白,Web App最复杂的部分就在HTML页面。HTML不仅要正确,还要通过CSS美化,再加上复杂的JavaScript脚本来实现各种交互和动画效果。总之,生成HTML页面的难度很大。
由于在Python代码里拼字符串是不现实的,所以,模板技术出现了。
使用模板,我们需要预先准备一个HTML文档,这个HTML文档不是普通的HTML,而是嵌入了一些变量和指令,然后,根据我们传入的数据,替换后,得到最终的HTML,发送给用户:
[attach]107068[/attach]
这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。
Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;
包含变量{{ name }}的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。
MVC中的Model在哪?Model是用来传给View的,这样View在替换变量的时候,就可以从Model中取出相应的数据。
上面的例子中,Model就是一个dict:
{ 'name': 'Michael' }只是因为Python支持关键字参数,很多Web框架允许传入关键字参数,然后,在框架内部组装出一个dict作为Model。
现在,我们把上次直接输出字符串作为HTML的例子用高端大气上档次的MVC模式改写一下:
from flask import Flask, request, render_templateapp = Flask(__name__)@app.route('/', methods=['GET', 'POST'])def home(): return render_template('home.html')@app.route('/signin', methods=['GET'])def signin_form(): return render_template('form.html')@app.route('/signin', methods=['POST'])def signin(): username = request.form['username'] password = request.form['password'] if username=='admin' and password=='password': return render_template('signin-ok.html', username=username) return render_template('form.html', message='Bad username or password', username=username)if __name__ == '__main__': app.run()Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2,所以我们先直接安装jinja2:
$ pip install jinja2然后,开始编写jinja2模板:
home.html用来显示首页的模板:
<html><head> <title>Home</title></head><body> <h1 style="font-style:italic">Home</h1></body></html>form.html用来显示登录表单的模板:
<html><head> <title>Please Sign In</title></head><body> {% if message %} <p style="color:red">{{ message }}</p> {% endif %} <form action="/signin" method="post"> <legend>Please sign in:</legend> <p><input name="username" placeholder="Username" value="{{ username }}"></p> <p><input name="password" placeholder="Password" type="password"></p> <p><button type="submit">Sign In</button></p> </form></body></html>signin-ok.html登录成功的模板:
<html><head> <title>Welcome, {{ username }}</title></head><body> <p>Welcome, {{ username }}!</p></body></html>登录失败的模板呢?我们在form.html中加了一点条件判断,把form.html重用为登录失败的模板。
最后,一定要把模板放到正确的templates目录下,templates和app.py在同级目录下:
[attach]107069[/attach]
启动python app.py,看看使用模板的页面效果:
[attach]107070[/attach]
通过MVC,我们在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样,我们就成功地把Python代码和HTML代码最大限度地分离了。
使用模板的另一大好处是,模板改起来很方便,而且,改完保存后,刷新浏览器就能看到最新的效果,这对于调试HTML、CSS和JavaScript的前端工程师来说实在是太重要了。
在Jinja2模板中,我们用{{ name }}表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用{% ... %}表示指令。
比如循环输出页码:
{% for i in page_list %} <a href="/page/{{ i }}">{{ i }}</a>{% endfor %}如果page_list是一个list:[1, 2, 3, 4, 5],上面的模板将输出5个超链接。
除了Jinja2,常见的模板还有:
Mako:用<% ... %>和${xxx}的一个模板;
Cheetah:也是用<% ... %>和${xxx}的一个模板;
Django:Django是一站式框架,内置一个用{% ... %}和{{ xxx }}的模板。
小结有了MVC,我们就分离了Python代码和HTML代码。HTML代码全部放到模板里,写起来更有效率。
作者: 八戒你干嘛 时间: 2017-7-6 16:58
异步IO【转】
在IO编程一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。
多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。
另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的:
do_some_code()f = open('/path/to/file', 'r')r = f.read() # <== 线程停在此处等待IO操作结果# IO操作完成后线程才能继续执行:do_some_code(r)所以,同步IO模型的代码是无法实现异步IO模型的。
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:
loop = get_event_loop()while True: event = loop.get_event() process_event(event)消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。
由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。
消息模型是如何解决同步IO必须等待IO操作这一问题的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。
在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
作者: 八戒你干嘛 时间: 2017-7-6 17:00
协程[转]
在学习异步IO模型前,我们先来了解协程。
协程,又称微线程,纤程。英文名Coroutine。
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:
def A(): print('1') print('2') print('3')def B(): print('x') print('y') print('z')假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
12xy3z但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。
看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。
但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = '200 OK'def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close()c = consumer()produce(c)执行结果:
[PRODUCER] Producing 1...[CONSUMER] Consuming 1...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 2...[CONSUMER] Consuming 2...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 3...[CONSUMER] Consuming 3...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 4...[CONSUMER] Consuming 4...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 5...[CONSUMER] Consuming 5...[PRODUCER] Consumer return: 200 OK注意到consumer函数是一个generator,把一个consumer传入produce后:
首先调用c.send(None)启动生成器;
然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
consumer通过yield拿到消息,处理,又通过yield把结果传回;
produce拿到consumer处理的结果,继续生产下一条消息;
produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点:
“子程序就是协程的一种特例。”
作者: 八戒你干嘛 时间: 2017-7-6 17:04
asyncio[转]
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
用asyncio实现Hello world代码如下:
import asyncio@asyncio.coroutinedef hello(): print("Hello world!") # 异步调用asyncio.sleep(1): r = yield from asyncio.sleep(1) print("Hello again!")# 获取EventLoop:loop = asyncio.get_event_loop()# 执行coroutineloop.run_until_complete(hello())loop.close()@asyncio.coroutine把一个generator标记为coroutine类型,然后,我们就把这个coroutine扔到EventLoop中执行。
hello()会首先打印出Hello world!,然后,yield from语法可以让我们方便地调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。
把asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。
我们用Task封装两个coroutine试试:
import threadingimport asyncio@asyncio.coroutinedef hello(): print('Hello world! (%s)' % threading.currentThread()) yield from asyncio.sleep(1) print('Hello again! (%s)' % threading.currentThread())loop = asyncio.get_event_loop()tasks = [hello(), hello()]loop.run_until_complete(asyncio.wait(tasks))loop.close()观察执行过程:
Hello world! (<_MainThread(MainThread, started 140735195337472)>)Hello world! (<_MainThread(MainThread, started 140735195337472)>)(暂停约1秒)Hello again! (<_MainThread(MainThread, started 140735195337472)>)Hello again! (<_MainThread(MainThread, started 140735195337472)>)由打印的当前线程名称可以看出,两个coroutine是由同一个线程并发执行的。
如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。
我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:
import asyncio@asyncio.coroutinedef wget(host): print('wget %s...' % host) connect = asyncio.open_connection(host, 80) reader, writer = yield from connect header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host writer.write(header.encode('utf-8')) yield from writer.drain() while True: line = yield from reader.readline() if line == b'\r\n': break print('%s header > %s' % (host, line.decode('utf-8').rstrip())) # Ignore the body, close the socket writer.close()loop = asyncio.get_event_loop()tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]loop.run_until_complete(asyncio.wait(tasks))loop.close()执行结果如下:
wget www.sohu.com...wget www.sina.com.cn...wget www.163.com...(等待一段时间)(打印出sohu的header)www.sohu.com header > HTTP/1.1 200 OKwww.sohu.com header > Content-Type: text/html...(打印出sina的header)www.sina.com.cn header > HTTP/1.1 200 OKwww.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT...(打印出163的header)www.163.com header > HTTP/1.0 302 Moved Temporarilywww.163.com header > Server: Cdn Cache Server V2.0...可见3个连接由一个线程通过coroutine并发完成。
小结asyncio提供了完善的异步IO支持;
异步操作需要在coroutine中通过yield from完成;
多个coroutine可以封装成一组Task然后并发执行。
作者: 八戒你干嘛 时间: 2017-7-6 17:05
async/await[转]
用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:
- 把@asyncio.coroutine替换为async;
- 把yield from替换为await。
让我们对比一下上一节的代码:
@asyncio.coroutinedef hello(): print("Hello world!") r = yield from asyncio.sleep(1) print("Hello again!")用新语法重新编写如下:
async def hello(): print("Hello world!") r = await asyncio.sleep(1) print("Hello again!")剩下的代码保持不变。
小结Python从3.5版本开始为asyncio提供了async和await的新语法;
注意新语法只能用在Python 3.5以及后续版本,如果使用3.4版本,则仍需使用上一节的方案。
练习将上一节的异步获取sina、sohu和163的网站首页源码用新语法重写并运行
作者: 八戒你干嘛 时间: 2017-7-6 17:07
aiohttp[转]
asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。
asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。
我们先安装aiohttp:
pip install aiohttp然后编写一个HTTP服务器,分别处理以下URL:
代码如下:
import asynciofrom aiohttp import webasync def index(request): await asyncio.sleep(0.5) return web.Response(body=b'<h1>Index</h1>')async def hello(request): await asyncio.sleep(0.5) text = '<h1>hello, %s!</h1>' % request.match_info['name'] return web.Response(body=text.encode('utf-8'))async def init(loop): app = web.Application(loop=loop) app.router.add_route('GET', '/', index) app.router.add_route('GET', '/hello/{name}', hello) srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000) print('Server started at http://127.0.0.1:8000...') return srvloop = asyncio.get_event_loop()loop.run_until_complete(init(loop))loop.run_forever()注意aiohttp的初始化函数init()也是一个coroutine,loop.create_server()则利用asyncio创建TCP服务。
作者: 八戒你干嘛 时间: 2017-7-6 17:12
实战【转】
看完了教程,是不是有这么一种感觉:看的时候觉得很简单,照着教程敲代码也没啥大问题。
于是准备开始独立写代码,就发现不知道从哪开始下手了。
这种情况是完全正常的。好比学写作文,学的时候觉得简单,写的时候就无从下笔了。
虽然这个教程是面向小白的零基础Python教程,但是我们的目标不是学到60分,而是学到90分。
所以,用Python写一个真正的Web App吧!
目标我们设定的实战目标是一个Blog网站,包含日志、用户和评论3大部分。
很多童鞋会想,这是不是太简单了?
比如webpy.org上就提供了一个Blog的例子,目测也就100行代码。
但是,这样的页面:
[attach]107086[/attach]
你拿得出手么?
我们要写出用户真正看得上眼的页面,首页长得像这样:
[attach]107087[/attach]
评论区:
[attach]107088[/attach]
还有极其强大的后台管理页面:
[attach]107089[/attach]
是不是一下子变得高端大气上档次了?
项目名称必须是高端大气上档次的名称,命名为awesome-python3-webapp。
项目计划项目计划开发周期为16天。每天,你需要完成教程中的内容。如果你觉得编写代码难度实在太大,可以参考一下当天在GitHub上的代码。
以此类推,要预览awesome-python3-webapp的最终页面效果。
作者: jingzizx 时间: 2017-7-6 21:57
作者: 八戒你干嘛 时间: 2017-7-7 09:19
Day 1 - 搭建开发环境[转]
搭建开发环境首先,确认系统安装的Python版本是3.5.x:
$ python3 --versionPython 3.5.1然后,用pip安装开发Web App需要的第三方库:
异步框架aiohttp:
$pip3 install aiohttp前端模板引擎jinja2:
$ pip3 install jinja2MySQL 5.x数据库,从官方网站下载并安装,安装完毕后,请务必牢记root口令。为避免遗忘口令,建议直接把root口令设置为password;
MySQL的Python异步驱动程序aiomysql:
$ pip3 install aiomysql项目结构选择一个工作目录,然后,我们建立如下的目录结构:
awesome-python3-webapp/ <-- 根目录|+- backup/ <-- 备份目录|+- conf/ <-- 配置文件|+- dist/ <-- 打包目录|+- www/ <-- Web目录,存放.py文件| || +- static/ <-- 存放静态文件| || +- templates/ <-- 存放模板文件|+- ios/ <-- 存放iOS App工程|+- LICENSE <-- 代码LICENSE创建好项目的目录结构后,建议同时建立git仓库并同步至GitHub,保证代码修改的安全。
要了解git和GitHub的用法,请移步Git教程。
开发工具自备,推荐用Sublime Text,请参考使用文本编辑器。
作者: 八戒你干嘛 时间: 2017-7-7 09:25
Day 2 - 编写Web App骨架[转]
由于我们的Web App建立在asyncio的基础上,因此用aiohttp写一个基本的app.py:
import logging; logging.basicConfig(level=logging.INFO)import asyncio, os, json, timefrom datetime import datetimefrom aiohttp import webdef index(request): return web.Response(body=b'<h1>Awesome</h1>')@asyncio.coroutinedef init(loop): app = web.Application(loop=loop) app.router.add_route('GET', '/', index) srv = yield from loop.create_server(app.make_handler(), '127.0.0.1', 9000) logging.info('server started at http://127.0.0.1:9000...') return srvloop = asyncio.get_event_loop()loop.run_until_complete(init(loop))loop.run_forever()运行python app.py,Web App将在9000端口监听HTTP请求,并且对首页/进行响应:
$ python3 app.pyINFO:root:server started at http://127.0.0.1:9000...这里我们简单地返回一个Awesome字符串,在浏览器中可以看到效果:
[attach]107091[/attach]
这说明我们的Web App骨架已经搭好了,可以进一步往里面添加更多的东西。
作者: 八戒你干嘛 时间: 2017-7-7 09:30
Day 3 - 编写ORM[转]
在一个Web App中,所有数据,包括用户信息、发布的日志、评论等,都存储在数据库中。在awesome-python3-webapp中,我们选择MySQL作为数据库。
Web App里面有很多地方都要访问数据库。访问数据库需要创建数据库连接、游标对象,然后执行SQL语句,最后处理异常,清理资源。这些访问数据库的代码如果分散到各个函数中,势必无法维护,也不利于代码复用。
所以,我们要首先把常用的SELECT、INSERT、UPDATE和DELETE操作用函数封装起来。
由于Web框架使用了基于asyncio的aiohttp,这是基于协程的异步模型。在协程中,不能调用普通的同步IO操作,因为所有用户都是由一个线程服务的,协程的执行速度必须非常快,才能处理大量用户的请求。而耗时的IO操作不能在协程中以同步的方式调用,否则,等待一个IO操作时,系统无法响应任何其他用户。
这就是异步编程的一个原则:一旦决定使用异步,则系统每一层都必须是异步,“开弓没有回头箭”。
幸运的是aiomysql为MySQL数据库提供了异步IO的驱动。
创建连接池我们需要创建一个全局的连接池,每个HTTP请求都可以从连接池中直接获取数据库连接。使用连接池的好处是不必频繁地打开和关闭数据库连接,而是能复用就尽量复用。
连接池由全局变量__pool存储,缺省情况下将编码设置为utf8,自动提交事务:
@asyncio.coroutinedef create_pool(loop, **kw): logging.info('create database connection pool...') global __pool __pool = yield from aiomysql.create_pool( host=kw.get('host', 'localhost'), port=kw.get('port', 3306), user=kw['user'], password=kw['password'], db=kw['db'], charset=kw.get('charset', 'utf8'), autocommit=kw.get('autocommit', True), maxsize=kw.get('maxsize', 10), minsize=kw.get('minsize', 1), loop=loop )Select要执行SELECT语句,我们用select函数执行,需要传入SQL语句和SQL参数:
@asyncio.coroutinedef select(sql, args, size=None): log(sql, args) global __pool with (yield from __pool) as conn: cur = yield from conn.cursor(aiomysql.DictCursor) yield from cur.execute(sql.replace('?', '%s'), args or ()) if size: rs = yield from cur.fetchmany(size) else: rs = yield from cur.fetchall() yield from cur.close() logging.info('rows returned: %s' % len(rs)) return rsSQL语句的占位符是?,而MySQL的占位符是%s,select()函数在内部自动替换。注意要始终坚持使用带参数的SQL,而不是自己拼接SQL字符串,这样可以防止SQL注入攻击。
注意到yield from将调用一个子协程(也就是在一个协程中调用另一个协程)并直接获得子协程的返回结果。
如果传入size参数,就通过fetchmany()获取最多指定数量的记录,否则,通过fetchall()获取所有记录。
Insert, Update, Delete要执行INSERT、UPDATE、DELETE语句,可以定义一个通用的execute()函数,因为这3种SQL的执行都需要相同的参数,以及返回一个整数表示影响的行数:
@asyncio.coroutinedef execute(sql, args): log(sql) with (yield from __pool) as conn: try: cur = yield from conn.cursor() yield from cur.execute(sql.replace('?', '%s'), args) affected = cur.rowcount yield from cur.close() except BaseException as e: raise return affectedexecute()函数和select()函数所不同的是,cursor对象不返回结果集,而是通过rowcount返回结果数。
ORM有了基本的select()和execute()函数,我们就可以开始编写一个简单的ORM了。
设计ORM需要从上层调用者角度来设计。
我们先考虑如何定义一个User对象,然后把数据库表users和它关联起来。
from orm import Model, StringField, IntegerFieldclass User(Model): __table__ = 'users' id = IntegerField(primary_key=True) name = StringField()注意到定义在User类中的__table__、id和name是类的属性,不是实例的属性。所以,在类级别上定义的属性用来描述User对象和表的映射关系,而实例属性必须通过__init__()方法去初始化,所以两者互不干扰:
# 创建实例:user = User(id=123, name='Michael')# 存入数据库:user.insert()# 查询所有User对象:users = User.findAll()
作者: 八戒你干嘛 时间: 2017-7-7 09:30
定义Model首先要定义的是所有ORM映射的基类Model:
class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kw): super(Model, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Model' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value def getValue(self, key): return getattr(self, key, None) def getValueOrDefault(self, key): value = getattr(self, key, None) if value is None: field = self.__mappings__[key] if field.default is not None: value = field.default() if callable(field.default) else field.default logging.debug('using default value for %s: %s' % (key, str(value))) setattr(self, key, value) return valueModel从dict继承,所以具备所有dict的功能,同时又实现了特殊方法__getattr__()和__setattr__(),因此又可以像引用普通字段那样写:
>>> user['id']123>>> user.id123以及Field和各种Field子类:
class Field(object): def __init__(self, name, column_type, primary_key, default): self.name = name self.column_type = column_type self.primary_key = primary_key self.default = default def __str__(self): return '<%s, %s:%s>' % (self.__class__.__name__, self.column_type, self.name)映射varchar的StringField:
class StringField(Field): def __init__(self, name=None, primary_key=False, default=None, ddl='varchar(100)'): super().__init__(name, ddl, primary_key, default)注意到Model只是一个基类,如何将具体的子类如User的映射信息读取出来呢?答案就是通过metaclass:ModelMetaclass:
class ModelMetaclass(type): def __new__(cls, name, bases, attrs): # 排除Model类本身: if name=='Model': return type.__new__(cls, name, bases, attrs) # 获取table名称: tableName = attrs.get('__table__', None) or name logging.info('found model: %s (table: %s)' % (name, tableName)) # 获取所有的Field和主键名: mappings = dict() fields = [] primaryKey = None for k, v in attrs.items(): if isinstance(v, Field): logging.info(' found mapping: %s ==> %s' % (k, v)) mappings[k] = v if v.primary_key: # 找到主键: if primaryKey: raise RuntimeError('Duplicate primary key for field: %s' % k) primaryKey = k else: fields.append(k) if not primaryKey: raise RuntimeError('Primary key not found.') for k in mappings.keys(): attrs.pop(k) escaped_fields = list(map(lambda f: '`%s`' % f, fields)) attrs['__mappings__'] = mappings # 保存属性和列的映射关系 attrs['__table__'] = tableName attrs['__primary_key__'] = primaryKey # 主键属性名 attrs['__fields__'] = fields # 除主键外的属性名 # 构造默认的SELECT, INSERT, UPDATE和DELETE语句: attrs['__select__'] = 'select `%s`, %s from `%s`' % (primaryKey, ', '.join(escaped_fields), tableName) attrs['__insert__'] = 'insert into `%s` (%s, `%s`) values (%s)' % (tableName, ', '.join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) + 1)) attrs['__update__'] = 'update `%s` set %s where `%s`=?' % (tableName, ', '.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f), fields)), primaryKey) attrs['__delete__'] = 'delete from `%s` where `%s`=?' % (tableName, primaryKey) return type.__new__(cls, name, bases, attrs)这样,任何继承自Model的类(比如User),会自动通过ModelMetaclass扫描映射关系,并存储到自身的类属性如__table__、__mappings__中。
然后,我们往Model类添加class方法,就可以让所有子类调用class方法:
class Model(dict): ... @classmethod @asyncio.coroutine def find(cls, pk): ' find object by primary key. ' rs = yield from select('%s where `%s`=?' % (cls.__select__, cls.__primary_key__), [pk], 1) if len(rs) == 0: return None return cls(**rs[0])User类现在就可以通过类方法实现主键查找:
user = yield from User.find('123')往Model类添加实例方法,就可以让所有子类调用实例方法:
class Model(dict): ... @asyncio.coroutine def save(self): args = list(map(self.getValueOrDefault, self.__fields__)) args.append(self.getValueOrDefault(self.__primary_key__)) rows = yield from execute(self.__insert__, args) if rows != 1: logging.warn('failed to insert record: affected rows: %s' % rows)这样,就可以把一个User实例存入数据库:
user = User(id=123, name='Michael')yield from user.save()最后一步是完善ORM,对于查找,我们可以实现以下方法:
以及update()和remove()方法。
所有这些方法都必须用@asyncio.coroutine装饰,变成一个协程。
调用时需要特别注意:
user.save()没有任何效果,因为调用save()仅仅是创建了一个协程,并没有执行它。一定要用:
yield from user.save()才真正执行了INSERT操作。
最后看看我们实现的ORM模块一共多少行代码?累计不到300多行。用Python写一个ORM是不是很容易呢?
作者: 八戒你干嘛 时间: 2017-7-7 09:37
Day 4 - 编写Model[转]
有了ORM,我们就可以把Web App需要的3个表用Model表示出来:
import time, uuidfrom orm import Model, StringField, BooleanField, FloatField, TextFielddef next_id(): return '%015d%s000' % (int(time.time() * 1000), uuid.uuid4().hex)class User(Model): __table__ = 'users' id = StringField(primary_key=True, default=next_id, ddl='varchar(50)') email = StringField(ddl='varchar(50)') passwd = StringField(ddl='varchar(50)') admin = BooleanField() name = StringField(ddl='varchar(50)') image = StringField(ddl='varchar(500)') created_at = FloatField(default=time.time)class Blog(Model): __table__ = 'blogs' id = StringField(primary_key=True, default=next_id, ddl='varchar(50)') user_id = StringField(ddl='varchar(50)') user_name = StringField(ddl='varchar(50)') user_image = StringField(ddl='varchar(500)') name = StringField(ddl='varchar(50)') summary = StringField(ddl='varchar(200)') content = TextField() created_at = FloatField(default=time.time)class Comment(Model): __table__ = 'comments' id = StringField(primary_key=True, default=next_id, ddl='varchar(50)') blog_id = StringField(ddl='varchar(50)') user_id = StringField(ddl='varchar(50)') user_name = StringField(ddl='varchar(50)') user_image = StringField(ddl='varchar(500)') content = TextField() created_at = FloatField(default=time.time)在编写ORM时,给一个Field增加一个default参数可以让ORM自己填入缺省值,非常方便。并且,缺省值可以作为函数对象传入,在调用save()时自动计算。
例如,主键id的缺省值是函数next_id,创建时间created_at的缺省值是函数time.time,可以自动设置当前日期和时间。
日期和时间用float类型存储在数据库中,而不是datetime类型,这么做的好处是不必关心数据库的时区以及时区转换问题,排序非常简单,显示的时候,只需要做一个float到str的转换,也非常容易。
初始化数据库表如果表的数量很少,可以手写创建表的SQL脚本:
-- schema.sqldrop database if exists awesome;create database awesome;use awesome;grant select, insert, update, delete on awesome.* to 'www-data'@'localhost' identified by 'www-data';create table users ( `id` varchar(50) not null, `email` varchar(50) not null, `passwd` varchar(50) not null, `admin` bool not null, `name` varchar(50) not null, `image` varchar(500) not null, `created_at` real not null, unique key `idx_email` (`email`), key `idx_created_at` (`created_at`), primary key (`id`)) engine=innodb default charset=utf8;create table blogs ( `id` varchar(50) not null, `user_id` varchar(50) not null, `user_name` varchar(50) not null, `user_image` varchar(500) not null, `name` varchar(50) not null, `summary` varchar(200) not null, `content` mediumtext not null, `created_at` real not null, key `idx_created_at` (`created_at`), primary key (`id`)) engine=innodb default charset=utf8;create table comments ( `id` varchar(50) not null, `blog_id` varchar(50) not null, `user_id` varchar(50) not null, `user_name` varchar(50) not null, `user_image` varchar(500) not null, `content` mediumtext not null, `created_at` real not null, key `idx_created_at` (`created_at`), primary key (`id`)) engine=innodb default charset=utf8;如果表的数量很多,可以从Model对象直接通过脚本自动生成SQL脚本,使用更简单。
把SQL脚本放到MySQL命令行里执行:
$ mysql -u root -p < schema.sql我们就完成了数据库表的初始化。
编写数据访问代码接下来,就可以真正开始编写代码操作对象了。比如,对于User对象,我们就可以做如下操作:
import ormfrom models import User, Blog, Commentdef test(): yield from orm.create_pool(user='www-data', password='www-data', database='awesome') u = User(name='Test', email='test@example.com', passwd='1234567890', image='about:blank') yield from u.save()for x in test(): pass可以在MySQL客户端命令行查询,看看数据是不是正常存储到MySQL里面了。
作者: 八戒你干嘛 时间: 2017-7-7 09:40
Day 5 - 编写Web框架[转]
在正式开始Web开发前,我们需要编写一个Web框架。
aiohttp已经是一个Web框架了,为什么我们还需要自己封装一个?
原因是从使用者的角度来说,aiohttp相对比较底层,编写一个URL的处理函数需要这么几步:
第一步,编写一个用@asyncio.coroutine装饰的函数:
@asyncio.coroutinedef handle_url_xxx(request): pass第二步,传入的参数需要自己从request中获取:
url_param = request.match_info['key']query_params = parse_qs(request.query_string)最后,需要自己构造Response对象:
text = render('template', data)return web.Response(text.encode('utf-8'))这些重复的工作可以由框架完成。例如,处理带参数的URL/blog/{id}可以这么写:
@get('/blog/{id}')def get_blog(id): pass处理query_string参数可以通过关键字参数**kw或者命名关键字参数接收:
@get('/api/comments')def api_comments(*, page='1'): pass对于函数的返回值,不一定是web.Response对象,可以是str、bytes或dict。
如果希望渲染模板,我们可以这么返回一个dict:
return { '__template__': 'index.html', 'data': '...'}因此,Web框架的设计是完全从使用者出发,目的是让使用者编写尽可能少的代码。
编写简单的函数而非引入request和web.Response还有一个额外的好处,就是可以单独测试,否则,需要模拟一个request才能测试。
@get和@post要把一个函数映射为一个URL处理函数,我们先定义@get():
def get(path): ''' Define decorator @get('/path') ''' def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): return func(*args, **kw) wrapper.__method__ = 'GET' wrapper.__route__ = path return wrapper return decorator这样,一个函数通过@get()的装饰就附带了URL信息。
@post与@get定义类似。
定义RequestHandlerURL处理函数不一定是一个coroutine,因此我们用RequestHandler()来封装一个URL处理函数。
RequestHandler是一个类,由于定义了__call__()方法,因此可以将其实例视为函数。
RequestHandler目的就是从URL函数中分析其需要接收的参数,从request中获取必要的参数,调用URL函数,然后把结果转换为web.Response对象,这样,就完全符合aiohttp框架的要求:
class RequestHandler(object): def __init__(self, app, fn): self._app = app self._func = fn ... @asyncio.coroutine def __call__(self, request): kw = ... 获取参数 r = yield from self._func(**kw) return r再编写一个add_route函数,用来注册一个URL处理函数:
def add_route(app, fn): method = getattr(fn, '__method__', None) path = getattr(fn, '__route__', None) if path is None or method is None: raise ValueError('@get or @post not defined in %s.' % str(fn)) if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn): fn = asyncio.coroutine(fn) logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys()))) app.router.add_route(method, path, RequestHandler(app, fn))最后一步,把很多次add_route()注册的调用:
add_route(app, handles.index)add_route(app, handles.blog)add_route(app, handles.create_comment)...变成自动扫描:
# 自动把handler模块的所有符合条件的函数注册了:add_routes(app, 'handlers')add_routes()定义如下:
def add_routes(app, module_name): n = module_name.rfind('.') if n == (-1): mod = __import__(module_name, globals(), locals()) else: name = module_name[n+1:] mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name) for attr in dir(mod): if attr.startswith('_'): continue fn = getattr(mod, attr) if callable(fn): method = getattr(fn, '__method__', None) path = getattr(fn, '__route__', None) if method and path: add_route(app, fn)最后,在app.py中加入middleware、jinja2模板和自注册的支持:
app = web.Application(loop=loop, middlewares=[ logger_factory, response_factory])init_jinja2(app, filters=dict(datetime=datetime_filter))add_routes(app, 'handlers')add_static(app)middlewaremiddleware是一种拦截器,一个URL在被某个函数处理前,可以经过一系列的middleware的处理。
一个middleware可以改变URL的输入、输出,甚至可以决定不继续处理而直接返回。middleware的用处就在于把通用的功能从每个URL处理函数中拿出来,集中放到一个地方。例如,一个记录URL日志的logger可以简单定义如下:
@asyncio.coroutinedef logger_factory(app, handler): @asyncio.coroutine def logger(request): # 记录日志: logging.info('Request: %s %s' % (request.method, request.path)) # 继续处理请求: return (yield from handler(request)) return logger而response这个middleware把返回值转换为web.Response对象再返回,以保证满足aiohttp的要求:
@asyncio.coroutinedef response_factory(app, handler): @asyncio.coroutine def response(request): # 结果: r = yield from handler(request) if isinstance(r, web.StreamResponse): return r if isinstance(r, bytes): resp = web.Response(body=r) resp.content_type = 'application/octet-stream' return resp if isinstance(r, str): resp = web.Response(body=r.encode('utf-8')) resp.content_type = 'text/html;charset=utf-8' return resp if isinstance(r, dict): ...有了这些基础设施,我们就可以专注地往handlers模块不断添加URL处理函数了,可以极大地提高开发效率。
作者: 八戒你干嘛 时间: 2017-7-7 09:41
Day 6 - 编写配置文件[转]
有了Web框架和ORM框架,我们就可以开始装配App了。
通常,一个Web App在运行时都需要读取配置文件,比如数据库的用户名、口令等,在不同的环境中运行时,Web App可以通过读取不同的配置文件来获得正确的配置。
由于Python本身语法简单,完全可以直接用Python源代码来实现配置,而不需要再解析一个单独的.properties或者.yaml等配置文件。
默认的配置文件应该完全符合本地开发环境,这样,无需任何设置,就可以立刻启动服务器。
我们把默认的配置文件命名为config_default.py:
# config_default.pyconfigs = { 'db': { 'host': '127.0.0.1', 'port': 3306, 'user': 'www-data', 'password': 'www-data', 'database': 'awesome' }, 'session': { 'secret': 'AwEsOmE' }}上述配置文件简单明了。但是,如果要部署到服务器时,通常需要修改数据库的host等信息,直接修改config_default.py不是一个好办法,更好的方法是编写一个config_override.py,用来覆盖某些默认设置:
# config_override.pyconfigs = { 'db': { 'host': '192.168.0.100' }}把config_default.py作为开发环境的标准配置,把config_override.py作为生产环境的标准配置,我们就可以既方便地在本地开发,又可以随时把应用部署到服务器上。
应用程序读取配置文件需要优先从config_override.py读取。为了简化读取配置文件,可以把所有配置读取到统一的config.py中:
# config.pyconfigs = config_default.configstry: import config_override configs = merge(configs, config_override.configs)except ImportError: pass这样,我们就完成了App的配置。
作者: 八戒你干嘛 时间: 2017-7-7 09:43
Day 7 - 编写MVC[转]
现在,ORM框架、Web框架和配置都已就绪,我们可以开始编写一个最简单的MVC,把它们全部启动起来。
通过Web框架的@get和ORM框架的Model支持,可以很容易地编写一个处理首页URL的函数:
@get('/')def index(request): users = yield from User.findAll() return { '__template__': 'test.html', 'users': users }'__template__'指定的模板文件是test.html,其他参数是传递给模板的数据,所以我们在模板的根目录templates下创建test.html:
<!DOCTYPE html><html><head> <meta charset="utf-8" /> <title>Test users - Awesome Python Webapp</title></head><body> <h1>All users</h1> {% for u in users %} <p>{{ u.name }} / {{ u.email }}</p> {% endfor %}</body></html>接下来,如果一切顺利,可以用命令行启动Web服务器:
$ python3 app.py然后,在浏览器中访问http://localhost:9000/。
如果数据库的users表什么内容也没有,你就无法在浏览器中看到循环输出的内容。可以自己在MySQL的命令行里给users表添加几条记录,然后再访问:
[attach]107092[/attach]
作者: 八戒你干嘛 时间: 2017-7-7 09:49
Day 8 - 构建前端[转]
虽然我们跑通了一个最简单的MVC,但是页面效果肯定不会让人满意。
对于复杂的HTML前端页面来说,我们需要一套基础的CSS框架来完成页面布局和基本样式。另外,jQuery作为操作DOM的JavaScript库也必不可少。
从零开始写CSS不如直接从一个已有的功能完善的CSS框架开始。有很多CSS框架可供选择。我们这次选择uikit这个强大的CSS框架。它具备完善的响应式布局,漂亮的UI,以及丰富的HTML组件,让我们能轻松设计出美观而简洁的页面。
可以从uikit首页下载打包的资源文件。
所有的静态资源文件我们统一放到www/static目录下,并按照类别归类:
static/+- css/| +- addons/| | +- uikit.addons.min.css| | +- uikit.almost-flat.addons.min.css| | +- uikit.gradient.addons.min.css| +- awesome.css| +- uikit.almost-flat.addons.min.css| +- uikit.gradient.addons.min.css| +- uikit.min.css+- fonts/| +- fontawesome-webfont.eot| +- fontawesome-webfont.ttf| +- fontawesome-webfont.woff| +- FontAwesome.otf+- js/ +- awesome.js +- html5.js +- jquery.min.js +- uikit.min.js由于前端页面肯定不止首页一个页面,每个页面都有相同的页眉和页脚。如果每个页面都是独立的HTML模板,那么我们在修改页眉和页脚的时候,就需要把每个模板都改一遍,这显然是没有效率的。
常见的模板引擎已经考虑到了页面上重复的HTML部分的复用问题。有的模板通过include把页面拆成三部分:
<html> <% include file="inc_header.html" %> <% include file="index_body.html" %> <% include file="inc_footer.html" %></html>这样,相同的部分inc_header.html和inc_footer.html就可以共享。
但是include方法不利于页面整体结构的维护。jinjia2的模板还有另一种“继承”方式,实现模板的复用更简单。
“继承”模板的方式是通过编写一个“父模板”,在父模板中定义一些可替换的block(块)。然后,编写多个“子模板”,每个子模板都可以只替换父模板定义的block。比如,定义一个最简单的父模板:
<!-- base.html --><html> <head> <title>{% block title%} 这里定义了一个名为title的block {% endblock %}</title> </head> <body> {% block content %} 这里定义了一个名为content的block {% endblock %} </body></html>对于子模板a.html,只需要把父模板的title和content替换掉:
{% extends 'base.html' %}{% block title %} A {% endblock %}{% block content %} <h1>Chapter A</h1> <p>blablabla...</p>{% endblock %}对于子模板b.html,如法炮制:
{% extends 'base.html' %}{% block title %} B {% endblock %}{% block content %} <h1>Chapter B</h1> <ul> <li>list 1</li> <li>list 2</li> </ul>{% endblock %}这样,一旦定义好父模板的整体布局和CSS样式,编写子模板就会非常容易。
让我们通过uikit这个CSS框架来完成父模板__base__.html的编写:
<!DOCTYPE html><html><head> <meta charset="utf-8" /> {% block meta %}<!-- block meta -->{% endblock %} <title>{% block title %} ? {% endblock %} - Awesome Python Webapp</title> <link rel="stylesheet" href="/static/css/uikit.min.css"> <link rel="stylesheet" href="/static/css/uikit.gradient.min.css"> <link rel="stylesheet" href="/static/css/awesome.css" /> <script src="/static/js/jquery.min.js"></script> <script src="/static/js/md5.js"></script> <script src="/static/js/uikit.min.js"></script> <script src="/static/js/awesome.js"></script> {% block beforehead %}<!-- before head -->{% endblock %}</head><body> <nav class="uk-navbar uk-navbar-attached uk-margin-bottom"> <div class="uk-container uk-container-center"> <a href="/" class="uk-navbar-brand">Awesome</a> <ul class="uk-navbar-nav"> <li data-url="blogs"><a href="/"><i class="uk-icon-home"></i> 日志</a></li> <li><a target="_blank" href="#"><i class="uk-icon-book"></i> 教程</a></li> <li><a target="_blank" href="#"><i class="uk-icon-code"></i> 源码</a></li> </ul> <div class="uk-navbar-flip"> <ul class="uk-navbar-nav"> {% if user %} <li class="uk-parent" data-uk-dropdown> <a href="#0"><i class="uk-icon-user"></i> {{ user.name }}</a> <div class="uk-dropdown uk-dropdown-navbar"> <ul class="uk-nav uk-nav-navbar"> <li><a href="/signout"><i class="uk-icon-sign-out"></i> 登出</a></li> </ul> </div> </li> {% else %} <li><a href="/signin"><i class="uk-icon-sign-in"></i> 登陆</a></li> <li><a href="/register"><i class="uk-icon-edit"></i> 注册</a></li> {% endif %} </ul> </div> </div> </nav> <div class="uk-container uk-container-center"> <div class="uk-grid"> <!-- content --> {% block content %} {% endblock %} <!-- // content --> </div> </div> <div class="uk-margin-large-top" style="background-color:#eee; border-top:1px solid #ccc;"> <div class="uk-container uk-container-center uk-text-center"> <div class="uk-panel uk-margin-top uk-margin-bottom"> <p> <a target="_blank" href="#" class="uk-icon-button uk-icon-weibo"></a> <a target="_blank" href="#" class="uk-icon-button uk-icon-github"></a> <a target="_blank" href="#" class="uk-icon-button uk-icon-linkedin-square"></a> <a target="_blank" href="#" class="uk-icon-button uk-icon-twitter"></a> </p> <p>Powered by <a href="#">Awesome Python Webapp</a>. Copyright © 2014. [<a href="/manage/" target="_blank">Manage</a>]</p> <p><a href="http://www.liaoxuefeng.com/" target="_blank">www.liaoxuefeng.com</a>. All rights reserved.</p> <a target="_blank" href="#"><i class="uk-icon-html5" style="font-size:64px; color: #444;"></i></a> </div> </div> </div></body></html>
作者: 八戒你干嘛 时间: 2017-7-7 09:50
__base__.html定义的几个block作用如下:
用于子页面定义一些meta,例如rss feed:
{% block meta %} ... {% endblock %}覆盖页面的标题:
{% block title %} ... {% endblock %}子页面可以在<head>标签关闭前插入JavaScript代码:
{% block beforehead %} ... {% endblock %}子页面的content布局和内容:
{% block content %} ...{% endblock %}我们把首页改造一下,从__base__.html继承一个blogs.html:
{% extends '__base__.html' %}{% block title %}日志{% endblock %}{% block content %} <div class="uk-width-medium-3-4"> {% for blog in blogs %} <article class="uk-article"> <h2><a href="/blog/{{ blog.id }}">{{ blog.name }}</a></h2> <p class="uk-article-meta">发表于{{ blog.created_at}}</p> <p>{{ blog.summary }}</p> <p><a href="/blog/{{ blog.id }}">继续阅读 <i class="uk-icon-angle-double-right"></i></a></p> </article> <hr class="uk-article-divider"> {% endfor %} </div> <div class="uk-width-medium-1-4"> <div class="uk-panel uk-panel-header"> <h3 class="uk-panel-title">友情链接</h3> <ul class="uk-list uk-list-line"> <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">编程</a></li> <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">读书</a></li> <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">Python教程</a></li> <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">Git教程</a></li> </ul> </div> </div>{% endblock %}相应地,首页URL的处理函数更新如下:
@get('/')def index(request): summary = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' blogs = [ Blog(id='1', name='Test Blog', summary=summary, created_at=time.time()-120), Blog(id='2', name='Something New', summary=summary, created_at=time.time()-3600), Blog(id='3', name='Learn Swift', summary=summary, created_at=time.time()-7200) ] return { '__template__': 'blogs.html', 'blogs': blogs }Blog的创建日期显示的是一个浮点数,因为它是由这段模板渲染出来的:
<p class="uk-article-meta">发表于{{ blog.created_at }}</p>解决方法是通过jinja2的filter(过滤器),把一个浮点数转换成日期字符串。我们来编写一个datetime的filter,在模板里用法如下:
<p class="uk-article-meta">发表于{{ blog.created_at|datetime }}</p>filter需要在初始化jinja2时设置。相关代码如下:
def datetime_filter(t): delta = int(time.time() - t) if delta < 60: return '1分钟前' if delta < 3600: return '%s分钟前' % (delta // 60) if delta < 86400: return '%s小时前' % (delta // 3600) if delta < 604800: return '%s天前' % (delta // 86400) dt = datetime.fromtimestamp(t) return '%s年%s月%s日' % (dt.year, dt.month, dt.day)...init_jinja2(app, filters=dict(datetime=datetime_filter))...现在,完善的首页显示如下:
[attach]107094[/attach]
作者: 八戒你干嘛 时间: 2017-7-7 09:52
Day 9 - 编写API[转]
自从Roy Fielding博士在2000年他的博士论文中提出REST(Representational State Transfer)风格的软件架构模式后,REST就基本上迅速取代了复杂而笨重的SOAP,成为Web API的标准了。
什么是Web API呢?
如果我们想要获取一篇Blog,输入http://localhost:9000/blog/123,就可以看到id为123的Blog页面,但这个结果是HTML页面,它同时混合包含了Blog的数据和Blog的展示两个部分。对于用户来说,阅读起来没有问题,但是,如果机器读取,就很难从HTML中解析出Blog的数据。
如果一个URL返回的不是HTML,而是机器能直接解析的数据,这个URL就可以看成是一个Web API。比如,读取http://localhost:9000/api/blogs/123,如果能直接返回Blog的数据,那么机器就可以直接读取。
REST就是一种设计API的模式。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。
编写API有什么好处呢?由于API就是把Web App的功能全部封装了,所以,通过API操作数据,可以极大地把前端和后端的代码隔离,使得后端代码易于测试,前端代码编写更简单。
一个API也是一个URL的处理函数,我们希望能直接通过一个@api来把函数变成JSON格式的REST API,这样,获取注册用户可以用一个API实现如下:
@get('/api/users')def api_get_users(*, page='1'): page_index = get_page_index(page) num = yield from User.findNumber('count(id)') p = Page(num, page_index) if num == 0: return dict(page=p, users=()) users = yield from User.findAll(orderBy='created_at desc', limit=(p.offset, p.limit)) for u in users: u.passwd = '******' return dict(page=p, users=users)只要返回一个dict,后续的response这个middleware就可以把结果序列化为JSON并返回。
我们需要对Error进行处理,因此定义一个APIError,这种Error是指API调用时发生了逻辑错误(比如用户不存在),其他的Error视为Bug,返回的错误代码为internalerror。
客户端调用API时,必须通过错误代码来区分API调用是否成功。错误代码是用来告诉调用者出错的原因。很多API用一个整数表示错误码,这种方式很难维护错误码,客户端拿到错误码还需要查表得知错误信息。更好的方式是用字符串表示错误代码,不需要看文档也能猜到错误原因。
可以在浏览器直接测试API,例如,输入http://localhost:9000/api/users,就可以看到返回的JSON:
[attach]107095[/attach]
作者: 八戒你干嘛 时间: 2017-7-7 09:57
Day 10 - 用户注册和登录[转]
用户管理是绝大部分Web网站都需要解决的问题。用户管理涉及到用户注册和登录。
用户注册相对简单,我们可以先通过API把用户注册这个功能实现了:
_RE_EMAIL = re.compile(r'^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$')_RE_SHA1 = re.compile(r'^[0-9a-f]{40}$')@post('/api/users')def api_register_user(*, email, name, passwd): if not name or not name.strip(): raise APIValueError('name') if not email or not _RE_EMAIL.match(email): raise APIValueError('email') if not passwd or not _RE_SHA1.match(passwd): raise APIValueError('passwd') users = yield from User.findAll('email=?', [email]) if len(users) > 0: raise APIError('register:failed', 'email', 'Email is already in use.') uid = next_id() sha1_passwd = '%s:%s' % (uid, passwd) user = User(id=uid, name=name.strip(), email=email, passwd=hashlib.sha1(sha1_passwd.encode('utf-8')).hexdigest(), image='http://www.gravatar.com/avatar/%s?d=mm&s=120' % hashlib.md5(email.encode('utf-8')).hexdigest()) yield from user.save() # make session cookie: r = web.Response() r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True) user.passwd = '******' r.content_type = 'application/json' r.body = json.dumps(user, ensure_ascii=False).encode('utf-8') return r注意用户口令是客户端传递的经过SHA1计算后的40位Hash字符串,所以服务器端并不知道用户的原始口令。
接下来可以创建一个注册页面,让用户填写注册表单,然后,提交数据到注册用户的API:
{% extends '__base__.html' %}{% block title %}注册{% endblock %}{% block beforehead %}<script>function validateEmail(email) { var re = /^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$/; return re.test(email.toLowerCase());}$(function () { var vm = new Vue({ el: '#vm', data: { name: '', email: '', password1: '', password2: '' }, methods: { submit: function (event) { event.preventDefault(); var $form = $('#vm'); if (! this.name.trim()) { return $form.showFormError('请输入名字'); } if (! validateEmail(this.email.trim().toLowerCase())) { return $form.showFormError('请输入正确的Email地址'); } if (this.password1.length < 6) { return $form.showFormError('口令长度至少为6个字符'); } if (this.password1 !== this.password2) { return $form.showFormError('两次输入的口令不一致'); } var email = this.email.trim().toLowerCase(); $form.postJSON('/api/users', { name: this.name.trim(), email: email, passwd: CryptoJS.SHA1(email + ':' + this.password1).toString() }, function (err, r) { if (err) { return $form.showFormError(err); } return location.assign('/'); }); } } }); $('#vm').show();});</script>{% endblock %}{% block content %} <div class="uk-width-2-3"> <h1>欢迎注册!</h1> <form id="vm" v-on="submit: submit" class="uk-form uk-form-stacked"> <div class="uk-alert uk-alert-danger uk-hidden"></div> <div class="uk-form-row"> <label class="uk-form-label">名字:</label> <div class="uk-form-controls"> <input v-model="name" type="text" maxlength="50" placeholder="名字" class="uk-width-1-1"> </div> </div> <div class="uk-form-row"> <label class="uk-form-label">电子邮件:</label> <div class="uk-form-controls"> <input v-model="email" type="text" maxlength="50" placeholder="your-name@example.com" class="uk-width-1-1"> </div> </div> <div class="uk-form-row"> <label class="uk-form-label">输入口令:</label> <div class="uk-form-controls"> <input v-model="password1" type="password" maxlength="50" placeholder="输入口令" class="uk-width-1-1"> </div> </div> <div class="uk-form-row"> <label class="uk-form-label">重复口令:</label> <div class="uk-form-controls"> <input v-model="password2" type="password" maxlength="50" placeholder="重复口令" class="uk-width-1-1"> </div> </div> <div class="uk-form-row"> <button type="submit" class="uk-button uk-button-primary"><i class="uk-icon-user"></i> 注册</button> </div> </form> </div>{% endblock %}这样我们就把用户注册的功能完成了:
[attach]107096[/attach]
作者: 八戒你干嘛 时间: 2017-7-7 09:57
用户登录比用户注册复杂。由于HTTP协议是一种无状态协议,而服务器要跟踪用户状态,就只能通过cookie实现。大多数Web框架提供了Session功能来封装保存用户状态的cookie。
Session的优点是简单易用,可以直接从Session中取出用户登录信息。
Session的缺点是服务器需要在内存中维护一个映射表来存储用户登录信息,如果有两台以上服务器,就需要对Session做集群,因此,使用Session的Web App很难扩展。
我们采用直接读取cookie的方式来验证用户登录,每次用户访问任意URL,都会对cookie进行验证,这种方式的好处是保证服务器处理任意的URL都是无状态的,可以扩展到多台服务器。
由于登录成功后是由服务器生成一个cookie发送给浏览器,所以,要保证这个cookie不会被客户端伪造出来。
实现防伪造cookie的关键是通过一个单向算法(例如SHA1),举例如下:
当用户输入了正确的口令登录成功后,服务器可以从数据库取到用户的id,并按照如下方式计算出一个字符串:
"用户id" + "过期时间" + SHA1("用户id" + "用户口令" + "过期时间" + "SecretKey")当浏览器发送cookie到服务器端后,服务器可以拿到的信息包括:
如果未到过期时间,服务器就根据用户id查找用户口令,并计算:
SHA1("用户id" + "用户口令" + "过期时间" + "SecretKey")并与浏览器cookie中的MD5进行比较,如果相等,则说明用户已登录,否则,cookie就是伪造的。
这个算法的关键在于SHA1是一种单向算法,即可以通过原始字符串计算出SHA1结果,但无法通过SHA1结果反推出原始字符串。
所以登录API可以实现如下:
@post('/api/authenticate')def authenticate(*, email, passwd): if not email: raise APIValueError('email', 'Invalid email.') if not passwd: raise APIValueError('passwd', 'Invalid password.') users = yield from User.findAll('email=?', [email]) if len(users) == 0: raise APIValueError('email', 'Email not exist.') user = users[0] # check passwd: sha1 = hashlib.sha1() sha1.update(user.id.encode('utf-8')) sha1.update(b':') sha1.update(passwd.encode('utf-8')) if user.passwd != sha1.hexdigest(): raise APIValueError('passwd', 'Invalid password.') # authenticate ok, set cookie: r = web.Response() r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True) user.passwd = '******' r.content_type = 'application/json' r.body = json.dumps(user, ensure_ascii=False).encode('utf-8') return r# 计算加密cookie:def user2cookie(user, max_age): # build cookie string by: id-expires-sha1 expires = str(int(time.time() + max_age)) s = '%s-%s-%s-%s' % (user.id, user.passwd, expires, _COOKIE_KEY) L = [user.id, expires, hashlib.sha1(s.encode('utf-8')).hexdigest()] return '-'.join(L)对于每个URL处理函数,如果我们都去写解析cookie的代码,那会导致代码重复很多次。
利用middle在处理URL之前,把cookie解析出来,并将登录用户绑定到request对象上,这样,后续的URL处理函数就可以直接拿到登录用户:
@asyncio.coroutinedef auth_factory(app, handler): @asyncio.coroutine def auth(request): logging.info('check user: %s %s' % (request.method, request.path)) request.__user__ = None cookie_str = request.cookies.get(COOKIE_NAME) if cookie_str: user = yield from cookie2user(cookie_str) if user: logging.info('set current user: %s' % user.email) request.__user__ = user return (yield from handler(request)) return auth# 解密cookie:@asyncio.coroutinedef cookie2user(cookie_str): ''' Parse cookie and load user if cookie is valid. ''' if not cookie_str: return None try: L = cookie_str.split('-') if len(L) != 3: return None uid, expires, sha1 = L if int(expires) < time.time(): return None user = yield from User.find(uid) if user is None: return None s = '%s-%s-%s-%s' % (uid, user.passwd, expires, _COOKIE_KEY) if sha1 != hashlib.sha1(s.encode('utf-8')).hexdigest(): logging.info('invalid sha1') return None user.passwd = '******' return user except Exception as e: logging.exception(e) return None这样,我们就完成了用户注册和登录的功能。
作者: 八戒你干嘛 时间: 2017-7-7 10:01
Day 11 - 编写日志创建页[转]
在Web开发中,后端代码写起来其实是相当容易的。
例如,我们编写一个REST API,用于创建一个Blog:
@post('/api/blogs')def api_create_blog(request, *, name, summary, content): check_admin(request) if not name or not name.strip(): raise APIValueError('name', 'name cannot be empty.') if not summary or not summary.strip(): raise APIValueError('summary', 'summary cannot be empty.') if not content or not content.strip(): raise APIValueError('content', 'content cannot be empty.') blog = Blog(user_id=request.__user__.id, user_name=request.__user__.name, user_image=request.__user__.image, name=name.strip(), summary=summary.strip(), content=content.strip()) yield from blog.save() return blog编写后端Python代码不但很简单,而且非常容易测试,上面的API:api_create_blog()本身只是一个普通函数。
Web开发真正困难的地方在于编写前端页面。前端页面需要混合HTML、CSS和JavaScript,如果对这三者没有深入地掌握,编写的前端页面将很快难以维护。
更大的问题在于,前端页面通常是动态页面,也就是说,前端页面往往是由后端代码生成的。
生成前端页面最早的方式是拼接字符串:
s = '<html><head><title>' + title + '</title></head><body>' + body + '</body></html>'显然这种方式完全不具备可维护性。所以有第二种模板方式:
<html><head> <title>{{ title }}</title></head><body> {{ body }}</body></html>ASP、JSP、PHP等都是用这种模板方式生成前端页面。
如果在页面上大量使用JavaScript(事实上大部分页面都会),模板方式仍然会导致JavaScript代码与后端代码绑得非常紧密,以至于难以维护。其根本原因在于负责显示的HTML DOM模型与负责数据和交互的JavaScript代码没有分割清楚。
要编写可维护的前端代码绝非易事。和后端结合的MVC模式已经无法满足复杂页面逻辑的需要了,所以,新的MVVM:Model View ViewModel模式应运而生。
MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示:
<script> var blog = { name: 'hello', summary: 'this is summary', content: 'this is content...' };</script>View是纯HTML:
<form action="/api/blogs" method="post"> <input name="name"> <input name="summary"> <textarea name="content"></textarea> <button type="submit">OK</button></form>由于Model表示数据,View负责显示,两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。
ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。
好消息是已有许多成熟的MVVM框架,例如AngularJS,KnockoutJS等。我们选择Vue这个简单易用的MVVM框架来实现创建Blog的页面templates/manage_blog_edit.html:
{% extends '__base__.html' %}{% block title %}编辑日志{% endblock %}{% block beforehead %}<script>var ID = '{{ id }}', action = '{{ action }}';function initVM(blog) { var vm = new Vue({ el: '#vm', data: blog, methods: { submit: function (event) { event.preventDefault(); var $form = $('#vm').find('form'); $form.postJSON(action, this.$data, function (err, r) { if (err) { $form.showFormError(err); } else { return location.assign('/api/blogs/' + r.id); } }); } } }); $('#vm').show();}$(function () { if (ID) { getJSON('/api/blogs/' + ID, function (err, blog) { if (err) { return fatal(err); } $('#loading').hide(); initVM(blog); }); } else { $('#loading').hide(); initVM({ name: '', summary: '', content: '' }); }});</script>{% endblock %}{% block content %} <div class="uk-width-1-1 uk-margin-bottom"> <div class="uk-panel uk-panel-box"> <ul class="uk-breadcrumb"> <li><a href="/manage/comments">评论</a></li> <li><a href="/manage/blogs">日志</a></li> <li><a href="/manage/users">用户</a></li> </ul> </div> </div> <div id="error" class="uk-width-1-1"> </div> <div id="loading" class="uk-width-1-1 uk-text-center"> <span><i class="uk-icon-spinner uk-icon-medium uk-icon-spin"></i> 正在加载...</span> </div> <div id="vm" class="uk-width-2-3"> <form v-on="submit: submit" class="uk-form uk-form-stacked"> <div class="uk-alert uk-alert-danger uk-hidden"></div> <div class="uk-form-row"> <label class="uk-form-label">标题:</label> <div class="uk-form-controls"> <input v-model="name" name="name" type="text" placeholder="标题" class="uk-width-1-1"> </div> </div> <div class="uk-form-row"> <label class="uk-form-label">摘要:</label> <div class="uk-form-controls"> <textarea v-model="summary" rows="4" name="summary" placeholder="摘要" class="uk-width-1-1" style="resize:none;"></textarea> </div> </div> <div class="uk-form-row"> <label class="uk-form-label">内容:</label> <div class="uk-form-controls"> <textarea v-model="content" rows="16" name="content" placeholder="内容" class="uk-width-1-1" style="resize:none;"></textarea> </div> </div> <div class="uk-form-row"> <button type="submit" class="uk-button uk-button-primary"><i class="uk-icon-save"></i> 保存</button> <a href="/manage/blogs" class="uk-button"><i class="uk-icon-times"></i> 取消</a> </div> </form> </div>{% endblock %}初始化Vue时,我们指定3个参数:
el:根据选择器查找绑定的View,这里是#vm,就是id为vm的DOM,对应的是一个<div>标签;
data:JavaScript对象表示的Model,我们初始化为{ name: '', summary: '', content: ''};
methods:View可以触发的JavaScript函数,submit就是提交表单时触发的函数。
接下来,我们在<form>标签中,用几个简单的v-model,就可以让Vue把Model和View关联起来:
<!-- input的value和Model的name关联起来了 --><input v-model="name" class="uk-width-1-1">Form表单通过<form v-on="submit: submit">把提交表单的事件关联到submit方法。
作者: 八戒你干嘛 时间: 2017-7-7 10:02
需要特别注意的是,在MVVM中,Model和View是双向绑定的。如果我们在Form中修改了文本框的值,可以在Model中立刻拿到新的值。试试在表单中输入文本,然后在Chrome浏览器中打开JavaScript控制台,可以通过vm.name访问单个属性,或者通过vm.$data访问整个Model:
[attach]107101[/attach]
如果我们在JavaScript逻辑中修改了Model,这个修改会立刻反映到View上。试试在JavaScript控制台输入vm.name = 'MVVM简介',可以看到文本框的内容自动被同步了:
[attach]107102[/attach]
双向绑定是MVVM框架最大的作用。借助于MVVM,我们把复杂的显示逻辑交给框架完成。由于后端编写了独立的REST API,所以,前端用AJAX提交表单非常容易,前后端分离得非常彻底。
作者: 八戒你干嘛 时间: 2017-7-7 10:05
Day 12 - 编写日志列表页[转]
MVVM模式不但可用于Form表单,在复杂的管理页面中也能大显身手。例如,分页显示Blog的功能,我们先把后端代码写出来:
在apis.py中定义一个Page类用于存储分页信息:
class Page(object): def __init__(self, item_count, page_index=1, page_size=10): self.item_count = item_count self.page_size = page_size self.page_count = item_count // page_size + (1 if item_count % page_size > 0 else 0) if (item_count == 0) or (page_index > self.page_count): self.offset = 0 self.limit = 0 self.page_index = 1 else: self.page_index = page_index self.offset = self.page_size * (page_index - 1) self.limit = self.page_size self.has_next = self.page_index < self.page_count self.has_previous = self.page_index > 1 def __str__(self): return 'item_count: %s, page_count: %s, page_index: %s, page_size: %s, offset: %s, limit: %s' % (self.item_count, self.page_count, self.page_index, self.page_size, self.offset, self.limit) __repr__ = __str__在handlers.py中实现API:
@get('/api/blogs')def api_blogs(*, page='1'): page_index = get_page_index(page) num = yield from Blog.findNumber('count(id)') p = Page(num, page_index) if num == 0: return dict(page=p, blogs=()) blogs = yield from Blog.findAll(orderBy='created_at desc', limit=(p.offset, p.limit)) return dict(page=p, blogs=blogs)管理页面:
@get('/manage/blogs')def manage_blogs(*, page='1'): return { '__template__': 'manage_blogs.html', 'page_index': get_page_index(page) }模板页面首先通过API:GET /api/blogs?page=?拿到Model:
{ "page": { "has_next": true, "page_index": 1, "page_count": 2, "has_previous": false, "item_count": 12 }, "blogs": [...]}然后,通过Vue初始化MVVM:
<script>function initVM(data) { var vm = new Vue({ el: '#vm', data: { blogs: data.blogs, page: data.page }, methods: { edit_blog: function (blog) { location.assign('/manage/blogs/edit?id=' + blog.id); }, delete_blog: function (blog) { if (confirm('确认要删除“' + blog.name + '”?删除后不可恢复!')) { postJSON('/api/blogs/' + blog.id + '/delete', function (err, r) { if (err) { return alert(err.message || err.error || err); } refresh(); }); } } } }); $('#vm').show();}$(function() { getJSON('/api/blogs', { page: {{ page_index }} }, function (err, results) { if (err) { return fatal(err); } $('#loading').hide(); initVM(results); });});</script>View的容器是#vm,包含一个table,我们用v-repeat可以把Model的数组blogs直接变成多行的<tr>:
<div id="vm" class="uk-width-1-1"> <a href="/manage/blogs/create" class="uk-button uk-button-primary"><i class="uk-icon-plus"></i> 新日志</a> <table class="uk-table uk-table-hover"> <thead> <tr> <th class="uk-width-5-10">标题 / 摘要</th> <th class="uk-width-2-10">作者</th> <th class="uk-width-2-10">创建时间</th> <th class="uk-width-1-10">操作</th> </tr> </thead> <tbody> <tr v-repeat="blog: blogs" > <td> <a target="_blank" v-attr="href: '/blog/'+blog.id" v-text="blog.name"></a> </td> <td> <a target="_blank" v-attr="href: '/user/'+blog.user_id" v-text="blog.user_name"></a> </td> <td> <span v-text="blog.created_at.toDateTime()"></span> </td> <td> <a href="#0" v-on="click: edit_blog(blog)"><i class="uk-icon-edit"></i> <a href="#0" v-on="click: delete_blog(blog)"><i class="uk-icon-trash-o"></i> </td> </tr> </tbody> </table> <div v-component="pagination" v-with="page"></div></div>往Model的blogs数组中增加一个Blog元素,table就神奇地增加了一行;把blogs数组的某个元素删除,table就神奇地减少了一行。所有复杂的Model-View的映射逻辑全部由MVVM框架完成,我们只需要在HTML中写上v-repeat指令,就什么都不用管了。
可以把v-repeat="blog: blogs"看成循环代码,所以,可以在一个<tr>内部引用循环变量blog。v-text和v-attr指令分别用于生成文本和DOM节点属性。
完整的Blog列表页如下:
[attach]107103[/attach]
作者: 八戒你干嘛 时间: 2017-7-7 10:10
Day 13 - 提升开发效率[转]
现在,我们已经把一个Web App的框架完全搭建好了,从后端的API到前端的MVVM,流程已经跑通了。
在继续工作前,注意到每次修改Python代码,都必须在命令行先Ctrl-C停止服务器,再重启,改动才能生效。
在开发阶段,每天都要修改、保存几十次代码,每次保存都手动来这么一下非常麻烦,严重地降低了我们的开发效率。有没有办法让服务器检测到代码修改后自动重新加载呢?
Django的开发环境在Debug模式下就可以做到自动重新加载,如果我们编写的服务器也能实现这个功能,就能大大提升开发效率。
可惜的是,Django没把这个功能独立出来,不用Django就享受不到,怎么办?
其实Python本身提供了重新载入模块的功能,但不是所有模块都能被重新载入。另一种思路是检测www目录下的代码改动,一旦有改动,就自动重启服务器。
按照这个思路,我们可以编写一个辅助程序pymonitor.py,让它启动wsgiapp.py,并时刻监控www目录下的代码改动,有改动时,先把当前wsgiapp.py进程杀掉,再重启,就完成了服务器进程的自动重启。
要监控目录文件的变化,我们也无需自己手动定时扫描,Python的第三方库watchdog可以利用操作系统的API来监控目录文件的变化,并发送通知。我们先用pip安装:
$ pip3 install watchdog利用watchdog接收文件变化的通知,如果是.py文件,就自动重启wsgiapp.py进程。
利用Python自带的subprocess实现进程的启动和终止,并把输入输出重定向到当前进程的输入输出中:
#!/usr/bin/env python3# -*- coding: utf-8 -*-__author__ = 'Michael Liao'import os, sys, time, subprocessfrom watchdog.observers import Observerfrom watchdog.events import FileSystemEventHandlerdef log(s): print('[Monitor] %s' % s)class MyFileSystemEventHander(FileSystemEventHandler): def __init__(self, fn): super(MyFileSystemEventHander, self).__init__() self.restart = fn def on_any_event(self, event): if event.src_path.endswith('.py'): log('Python source file changed: %s' % event.src_path) self.restart()command = ['echo', 'ok']process = Nonedef kill_process(): global process if process: log('Kill process [%s]...' % process.pid) process.kill() process.wait() log('Process ended with code %s.' % process.returncode) process = Nonedef start_process(): global process, command log('Start process %s...' % ' '.join(command)) process = subprocess.Popen(command, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)def restart_process(): kill_process() start_process()def start_watch(path, callback): observer = Observer() observer.schedule(MyFileSystemEventHander(restart_process), path, recursive=True) observer.start() log('Watching directory %s...' % path) start_process() try: while True: time.sleep(0.5) except KeyboardInterrupt: observer.stop() observer.join()if __name__ == '__main__': argv = sys.argv[1:] if not argv: print('Usage: ./pymonitor your-script.py') exit(0) if argv[0] != 'python3': argv.insert(0, 'python3') command = argv path = os.path.abspath('.') start_watch(path, None)一共70行左右的代码,就实现了Debug模式的自动重新加载。用下面的命令启动服务器:
$ python3 pymonitor.py wsgiapp.py或者给pymonitor.py加上可执行权限,启动服务器:
$ ./pymonitor.py app.py在编辑器中打开一个.py文件,修改后保存,看看命令行输出,是不是自动重启了服务器:
$ ./pymonitor.py app.py [Monitor] Watching directory /Users/michael/Github/awesome-python3-webapp/www...[Monitor] Start process python app.py......INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...[Monitor] Python source file changed: /Users/michael/Github/awesome-python-webapp/www/handlers.py[Monitor] Kill process [2747]...[Monitor] Process ended with code -9.[Monitor] Start process python app.py......INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...现在,只要一保存代码,就可以刷新浏览器看到效果,大大提升了开发效率。
作者: 八戒你干嘛 时间: 2017-7-7 10:14
Day 14 - 完成Web App[转]
在Web App框架和基本流程跑通后,剩下的工作全部是体力活了:在Debug开发模式下完成后端所有API、前端所有页面。我们需要做的事情包括:
把当前用户绑定到request上,并对URL/manage/进行拦截,检查当前用户是否是管理员身份:
@asyncio.coroutinedef auth_factory(app, handler): @asyncio.coroutine def auth(request): logging.info('check user: %s %s' % (request.method, request.path)) request.__user__ = None cookie_str = request.cookies.get(COOKIE_NAME) if cookie_str: user = yield from cookie2user(cookie_str) if user: logging.info('set current user: %s' % user.email) request.__user__ = user if request.path.startswith('/manage/') and (request.__user__ is None or not request.__user__.admin): return web.HTTPFound('/signin') return (yield from handler(request)) return auth后端API包括:
获取日志:GET /api/blogs
创建日志:POST /api/blogs
修改日志:POST /api/blogs/:blog_id
删除日志:POST /api/blogs/:blog_id/delete
获取评论:GET /api/comments
创建评论:POST /api/blogs/:blog_id/comments
删除评论:POST /api/comments/:comment_id/delete
创建新用户:POST /api/users
获取用户:GET /api/users
管理页面包括:
用户浏览页面包括:
注册页:GET /register
登录页:GET /signin
注销页:GET /signout
首页:GET /
日志详情页:GET /blog/:blog_id
把所有的功能实现,我们第一个Web App就宣告完成!
作者: 八戒你干嘛 时间: 2017-7-7 10:20
Day 15 - 部署Web App[转]
作为一个合格的开发者,在本地环境下完成开发还远远不够,我们需要把Web App部署到远程服务器上,这样,广大用户才能访问到网站。
很多做开发的同学把部署这件事情看成是运维同学的工作,这种看法是完全错误的。首先,最近流行DevOps理念,就是说,开发和运维要变成一个整体。其次,运维的难度,其实跟开发质量有很大的关系。代码写得垃圾,运维再好也架不住天天挂掉。最后,DevOps理念需要把运维、监控等功能融入到开发中。你想服务器升级时不中断用户服务?那就得在开发时考虑到这一点。
下面,我们就来把awesome-python3-webapp部署到Linux服务器。
搭建Linux服务器要部署到Linux,首先得有一台Linux服务器。要在公网上体验的同学,可以在Amazon的AWS申请一台EC2虚拟机(免费使用1年),或者使用国内的一些云服务器,一般都提供Ubuntu Server的镜像。想在本地部署的同学,请安装虚拟机,推荐使用VirtualBox。
我们选择的Linux服务器版本是Ubuntu Server 14.04 LTS,原因是apt太简单了。如果你准备使用其他Linux版本,也没有问题。
Linux安装完成后,请确保ssh服务正在运行,否则,需要通过apt安装:
$ sudo apt-get install openssh-server有了ssh服务,就可以从本地连接到服务器上。建议把公钥复制到服务器端用户的.ssh/authorized_keys中,这样,就可以通过证书实现无密码连接。
部署方式利用Python自带的asyncio,我们已经编写了一个异步高性能服务器。但是,我们还需要一个高性能的Web服务器,这里选择Nginx,它可以处理静态资源,同时作为反向代理把动态请求交给Python代码处理。这个模型如下:
[attach]107106[/attach]
Nginx负责分发请求:
[attach]107107[/attach]
在服务器端,我们需要定义好部署的目录结构:
/+- srv/ +- awesome/ <-- Web App根目录 +- www/ <-- 存放Python源码 | +- static/ <-- 存放静态资源文件 +- log/ <-- 存放log在服务器上部署,要考虑到新版本如果运行不正常,需要回退到旧版本时怎么办。每次用新的代码覆盖掉旧的文件是不行的,需要一个类似版本控制的机制。由于Linux系统提供了软链接功能,所以,我们把www作为一个软链接,它指向哪个目录,哪个目录就是当前运行的版本:
[attach]107108[/attach]
而Nginx和python代码的配置文件只需要指向www目录即可。
Nginx可以作为服务进程直接启动,但app.py还不行,所以,Supervisor登场!Supervisor是一个管理进程的工具,可以随系统启动而启动服务,它还时刻监控服务进程,如果服务进程意外退出,Supervisor可以自动重启服务。
总结一下我们需要用到的服务有:
Nginx:高性能Web服务器+负责反向代理;
Supervisor:监控服务进程的工具;
MySQL:数据库服务。
在Linux服务器上用apt可以直接安装上述服务:
$ sudo apt-get install nginx supervisor python3 mysql-server然后,再把我们自己的Web App用到的Python库安装了:
$ sudo pip3 install jinja2 aiomysql aiohttp在服务器上创建目录/srv/awesome/以及相应的子目录。
在服务器上初始化MySQL数据库,把数据库初始化脚本schema.sql复制到服务器上执行:
$ mysql -u root -p < schema.sql服务器端准备就绪。
部署用FTP还是SCP还是rsync复制文件?如果你需要手动复制,用一次两次还行,一天如果部署50次不但慢、效率低,而且容易出错。
正确的部署方式是使用工具配合脚本完成自动化部署。Fabric就是一个自动化部署工具。由于Fabric是用Python 2.x开发的,所以,部署脚本要用Python 2.7来编写,本机还必须安装Python 2.7版本。
要用Fabric部署,需要在本机(是开发机器,不是Linux服务器)安装Fabric:
$ easy_install fabricLinux服务器上不需要安装Fabric,Fabric使用SSH直接登录服务器并执行部署命令。
下一步是编写部署脚本。Fabric的部署脚本叫fabfile.py,我们把它放到awesome-python-webapp的目录下,与www目录平级:
awesome-python-webapp/+- fabfile.py+- www/+- ...Fabric的脚本编写很简单,首先导入Fabric的API,设置部署时的变量:
# fabfile.pyimport os, refrom datetime import datetime# 导入Fabric API:from fabric.api import *# 服务器登录用户名:env.user = 'michael'# sudo用户为root:env.sudo_user = 'root'# 服务器地址,可以有多个,依次部署:env.hosts = ['192.168.0.3']# 服务器MySQL用户名和口令:db_user = 'www-data'db_password = 'www-data'然后,每个Python函数都是一个任务。我们先编写一个打包的任务:
_TAR_FILE = 'dist-awesome.tar.gz'def build(): includes = ['static', 'templates', 'transwarp', 'favicon.ico', '*.py'] excludes = ['test', '.*', '*.pyc', '*.pyo'] local('rm -f dist/%s' % _TAR_FILE) with lcd(os.path.join(os.path.abspath('.'), 'www')): cmd = ['tar', '--dereference', '-czvf', '../dist/%s' % _TAR_FILE] cmd.extend(['--exclude=\'%s\'' % ex for ex in excludes]) cmd.extend(includes) local(' '.join(cmd))Fabric提供local('...')来运行本地命令,with lcd(path)可以把当前命令的目录设定为lcd()指定的目录,注意Fabric只能运行命令行命令,Windows下可能需要Cgywin环境。
在awesome-python-webapp目录下运行:
$ fab build看看是否在dist目录下创建了dist-awesome.tar.gz的文件。
打包后,我们就可以继续编写deploy任务,把打包文件上传至服务器,解压,重置www软链接,重启相关服务:
_REMOTE_TMP_TAR = '/tmp/%s' % _TAR_FILE_REMOTE_BASE_DIR = '/srv/awesome'def deploy(): newdir = 'www-%s' % datetime.now().strftime('%y-%m-%d_%H.%M.%S') # 删除已有的tar文件: run('rm -f %s' % _REMOTE_TMP_TAR) # 上传新的tar文件: put('dist/%s' % _TAR_FILE, _REMOTE_TMP_TAR) # 创建新目录: with cd(_REMOTE_BASE_DIR): sudo('mkdir %s' % newdir) # 解压到新目录: with cd('%s/%s' % (_REMOTE_BASE_DIR, newdir)): sudo('tar -xzvf %s' % _REMOTE_TMP_TAR) # 重置软链接: with cd(_REMOTE_BASE_DIR): sudo('rm -f www') sudo('ln -s %s www' % newdir) sudo('chown www-data:www-data www') sudo('chown -R www-data:www-data %s' % newdir) # 重启Python服务和nginx服务器: with settings(warn_only=True): sudo('supervisorctl stop awesome') sudo('supervisorctl start awesome') sudo('/etc/init.d/nginx reload')注意run()函数执行的命令是在服务器上运行,with cd(path)和with lcd(path)类似,把当前目录在服务器端设置为cd()指定的目录。如果一个命令需要sudo权限,就不能用run(),而是用sudo()来执行。
作者: 八戒你干嘛 时间: 2017-7-7 10:21
配置Supervisor上面让Supervisor重启awesome的命令会失败,因为我们还没有配置Supervisor呢。
编写一个Supervisor的配置文件awesome.conf,存放到/etc/supervisor/conf.d/目录下:
[program:awesome]command = /srv/awesome/www/app.pydirectory = /srv/awesome/wwwuser = www-datastartsecs = 3redirect_stderr = truestdout_logfile_maxbytes = 50MBstdout_logfile_backups = 10stdout_logfile = /srv/awesome/log/app.log配置文件通过[program:awesome]指定服务名为awesome,command指定启动app.py。
然后重启Supervisor后,就可以随时启动和停止Supervisor管理的服务了:
$ sudo supervisorctl reload$ sudo supervisorctl start awesome$ sudo supervisorctl statusawesome RUNNING pid 1401, uptime 5:01:34配置NginxSupervisor只负责运行app.py,我们还需要配置Nginx。把配置文件awesome放到/etc/nginx/sites-available/目录下:
server { listen 80; # 监听80端口 root /srv/awesome/www; access_log /srv/awesome/log/access_log; error_log /srv/awesome/log/error_log; # server_name awesome.liaoxuefeng.com; # 配置域名 # 处理静态文件/favicon.ico: location /favicon.ico { root /srv/awesome/www; } # 处理静态资源: location ~ ^\/static\/.*$ { root /srv/awesome/www; } # 动态请求转发到9000端口: location / { proxy_pass http://127.0.0.1:9000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}然后在/etc/nginx/sites-enabled/目录下创建软链接:
$ pwd/etc/nginx/sites-enabled$ sudo ln -s /etc/nginx/sites-available/awesome .让Nginx重新加载配置文件,不出意外,我们的awesome-python3-webapp应该正常运行:
$ sudo /etc/init.d/nginx reload如果有任何错误,都可以在/srv/awesome/log下查找Nginx和App本身的log。如果Supervisor启动时报错,可以在/var/log/supervisor下查看Supervisor的log。
如果一切顺利,你可以在浏览器中访问Linux服务器上的awesome-python3-webapp了:
[attach]107110[/attach]
如果在开发环境更新了代码,只需要在命令行执行:
$ fab build$ fab deploy自动部署完成!刷新浏览器就可以看到服务器代码更新后的效果。
友情链接嫌国外网速慢的童鞋请移步网易和搜狐的镜像站点。
作者: 八戒你干嘛 时间: 2017-7-7 10:25
Day 16 - 编写移动App[转]
网站部署上线后,还缺点啥呢?
在移动互联网浪潮席卷而来的今天,一个网站没有上线移动App,出门根本不好意思跟人打招呼。
所以,awesome-python3-webapp必须得有一个移动App版本!
开发iPhone版本我们首先来看看如何开发iPhone App。前置条件:一台Mac电脑,安装XCode和最新的iOS SDK。
在使用MVVM编写前端页面时,我们就能感受到,用REST API封装网站后台的功能,不但能清晰地分离前端页面和后台逻辑,现在这个好处更加明显,移动App也可以通过REST API从后端拿到数据。
我们来设计一个简化版的iPhone App,包含两个屏幕:列出最新日志和阅读日志的详细内容:
[attach]107111[/attach]
只需要调用API:/api/blogs。
在XCode中完成App编写:
[attach]107112[/attach]
由于我们的教程是Python,关于如何开发iOS,请移步Develop Apps for iOS。
作者: 八戒你干嘛 时间: 2017-7-7 10:39
FAQ【转】
常见问题本节列出常见的一些问题。
如何获取当前路径当前路径可以用'.'表示,再用os.path.abspath()将其转换为绝对路径:
# -*- coding:utf-8 -*-# test.pyimport osprint(os.path.abspath('.'))运行结果:
$ python3 test.py /Users/michael/workspace/testing如何获取当前模块的文件名可以通过特殊变量__file__获取:
# -*- coding:utf-8 -*-# test.pyprint(__file__)输出:
$ python3 test.pytest.py如何获取命令行参数可以通过sys模块的argv获取:
# -*- coding:utf-8 -*-# test.pyimport sysprint(sys.argv)输出:
$ python3 test.py -a -s "Hello world"['test.py', '-a', '-s', 'Hello world']argv的第一个元素永远是命令行执行的.py文件名。
如何获取当前Python命令的可执行文件路径sys模块的executable变量就是Python命令可执行文件的路径:
# -*- coding:utf-8 -*-# test.pyimport sysprint(sys.executable)在Mac下的结果:
$ python3 test.py /usr/local/opt/python3/bin/python3.4
作者: 八戒你干嘛 时间: 2017-7-7 10:41
期末总结!
经过一段时间的学习,相信都能对Python已经有了初步掌握。一开始,可能觉得Python上手很容易,可是越往后学,会越困难,有的时候,发现理解不了代码,这时,不妨停下来思考一下,先把概念搞清楚,代码自然就明白了。
Python非常适合初学者用来进入计算机编程领域。Python属于非常高级的语言,掌握了这门高级语言,就对计算机编程的核心思想——抽象有了初步理解。如果希望继续深入学习计算机编程,可以学习Java、C、JavaScript、Lisp等不同类型的语言,只有多掌握不同领域的语言,有比较才更有收获。
[attach]107113[/attach]
作者: cyclone252 时间: 2018-6-20 14:34
谢谢分享
欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) |
Powered by Discuz! X3.2 |