面向对象oopclass Student(object):
def __init__(self,name,score)
self.name = name
self.score = score
def print_score(self)
print('%s: %s' % (self.name,self.score))
给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:
bart = Student(‘zhangyuang’,90)
lisa = Student(‘janvier’,90)
bart.print_score()
lisa.print_score()
类和实例class Student(object):
pass
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念一行再讲。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现
>>> bart = Student()
>>> bart
>>> Studeng
可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590
是内存地址,每个object的地址都不一样,而Student本身则是一个类。
可以自由的给一个实例变量绑定属性
>>> bart.name = 'zhangyuang'
>>> bart.name
'zhangyuang'
由于类可以起到模版的作用,因此在创建实例的时候,把一些我们认为必须绑定的属性强制天蝎进去。通过定义一个特殊的init方法,在创建实例的时候,就把name,score等属性绑上去:
>>> class Student(object):
def __init__(self,name,score):
self.name = name
self.score = score
注意到init方法的第一个参数永远是self.表示创建的实例本身,因此,在init方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
有了init方法,在创建实例的时候,就不能传入空的参数了,必须传入与init方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
>>> bart = Student('zhangyuang','90')
>>> bart.name
'zhangyuang'
>>> bart.score
90
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:
>>> def print_score(std):
print('%s: %s' % (std.name,std.score))
>>> print_score(bart)
zhangyuang:90
但是既然Student实例本身就拥有这些数据,要访问这些数据就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法:
class Student(object):
def init(self,name,score):
self.name = name
self.score = score
def print_score(self):
print(‘%s: %s’ % (self.name,self.score))
要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入。
>>> bart.print_score()
zhangyuang: 90
封装的另一个好处是可以给Student类增加新的方法,比如get_grade:
class Student(object):
def get_grade(self):
if self.score >= 90:
return ‘a’
elif self .score >= 60:
return ‘b’
else:
return ‘c’
>>>bart.get_grade()
'c'
访问限制
如果要让内部属性不被外部访问,可以把属性的名称前加两个下划线,在python中,实例的变量名如果以开头就变成了一个私有变量(private),只有内部可以访问,外部不能访问。
class Student(object):
def init(self,name,score):
self.name = name
self.score = score
def print_score(self):
print(‘%s: %s’ % (self.name,self.score))
改完后,对于外部代码来说,没什么变动但是已经无法从外部访问实例变量.name和实例变量.score了
>>> bart = Student('zhangyuang',90)
>>> bart.__name
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Student' object has no attribute '__name'
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法
class Student(object):
def get_name(self):
return self.name
def get_score(self)
return self.score
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
class Student(object):
def set_score(self,score):
self.score = score
你也许会问,原先那种直接通过bart.score = 59也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
class Student(object):
def set_score(self,score):
if 0<=score<=100:
self.score = score
else:
raise ValueError(‘bad score’)
需要注意的是,在Python中,变量名类似xxx的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用name、score这样的变量名。
有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
继承和多态
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:
class Animal(object):
def run(self):
print(‘Animal is running’)
当我们需要编写Dog和Cat类时,就可以直接从Animal继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。cat和Dog类似。
继承有什么好处?最大的好处是子嘞获得了父类的全部功能。由于Animal实现了run()方法,因此Dog Cat作为它的子类,什么事也没干就拥有了run()方法。
dog = Dog()
dog.run()
cat = Cat()
cat.run()
Animal is running
当然也可以对子类增加一些方法,
class Dog(Animal):
def run(self):
print(‘dog is running’)
def eat(slef)
print(‘eating meat’)
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是dog还是cat它们run()的时候,显示的都是Animal is running 符合逻辑的做法是分别显示dog is running 和 cat is running因此对Dog类和Cat类做如下改进
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # 是Dog类型
判断一个变量是否是某个类型可以用istance()判断
>>> isintance(a,list)
True
>>> isinstance(b,Animal)
True
>>> isinstance(c,Dog)
True
>>> isinstance(c,Animal)
True
看来b不仅是Dog类型还是Animal类型
>>> b =Animal()
>>> isinstance(b,Dog)
False
Dog可以看成Animal,但Animal不可以看成Dog
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量
def run_twice(animal):
animal.run()
animal.run()
当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
当我们传入Dog的实例时,run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
当我们传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat is running...
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生: class Tortoise(Animal):
def run(self):
print(‘tortoise is running’)
当我们调用run_twice()时,传入Tortoise的实例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:
class Timer(object):
def run(self):
print('Start...')
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
获取对象的信息
使用type()
判断对象类型,使用type()函数
基本类型都可以用type()
>>> type(123)
>>> type('str')
>>> type(None)
如果一个变量指向函数或者类,也可以用type()判断
>>> type(abs)
>>> type(a)
但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同
>>> type(123) == type(456)
True
>>> type(123) == int
True
>>> type('abc') = type('123')
True
>>> type('abc') == str
True
>>> type('abc') == type(123)
False
判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量
>>> import types
>>> def fn():
pass
>>> type(fn) == types.FunctionType
True
>>> type(abs) == type.BuiltinFunctionType
True
>>> type(lambda x:x) == type.LambdaType
True
>>> type((x for x in range(10))) == types.GeneratorType
True
使用isinstance()
对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型可以使用isinstance()函数
我们回顾上次的例子,如果继承关系是:
object->Animal->Dog->Husky
那么,isinstance()就可以告诉我们一个对象是否是某种类型。先创建3种类型的对象。
>>> a = Animal()
>>> d= Dog()
>>> h = Husky()
然后,判断:
>>> isinstance(h,Husky)
True
>>> isinstance(h,Dog)
True
h虽然自身是Husky类型,但由于Husky是从Dog继承袭来的,所以h也还是Dog类型。换句话说,isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1,2,3],(list,tuple))
True
>>> isinstance((1,2,3),(list,tuple))
True
使用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(obj)的话,就自己写一个len()方法
>>> class MyDog(object):
def __len__(self):
return 100
dog = myDog()
len(dog)
100
剩下的都是普通属性或方法,比如lower()返回小写的字符串
>>> 'ABC'.low()
'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
19
可以传入一个default参数,如果属性不存在,就返回默认值:
>>> getattr(obj,'z',404) # 获取属性'z',如果不存在返回默认值404
404
也可以获得对象的方法
>>> hasattr(obj,'power') # 有属性'power'吗
True
>>> getattr(obj,'power') # 获取属性'power'
>
>>> fn = getattr(obj,'power') # 获取属性'power'并复制到变量fn
>>> fn()
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()方法返回的是有效的图像数据,就不影响读取图像功能。
实例属性和类属性
由于python是动态语言,根据类创建的实例可以任意绑定属性。
由给实例绑定属性的方法是通过实例变量,或者通过self变量
class Student(object):
def init(self,name):
self.name = name
s = Student(‘zhangyuang’)
s.score = 90
但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性。归Student类所有:
class Student(object):
name = ‘Student’
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
>>> class Student(object):
name = 'Student'
>>> s = Student()
>>> print(s.name) # 因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name)
Student
>>> s.name = 'zhangyuang'
>>> print(s.name)
zhangyuang
>>> print(Student.name)
Student
>>> del s.name
>>> print(s.name)
Student
从上面的例子可以看出,在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
使用slots
正常情况下,当我们定义了一个class创建了一个class的实例后,我们可以给该实例绑定任何属性和方法。
class Student(object):
pass
然后尝试给实例绑定一个属性:
>>> s = Student()
>>> s.name = 'zhangyuang'
>>> print(s.name)
zhangyuang
还可以尝试给实例绑定一个方法:
>>> def set_age(self,age):
self.age = age
>>> from types import MethodType
>>> s.set_age = MethodType(set_age,s)
>>> s.set_age(19)
>>> s.age
25
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
>>> s2 = Student()
>>> s2.set_age(19)
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Student' object has no attribute 'set_age'
为了给所有实例都绑定方法,可以给class绑定方法
>>> def set_score(self,score):
self.score = score
>>> Student.set_score = set_score
通常情况下,上面的set_score方法可以直接定义在class中,但动态绑定允许我们在程序运行的过程中动态给class加上功能,这在静态语言中很难实现。
使用slots
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性
为了达到限制目的,python允许在定义class的时候定义一个特殊的slots变量,来限制该class实例能添加的属性 class Student(object):
slots = (‘name’,’age’)
然后我们试试
>>> s = Student()
>>> s.name = 'zhangyuang'
>>> s.age = 19
>>> s.score = 90
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Student' object has no attribute 'score'
由于’score’没有被放到slots中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。
使用slots要注意,slots定义的属性仅对当前类实例起作用,对继承的子类是不起作用的
>>> class collegestudent(Student):
pass
>>> g = collegestudent()
>>> g.score = 99
除非在子类中也定义slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots。
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是没办法检查参数,导致可以把成绩随便改:
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!’)
现在,对任意的Student实例进行操作,就不能随心所欲地设置score了:
>>> s = Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。
有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?
还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用,[email protected]�属性调用:
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方法变成属性,[email protected]@[email protected],负责把一个setter方法变成属性赋值,于是我们就拥有一个可控的属性操作
>>> s = Student()
>>> s.score = 60
>>> s.score
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
[email protected],[email protected]�不是直接暴露的,而是通过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 2017-self._birth
上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
多重继承
继承是面向对象编程的一个重要方式,因为通过继承,子类就可以扩展父类的功能。
多重继承
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 flu(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
定制类
看到类似slots这种形如xxx的变量或者函数名就要注意,这些python是有特殊用途的。
slots我们已经知道怎么用了,len()方法我们也知道是为了让class作用与len()函数。
除此之外,Python的class中还有许多这样特殊的函数帮助我们定制类。
str
我们先定义一个Student类,打印一个实例:
>>> class Student(object):
def __init__(self,name):
self.name = name
>>> print(Student('zhangyuang'))
打印出一堆<__main__.student object="" at="">不好看。
怎么才能打印的好看呢?只需要定义好str()方法,返回一个好看的字符串就可以了:
>>> class Student(object):
def __init__(self,name):
self.name = name
def __str__(self):
return 'Student object (name: %s)' % self.name
>>> print(Student('zhangyuang'))
Student object (name: zhangyuang)
但是细心的朋友会发现直接敲变量不用print,打印出来的实例还是不好看:
>>> s = Student('zhangyuang')
>>> s
这是因为直接显示变量调用的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()方法拿到循环的下一个值,直接遇到StpIteration错误时退出。
我们以斐波那契数列为例
class Fib(object):
def init(self):
self.a,self.b = 0,1
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
>>> for n in Fib():
print(n)
1
1
2
3
5
....
46368
getitem
Fib实例虽然能作用于for循环看起来和list有点像但是把它当list来用还是不行的
>>> Fib()(5)
Traceback (most recent call last):
File "", line 1, in
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[2]
2
但是list有个神奇的切片方法:
>>> list(range(100))[5:10]
[5,6,7,8,9]
对于Fib却报错。原因是getitem()传入的参数可能是一个int也可能是一个切片对象silce,所以要做判断
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 i range(stop):
if x>=start:
L.append(a)
a,b = b,a+b
return L
>>> f = Fib()
>>> f[0:5]
[1,1,2,3,5]
>>> f[:10]
[1,1,2,3,.....,21,34,55]
正常情况下当我们调用类的方法或属性如果不存在就会报错要避免这个错误,除了可以加上改属性外,python还有另一个机制,那就是写一个getattr()方法动态返回一个属性
class Student(object):
def init(self):
self.name = ‘zhangyuang’
def getattr(self,attr):
if attr == ‘score’:
return 99
if attr == ‘age’
return lambda:25
>>> s.score
99
>>> 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,比如新浪微博、豆瓣啥的,调用API的URL类似:
利用完全动态的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
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢,在python中,答案是肯定的。
任何类只需要定义一个call()方法就可以直接对实例进行调用
class Student(object):
def init(self,name):
self.name = name
def call(self):
print(‘My name si %s’ %self.name)
>>> s = Student('zhangyuang')
s()
My name is zhangyuang
通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
>>> callable(Student())
True
>>> callable('str')
false
使用枚举类
当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份
JAN = 1
FEB = 2
MAr = 3
好处是简单,缺点是类型是int,并且仍然是变量。
更好的放啊是为了这样的枚举类型定义一个class类型,然后每个常量都是class的一个唯一实例。python提供Enum类来实现这个功能。
from enum import Enum
Month = Enum(‘Month’,(‘Jab’,’Feb’…..,’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
@unique
class weekday(Enum):
Sun = 0
Mon = 1
Tue = 2
@unique装饰器可以帮助我们检查保证没有重复值。
访问这些枚举类型可以有若干种方法:
>>> day1 = weekday.Mon
>>> print(day1)
weekday.Mon
>>> print(weekday.Mon.value)
1
>>> print(weekday(1))
weekday.Mon
使用元类
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))
>>> print(type(h))
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))
>>> print(type(h))
要创建一个class对象,type()函数依次传入3个参数:
1、class名称
2、继承的父类集合,注意python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法
3、class的方法名称与函数绑定,这里我们把fn绑定到方法名hello上。
错误处理
程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就可以知道是否有错,以及出错的原因。在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数open(),成功时返回文件描述符(就是一个整数),出错时返回-1。
用错误码来表示是否出错十分不便,因为函数本身应该返回的正常结果和错误码混在一起,造成调用者必须用大量的代码来判断是否出错:
def foo():
r = some_function()
if r == (-1):
return (-1)
# do something
return r
def 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 zero
finally
END
从输出可以看到,当错误发生时,后续语句print(‘result’,r)except由于补货到ZeroDivisionError,因此被执行。最后,finally语句被执行。然后程序继续按照流程往下走。
如果把除数0改成2,则执行结果如下:
try…
result:5
finally
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,所以我们用一个expcept捕获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类派生的,常见的错误类型和继承关系看这里:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
使用try…except捕获错误还有一个好处就是可以跨越多层调用,比如函数main()调用foo(),bar()调用foo(),foo()出错了就可以在main()捕获到就可以处理
# err.py
def foo(s):
return 10/int(s)
def bar(s)
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:',e)
finally:
print('finally')
调用堆栈
如何错误没有被捕获它就会一直往上抛,最后被python解释器捕获,打印一个错误信息然后程序退出
$ python3 err.py
Traceback (most recent call last):
File “err.py”, line 11, in 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
logging模块可以记录错误信息import logging
def foo(s):
return 10/int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
exception Exception as e:
logging.exception(e)
main()
print('END')
同样是出错但程序打印完错误信息后会继续执行并正常退出
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (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 zero
END
抛出错误
如果要抛出错误,首先根据需要可以定义一个错误的class选择好继承关系,然后用raise语句抛出一个错误
# err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid valye: %s' % s)
return 10/n
foo('0')
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
只有在必要的时候才定义我们自己的错误类型,如果可以选择python已有的内置错误类型(ValueError,TypeError)尽量使用内置类型
最后我们来看另一种错误处理方式。
# err_reraise.py
def foo(s):
n = int(s)
if n == 0:
raise ValueError('invalid value:%s' %s)
return 10/n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError')
raise
在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?
其实这种错误处理方式不但没病,而且相当常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。
调试
第一种方法简单粗暴,就使用print()把可能有问题的变量打印出来
def foo(s):
n = int(s)
print(‘>>> n = %d’ % n)
return 10/n
def main():
foo(‘0’)
main()
执行后在输出中查找打印的变量值
$ python3 err.py
>>> n = 0
Traceback (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/n
def main():
foo(‘0’)
assert的意思是,表达式n!=0应该是True否则根据程序运行的逻辑,后面的代码肯定会出错。如果断言失败,assert语句本身就会抛出AssertionError:
$ python3 err.py
Traceback (most recent call last):
…
AssertionError: n is zero!
程序中如果导出充斥着assert和print()相比也好不到哪去。不过启动python解释器时可以用-O参数来关闭assert
$ python3 -O err.py
Traceback (most recent call last):
…
ZeroDivisionError: division by zero
关闭后,你可以把所有的assert语句当成pass来看
logging
把print()替换成logging是第3种方式,和assert相比logging不仅会抛出错误,而且可以输出到文件
impoet logging
s = ‘0’
n = int(s)
logging.info(‘n = %d’ % n)
print(10/n)
logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError没有任何信息。怎么回事?别急,在import logging之后添加一行配置再试试
import logging
logging.basicConfig(level=logging.INFO)
看到输出了:
$ python3 err.py
INFO:root: n = 0
Traceback (most recent call last):
File “err.py”, line 8, in 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.py
s = '0'
n = int(s)
print(10/n)
然后启动
python3 -m pdb err.py
> /Users/zhangyuang/Desktop/python - python3/samples/debug/err.py(2)()
-> s = '0'
以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = ‘0’输入命令1来查看代码
(pdb)1
1 #err.py
2 -> s = ‘0’
3 n = int(s)
4 print(10/n)
输入命令n可以单步执行代码
(pdb)n
> /Users/zhangyuang/Desktop/python - python3/samples/debug/err.py(3)()
-> n = int(s)
>/Users/zhangyuang/Desktop/python - python3/samples/debug/err.py(4)()
-> print(10/n)
任何时候都可以输入命令p变量名来查看变量
(pdb) p s
‘0’
(pdb) p n
0
输入命令q结束调试,退出程序
(pdb) q
这种通过pdb在命令行调试的方法理论上是万能的,但实在太麻烦了,如果有一千行代码要运行到第999行得敲多少命令啊。还好我们有另一种方法
pdb.set_trace
这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点
#err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() #运行到这里会自动暂停
print(10/n)
运行代码程序会自动在pdb.set_trace()暂停并进入pdb调试环境可以使用命令p查看变量或者用命令c继续运行
$ python3 err.py
>/Users/zhangyuang/Desktop/python - python3/samples/debug/err.py(7)()
-> print(10/n)
(pdb) p n
0
(pdb) c
Traceback (most recent call last):
File "err.py", line 7, in
print(10 / n)
ZeroDivisionError: division by zero
这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪
IDE
如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。目前比较好的Python IDE有PyCharm:
另外,Eclipse加上pydev插件也可以调试Python程序。
单元测试
如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对abs(),我们可以编写出以下几个测试用例:
1、输入正数,比如1、1.2、0.99,期待返回值与输入相同
2、输入负数,比如-1、-1.2、-0.99期待返回值与输入相反;
3、输入0期待返回0
4、输入非数值类型,比如None、[]、{}期待抛出TypeError
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过说明我们测试的这个函数能够正常工作。如果测试不通过,要么函数有Bug要么测试条件输入不正确,总之需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问。
>>> d = Dict(a=1,b=2)
>>> d['a']
1
>>> d.a
1
mydict.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 unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1,b=’test’)
self.assertEqual(d.a,1)
self.assertEqul(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.000s
ok
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且有很多工具可以自动来运行这些单元测试。
setUP与testDown
可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
setUp()和testDown()方法有什么用呢?设想你的测试需要启动一个
数据库,这时就可以在setUp()方法中连接数据库,在testDown()方法中关闭数据库,这样不必在每个测试方法中重复相同的代码
class TestDict(unittest.TestCase):
def setUp(self):
print(‘setUp…’)
def tearDown(self):
print(‘tearDown…’)
小结
单元测试可以有效的测试某个程序模块的行为,是未来重构代码的信心保证
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单,如果测试代码太复杂那么测试代码本身就可能有BUG
单元测试通过了并不意味着程序就没有bug了但是不通过程序肯定有bug
IO编程
文件读写
读文件
要以读文件的模式打开一个文件使用python内置的open()函数,传入文件名和标识符:
>>> f = open('./test.txt','r')
标识符’r’表示读,如果文件不存在open()函数就会抛出一个IOError的错误并且给出错误码和详细信息。
如果文件打开成功接下来调用read()方法可以一次读取文件的全部内容,python把内容读到内存用一个str对象表示
>>> f.read()
'IO test'
最后一步是调用close()方法关闭文件,文件使用完毕后必须关闭因为文件对象会占用操作系统的资源并且操作系统同一时间能打开的文件数量也是有限的。
>>> f.close()
由于文件读写时都有可能产生IOError,一旦出错后面的f.close()就不会调用。所以为了保证无论是否出现
try:
f = open(‘./test.txt’,’r’)
print(f.read())
finally:
if f:
f.close()
但是每次都这么写实在是太繁琐了,所以python引入了with语句来自动帮我们调用close()方法
with open(‘./test.txt’,’r’) as f:
print(r.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('./test.jpg','rb')
>>> f.read()
b 'xffxd8xffxe1x00x18Exifx00x00...' #十六进制表示的字节
字符编码
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如读取GBK编码的文件
>>> f = open('./gbk.test','r',encoding='gbk')
>>> f.read()
'测试'
遇到有些编码不规范的文件你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接受一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略
>>> f = open('./gbk.txt','r',encoding='gbk',errors='ignore')
写文件
写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符’w’或者’wb’表示写文本文件或写二进制文件
>>> f = open('./test.txt','w')
>>> f.write('hello,world')
>>> f.close()
你可以反复调用write()来写入文件,但是无比要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘剩下的丢失了。所以,还是用with语句来的保险
with open(‘./test.txt’,’w’) as f:
f.write(‘hello world’)
要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串转换为指定编码
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('hellonHInGoodbye')
>>> while True:
s = f.readline()
if s == '':
break
print(s.strip())
Hello
HI
Goodbye
BytesIO
StringIO操作的只能str如果要操作二进制数据,就需要使用BytesIO
BytesIO实现在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes
>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'xe4xb8xadxe6x96x87'
请注意,写入的不是str而是经过UTF-8编码的bytes
和StringIO类似可以用一个bytes初始化BytesIO然后像读文件一样读取
>>> from io import BytesIO
>>> f = BytesIO(b'xe4xb8xadxe6x96x87')
>>> f.read()
b'xe4xb8xadxe6x96x87'
小结
StringIO和BytesIO是在内存中操作str和bytes的方法,是的和读写文件具有一致的借口。
操作文件和目录
如果我们要操作文件、目录可以在命令行下面输入操作系统提供的各种命令来完成。比如dir、cp等命令。
如果要在python程序中执行这些目录和文件的操作怎么办?其实操作系统提供的命令只是简单的调用了操作系统提供的借口函数,python内置的os模块也可以直接调用操作系统提供的接口函数。
打开python交互式命令行
>>> import os
>>> os.name #操作系统类型
'posix'
如果是posix,说明系统是Linux、unix或MacOSX,如果是nt,就是windows系统。
要获取详细的系统信息,可以调用uname()函数:
>>> os.uname()
posix.uname_result(sysname='Darwin', nodename='zhangyuangdeMBP', release='16.0.0', version='Darwin Kernel Version 16.0.0: Mon Aug 29 17:56:20 PDT 2016; root:xnu-3789.1.32~3/RELEASE_X86_64', machine='x86_64')
注意uname()函数在windows上不提供,也就是说os模块的某些函数是跟操作系统相关的
环境变量
在操作系统中定义的环境变量全部保存在os.environ这个变量中
>>> os.environ
environ({'TERM_PROGRAM': 'Apple_Terminal', 'TERM': 'xterm-256color', 'SHELL': '/bin/bash', 'TMPDIR': '/var/folders/g7/n76jd7897_s0xtyqlssfk9y00000gn/T/', 'Apple_PubSub_Socket_Render': '/private/tmp/com.apple.launchd.YKTDBWScTk/Render', 'TERM_PROGRAM_VERSION': '377', 'TERM_SESSION_ID': '50F4527E-810B-4A61-807C-3D9C8E2B345C', 'USER': 'zhangyuang', 'SSH_AUTH_SOCK': '/private/tmp/com.apple.launchd.zdqC9KFAal/Listeners', '__CF_USER_TEXT_ENCODING': '0x1F5:0x19:0x34', 'PATH': '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin', 'PWD': '/Users/zhangyuang/Desktop/github/Learn_python', 'LANG': 'zh_CN.UTF-8', 'XPC_FLAGS': '0x0', 'XPC_SERVICE_NAME': '0', 'HOME': '/Users/zhangyuang', 'SHLVL': '1', 'LOGNAME': 'zhangyuang', '_': '/usr/local/bin/python3', 'OLDPWD': '/Users/zhangyuang/Desktop/github', '__PYVENV_LAUNCHER__': '/usr/local/bin/python3'})
要获取某个环境变量的值,可以调用os.environ.get(‘key’)
>>> os.environ.get('PATH')
'/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
>>> os.environ.get('x','default')
'default'
操作文件和目录
操作文件和目录的函数一部分方法放在os模块中,一部分放在os.path模块中,这一点要注意一下。查看、创建和删除目录可以这么调用
#查看当前目录绝对路径
>>> os.path.abspath('.')
'/Users/zhangyuang/Desktop/github/Learn_python'
#在某个目录下创建一个新目录,首先把新目录的完整路径表示出来
>>> os.path.join('/Users/zhangyuang','testdir')
'/Users/zhangyuang/testdir'
#然后创建一个目录
>>> os.mkdir('/Users/zhangyuang/testdir')
#删掉一个目录
>>> os.rmdir('/User/zhangyuang/testdir')
把两个路径合成一个时,不要直接拼字符串,而是要通过os.path.join()函数,这样可以正确处理不同操作系统的路径分隔符。在Linux/Unix/Mac下,os.path.join()返回这样的字符串
part-1/part-2
而windows下会返回这样的字符串:
part-1part-2
同样的道理,要拆分路径时也不要直接去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆分为两部分,后一部分总是最后级别的目录或文件名
>>> os.path.split('/Users/zhangyuang/testdir/file.txt')
('/Users/zhangyuang/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.listfir('.') if os.path.isdir(x)]
['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]
要列出所有.py文件
>>> [x for x in os.listdir('x') 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'
序列化
在程序运行的过程中最难过,所有的变量都是在内存中,比如定义一个dict
d = dict(name=’zhangyuang’,age=19,score=99)
可以随时修改变量,比如把name改成janvier,但是一旦程序结束,变量所占用的内存就被操作系统回收。如果没有把修改后的janvier存储到磁盘上,下次重新运行程序,变量又被初始化为zhangyuang
我们把变量从内存中变成可存储或可传输的过程称之为序列化,在python中叫pickling,在其他语言中也被称为serialization,marshalling,flattening等等,都是一个意思。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器中。
反过来,把变量内容从序列化的对象重新读到内存里称之为反叙黎话,即unpicking。python提供了pickle模块来实现序列化。
首先,我们尝试把一个对象序列化并写入文件
>>> import pickle
>>> d = dict(name='zhangyuang,age=19,score=99')
>>> pickle.dumps(d)
b'x80x03}qx00(Xx03x00x00x00ageqx01Kx14Xx05x00x00x00scoreqx02KXXx04x00x00x00nameqx03Xx03x00x00x00Bobqx04u.'
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
{'name':'zhangyuang','score':'99','age':'19'}
变量的内容又回来了,当然,这个变量和原来的变量是完全不相干的对象,它们只是内容相同而已。
JSON
如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:
JSON类型 Python类型
{} dict
[] list
“string” str
123.56 int或float
true/false True/False
null None
python内置的json模块提供了非常完善的Python对象到JSON格式的转换。
我们先看看如何把python对象变成一个JSON
>>> import json
>>> d = dict(name='zhangyuang',age=20,score=99)
>>> json.dumps(d)
'{"age":20,"score":99,"name":"zhangyuang"}'
dumps()方法返回一个str,内容就是标准的JSON。类似的dump()方法可以直接把JSON写入一个file-like Object。要把JSON反序列化为python对象,用loads()或者对应的load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:
>>> json_str = '{"age":20,"name":"zhangyuang","score":99}'
>>> json.loads(json_str)
{'age':20,'score':99,"name":'zhangyuang'}
由于JSON标准规定JSON编码是UTF-8所以我们总是能正确的在Python的str与JSON的字符串之间转换。
JSON进阶
Python的dict对象可以直接序列化为JSON的{},不过很多时候我们更喜欢用class表示对象,比如定义Student类,然后序列化
import json
class Student(object):
def init(self,name,age,score):
self.name = name
self.age = age
self.score = score
s = Student(‘zhangyuang’,19,99)
print(json.dumps(s))
运行代码,毫不留情地得到一个TypeError:
Traceback (most recent call last):
…
TypeError: <__main__.student object="" at=""> is not JSON serializable
错误的原因是Student对象不是一个可序列化为JSON的对象,如果连class的实例对象都无法序列化为JSON这肯定不合理。别急,我们仔细看看dumps()方法的参数列表,发现除了第一个必须的obj参数外,dumps()还提供了一大堆可选参数https://docs.python.org/3/library/json.html#json.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":19,"name":"zhangyuang","score":99}
不过下次如果遇到一个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":99,"name":"zhangyuang"}'
打印出的是反序列化对象的Student实例对象
小结
python语言特定的序列化模块是pickle,但如果要把序列化搞得通用、更符合web标准,就可以使用json模块。json模块的dumps()和loads()函数是定义的非常好的接口的典范。当我们使用时,只需要传入一个必须的参数。但是当默认的序列化机制不满足我们的要求时,我们又可以传入更多的参数来定制序列化的规则,即做到了接口简单易用,又做到了充分的扩展性和灵活性。
进程和线程
多进程
要让python程序实现多进程,我们先了解操作系统的相关知识。Unix/Linux操作系统提供了一个fork()系统调用。它非常特殊。普通的函数调用,调用一次返回一次,但是fork()调用一次返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程)然后分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
python的os模块封装了常见的系统调用,其中就包括fork(),可以在python中轻松创建子进程
import os
print(‘Process(%s) start…’ % os.getpid())
# only works on Unix/Linux/Mac
pid = os.fork()
if pid == 0:
print('i am child process (%s) and my parent is %s' % (os.getpid(),os.getppid()))
else:
print('i (%s) just created a child process(%s)' %(os.getpid(),pid))
运行结果如下:
process(876)start
i(876)just created a child process(877)
i am child process(877) and my parent is 876
有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求,就fork()出子进程来处理新的http请求
正则表达式
d匹配一个数字
w匹配一个字母或数字
‘00d’可以匹配’007’但无法匹配’00A’
‘ddd’可以匹配’010’
‘wwd’可以匹配’py3’
.可以匹配任意字符
‘py.’可以匹配’pyc’,’pyo’,’py!’
*表示匹配任意个字符(包括0个)
+表示至少一个字符
?表示0个或1个字符
{n}表示n个字符,{n,m}表示n-m个字符
来看一个复杂的例子:d{3}s+d{3,8}
我们从左到右解读一下
1、d{3}表示匹配3个数字,例如’010’
2、s可以匹配一个空格(也包括Tab等空白符),所以s+表示至少有一个空格,例如匹配’ ‘,’ ‘
3、d{3,8}表示3-8个数字例如’1234567’
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配’010-12345’这样的号码?由于’-‘是特殊字符,在正则表达式要用’’转义,所以上面的正则是d{3}-d{3,8}
但是仍然无法匹配’010 - 12345’因为带有空格。
进阶
要做更精确的匹配,可以用[]表示范围比如
1、[0-9a-zA-Z_]可以匹配一个数字、字母或者下划线
2、[0-9a-zA-Z_]可以匹配至少由一个数字、字母或者下划线组成的字符串,比如’a100’,’0Z’,’Py3000’
3、[a-zA-Z][0-9a-zA-Z_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量
4、[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提供re模块,包含所有正则表达式的功能。由于Python的字符串本身也用转义所以要特别注意
s = ‘ABC-001’ #python字符串
#对应的正则表达式字符串变成
# 'ABC-001'
因此我们强烈建议使用Python的r前缀就不用考虑转义的问题了
s = r’ABC-001’
#对应的正则表达式字符串不变
#'ABC-001'
先看看如何判断正则表达式如何匹配
>>> import re
>>> re.match(r'^d{3}-d{3-8}$','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']
分组
除了简单的判断是否匹配之外,正则表达式还有提取字串的强大功能。用()表示要提取的分组。比如:
^(d{3})-(d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码
>>> m = re.match(r'^(d{3})-(d{3-8})$','010-12345')
>>> m
>>> 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.macth(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')
这个正则表达式可以直接识别合法的时间。到那时有时候用正则表达式也无法做到完全验证,比如识别日期对于2-3-,4-31这样的非法日期还是识别不了,这时候需要程序配合识别。
贪婪匹配
最后需要指出的是,正则匹配默认是贪婪匹配也就是匹配尽可能多的字符。
>>> re.macth(r'^(d+)(0*)$','102300').groups()
('102300','')
由于d+采用贪婪匹配,直接把后买呢0全部匹配了,结果0*只能匹配空字符串了。必须让d+采用非贪婪匹配(也就是尽可能少匹配)此啊能把后面的0匹配出来,加个?就可以让d+采用非贪婪匹配
>>> re.match(r'^(d+?)(0*)$','102300').groups()
('1023','00')
datetime
获取当前日期和时间
>>> from datetime import datetime
>>> now = datetime.now() #获取当前datetime
>>> print(now)
2015-05-18 16:23:23.192343
>>> print(type(now))
获取指定日期和时间
要指定某个日期和时间,我们直接用参数构造一个datetime:
>>> from datetime import datetime
>>> dt = datetime(2015,4,19,12,20)
print(dt)
2015-04-19 12:20:00
datetime转换为timestamp
在计算机中,时间上实际上是用数字表示的。我们把1970年1月1日00:00:00 UTC +00:00时区的时刻称为epochtime,计为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()
1429417200.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
timestamp也可以直接被转换到UTC标准时区的时间
>>> from datetie import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t))#本地时间
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t))
2015-04-19 04:20:00
str转为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
datetime转换为str
如果已经有了datetime对象,要把它格式化为字符串显示给用户,就需要转换为str,转换方法是通过strftime()实现的,同样需要一个日期和时间的格式化字符串
>>> from datetime import datetime
>>> now = datetime.now()
>>> print(now.strftime(%a,%b,%d %H:%M))
Mon,May 05 16:28
datetime加减
对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+和-运算符,不过需要导入timedelta这个类
>>> from datetime import datetime,timedelta
>>> now = datetime.now()
>>> now
datetime.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,hour=12)
datetime.datetime(2015,5,21,4,57,3,540997)
可见使用timedelta你可以很容易的算出前几天和后几天的时刻
本地时间转换为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()
小结
datetime表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。
如果要存储datetime,最佳方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关
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.x
1
>>> p.y
2
namedtuple是一个函数,它用来创建一个自定义的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')
>>> q
deque(['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']
'abc'
>>> dd['key2']
'N/A'
OrderedDict
使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。如果要保持Key的顺序,可以用OrderedDict:
>>> from collections import OrderedDict
>>> d = dict([('a',1),('b',2),('c',3)])
>>> d
{'a':1,'c':3,'b':2}
>>> od = OrderedDict([('a',1),('b',2),('c',3)])
>>> od
OrderedDict([('a',1),('b',2),('c',3)])
注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序
>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys())
['z','y','x']
Counter
Counter是一个简单的计数器,例如,统计字符出现的个数
>>> from collections import Counter
>>> c = Counter()
>>> for ch in 'programming'
c[ch] = c[ch] + 1
>>> c
Counter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1})
base64
base64是一种用64个字符来表示任意二进制数据的方法。
用记事本打开exe、jpg、pdf这些文件时,我们都会看到一大堆乱码,因为二进制文件包含很多无法打印和显示的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。Base的原理很简单,首先准备一个包含64个字符的数组
[‘A’,’B’,’C’,….,’a’,’b’,’c’……,’0’,’1’…’+’,’/‘]
然后对二进制数据进行处理,每3个字节一组一共是3*8=24bit,划分为4组,魅族正好6个bit.
这样我们得到4个数字作为索引,然后查表获得相应的4个字符,就是编码后的字符串。所以Base编码会把3字节的二进制数据编码为4个字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base用x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候会自动去掉。
python内置的base64可以直接进行base64的编解码
>>> import base64
>>> base64.b64encode(b'binaryx00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binaryx00string'
由于标准的Base64编码后可能出现的字符+和/,在URL中就不能直接作为参数,所以又有一种”url safe”的base64编码,其实就是把字符+和/分别变成-和_
>>> base64.b64encode(b'ixb7x1dxfbxefxff')
b'abcd++//'
>>> base64.urlsafe_b64encode(b'ixb7x1dxfbxefxff')
b'abcd--__'
>>> base64.urlsafe_b64decode('abcd--__')
b'ixb7x1dxfbxefxff'
HTMLParser
如果我们要编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,到底是新闻、图片还是视频。
假设第一步已经完成了,第二步应该如何解析HTML呢?
HTML本质是XML的子集,但是HTMl的语法没有XML那么严格,所以不能用标准的DO们或SAX来解析HTML。
好在python提供了HTMLParser来解析HTML,只需简单几行代码:
>>> from html.parser import HTMLParser
>>> from hrml.entities import name2codepoint
class MyHTMLParser(HTMLParser):
def handle_starttag(self,tag,attrs):
print('' % tag)
def handle_endtag(self,tag):
print('%s>' % tag)
def handle_startendtag(self,tag,attrs):
print('' % tag)
def handle_data(self,data):
print(data)
def handle_comment(self,data):
print('')
def handle_entityref(self,name):
print('&%s;' % name)
def handle_charref(self,name):
print('%s;' % name)
parser = MyHTMLPaeser()
paser.feed('''
Some html HTML tutorial...
END
''')
feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
特殊字符有两种,一种是英文表示的 ,一种是数字表示的Ӓ,这两种字符都可以通过Parser解析出来。
urllib
urillib提供了一系列用于操作URL的功能。
Get
urllib的request模块可以非常方便的抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP响应:
例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取,并返回响应
from urllib import request
with 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 OK
Server: nginx
Date: Tue, 26 May 2015 10:02:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2049
Connection: close
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
X-DAE-Node: pidl1
Data: {“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头,我们就可以把请求伪装成浏览器。例如,模拟iphone6去请求豆瓣首页
from urllib import request
req = 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的移动版网页:
…
…
Post
如果要以POST发送一个请求,只需要把参数data以bytes形式传入。
我们模拟一个微博登录,先读取登陆的邮箱和口令,然后按照weibo.cn的登陆页格式以username=xxx&password=xxx的编码传入:
from urllib import request,parse
print(‘Login to weibo.cn…’)
email = input(‘Email:’)
passwd = input(‘Password:’)
login_data = parse.urlencode([
(‘username’,email),
(‘password’,passwd),
(‘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.weobo.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 ok
Server: nginx/1.2.0
…
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
…
Data: {“retcode”:20000000,”msg”:””,”data”:{…,”uid”:”1658384301”}}
如果登陆失败,我们获得的响应如下:
…
Data: {“retcode”:50011015,”msg”:”u7528u6237u540du6216u5bc6u7801u9519u8bef”,”data”:{“username”:[email protected],”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