面向對象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