天天看點

python對面向對象程式設計完全支援_Python面向對象進階程式設計

參考原文

PS:資料封裝、繼承和多态隻是OOP中最基礎的3個概念。在Python中,面向對象還有很多進階的特性,我們會讨論多重繼承、定制類、元類等概念。

動态語言的靈活性

正常情況下,當我們定義了一個class,建立了該類的執行個體後,我們可以給該執行個體綁定任何屬性和方法,這就是動态語言的靈活性。先定義一個類:

class Student(object):

pass

然後給一個執行個體綁定一個屬性:

s =Student()

s.name= 'Alice'print(s.name) #result Alice

還可以為執行個體綁定一個方法:

>>> def set_age(self, age): #定義一個函數作為執行個體方法

... self.age =age

...>>> from types importMethodType>>> s.set_age = MethodType(set_age, s) #給執行個體綁定一個方法

>>> s.set_age(25) #調用執行個體方法

>>> s.age #測試結果

25

也可以為類動态添加方法使所有執行個體均可調用:

>>> defset_score(self, score):

... self.score=score

...>>> Student.set_score =set_score>>> s.set_score(100)>>>s.score100

>>> s2.set_score(99)>>>s2.score99

但我們不一定想要它“随心所欲”,如果我們想要限制執行個體的屬性怎麼辦?--使用__slots__。

__slots__

為了達到限制目的,Python允許在定義class的時候,定義一個特殊的變量__slots__來限制該class的執行個體能添加的屬性、方法。如:

classStudent(object):__slots__ = ('name', 'age') #用tuple定義允許綁定的屬性、方法名稱

試試:

>>> s =Student()>>> s.name = 'Alice'

>>> s.score = 12Traceback (most recent call last):

File"", line 1, in s.score= 12AttributeError:'Student' object has no attribute 'score'

Tips:__slots__定義的屬性僅對目前執行個體起作用,對繼承的子類是無效的,除非在子類也定義__slots__

@property

我們之前說過,可以将屬性設定成私有的,然後通過一個方法來進行操作該屬性,這樣就可以檢查參數的有效性。如

classStudent(object):defget_score(self):returnself._scoredefset_score(self, value):if notisinstance(value, int):raise ValueError('score must be an integer!')if value < 0 or value > 100:raise ValueError('score must between 0 ~ 100!')

self._score= value

現在,對任意的Student執行個體進行操作,就不能随心所欲地設定score:

>>> s =Student()>>> s.set_score(60) #ok!

>>>s.get_score()60

>>> s.set_score(9999)

Traceback (most recent call last):

...

ValueError: score must between 0~ 100!

但這樣做又難免複雜,不符合Python簡潔的定義,倒和C#這種“嚴格”的語言差不多了,那有沒有既能檢查參數,又可以用類似屬性這樣簡單的方式來通路累的變量呢?對于追求完美的Python程式員來說,這是必須的!回想下,已經學過的知識,似乎裝飾器(decorator)可以給函數動态加上功能。對于類的方法,裝飾器一樣起作用。

Python内置的@property裝飾器就是負責把一個方法變成屬性調用,如:

class Student(object):

@property

def score(self):returnself._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 and 100')

self._score= value

要把一個方法變成屬性,隻需在get方法上加上@property,此時@property本身又自動建立了另一個裝飾器@屬性名.setter,用于對屬性指派。此時,我們就擁有了一個可控的屬性操作:

>>> s =Student()>>> s.score = 60 #ok,實際調用形如s.set_score(60)>>>s.score #s實際調用形如s.get_score()60

>>> s.score = 101Traceback (most recent call last):

File"", line 1, in s.score= 101File"", line 12, inscore

raise ValueError('score must between 0 and 100')

ValueError: score must between0 and 100

Tips:當我們看到@property,就應該知道該屬性不是直接進行操作的,而是通過getter、setter方法來實作的,也可以隻定義隻讀屬性(不定義setter方法 )。

多重繼承

前面已經說過繼承了,通過繼承,子類可以獲得父類的全部功能。但如果子類還想獲得更多的功能怎麼辦呢?除了擴充自己的特色方法外,還可以通過多重繼承來獲得多個父類的功能。如:

classRunable(object):defrun(self):print('Runnng...')classEatable(object):defeat(self):print('Eating...')classDog(Runable, Eatable):passdog=Dog()

dog.run()

dog.eat()'''Runnng...

Eating...'''

MixIn

在設計類的繼承關系時,通常主線都是單一繼承下來的,如果要加入額外的功能可以通過多重繼承來實作。讓A繼承A1,同時繼承A2,這種設計通常稱之為MixIn。

這樣就可以把Runable和Eatable改成RunbleMixIn和EatableMixIn了,這樣就更加了然了。在Python中自帶的很多庫也使用了MixIn。舉個例子,Python中自帶了TCPServer和UDPServer這兩種網絡服務,而要同時服務多個使用者就必須使用多程序和多線程模型,這兩種模型分别由ForkingMixIn和ThreadingMixIn提供。通過組合就可以創造出合适的服務出來了,如編寫一個多程序的TCP服務:

classMyTCPServer(TCPServer, ForkingMixIn):pass

多線程的UDP服務:

classMyUDPServer(UDPServer, ThreadingMixIn):pass

Tips:由于Python允許多重繼承,是以MixIn是一種常見的設計,而隻允許單一繼承的語言(如Java)不能使用MixIn設計。

定制類

定制類,就是通過一些特殊的方法來使我們的類具有特殊的功能來應對某些特定的場合。前面我們已經說過了一些特殊的變量或函數名(形如__xx__)如__slots__和__len__()。接下來我們就要說一些特殊的方法了。

__str__

為了說明__str__的作用,我們先定義一個Student類,并列印出一個執行個體:

>>> classStudent(object):def __init__(self, name):

self.name=name>>> print(Student('Alice'))<__main__.Student object at 0x000001DFB4FC1DA0>

這列印出來的字元串明顯不好看,這時__str__()方法就可以派上用場了:傳回一個好看的字元串:

>>> classStudent(object):def __init__(self, name):

self.name=namedef __str__(self):return 'Student object (name: %s)' %self.name>>> print(Student('Alice'))

Student object (name: Alice)>>>

這樣列印出來的執行個體不但好看,而且容易看出執行個體内部的重要資料。但是在Python解釋器下,直接敲變量列印出來的執行個體還是和原來一樣不好看:

>>> s = Student('Alice')>>>s<__main__.Student object at 0x000001DFB4FC1D30>

為什麼呢?這是因為直接敲變量調用的不是__str()__,而是__repr__(),前者是傳回給使用者看的字元串,後者是傳回給程式開發者看到的字元串,也就是說__repr__()是為調試服務的。解決的辦法是再定義一個__repr__(),但是通常下__str()__和__repr__()代碼是一樣的,是以可以偷個懶直接使--repr__ = __str__:

classStudent(object):def __init__(self, name):

self.name=namedef __str__(self):return 'Student object (name=%s)' %self.name__repr__ = __str__

__iter__

我們已經知道list或tuple的資料類型可以被用于for ... in ... 循環,那麼如果類也想被用于for .. in ... 循環,該怎麼辦呢?那就是實作__iter()__方法,該方法傳回一個疊代對象,for循環不斷調用該疊代對象的__next()__方法拿到循環的下一個值,知道遇到StopIteration錯誤時退出循環。

我們以斐波那契數為例,寫一個Fib類,用作for循環:

classFib(object):def __init__(self):

self.a, self.b= 0, 1 #初始化兩個計數器a,b

def __iter__(self):return self #執行個體本身就是疊代對象,故傳回自己

def __next__(self):

self.a, self.b= self.b, self.a + self.b #計算下一個值

if self.a > 100: #退出循環

raiseStopIteration()return self.a #傳回下一個值

for n inFib():print(n)'''1

1

2

3

5

8

13

21

34

55

89'''

__getitem__

上面的Fib執行個體雖然能用作for循環了,但把它當成list來使用還是不行的,比如按索引取元素:

>>> Fib()[5]

Traceback (most recent call last):

File "", line 1, in

Fib()[5]

TypeError: 'Fib' object does not support indexing

此時如果要表現得想lsit那樣按索引取元素,就需要實作__getitem__()方法了:

>>> classFib(object):def __getitem__(self, n):

a, b= 1, 1

for x inrange(n):

a, b= b, a +breturna>>> Fib()[9]55

試試list的切片:

>>> Fib()[1:3]

Traceback (most recent call last):

File"", line 1, in Fib()[1:3]

File"", line 4, in __getitem__

for x inrange(n):

TypeError:'slice' object cannot be interpreted as an integer

報錯是因為__getitem__()傳入的參數可能是一個int,也可能是一個切片對象slice是以要做判斷:

classFib(object):def __getitem__(self, n):if isinstance(n, int): #n是索引

a, b = 1, 1

for x inrange(n):

a, b= b, a +breturnaif isinstance(n, slice): #n是切片

start =n.start

stop=n.stopif start isNone:

start=0

a, b= 1, 1L=[]for x inrange(stop):if x >=start:

L.append(a)

a, b= b, a +breturn L

再試試切片:

>>> Fib()[0:5]

[1, 1, 2, 3, 5]

但還是卻沒對step參數作處理:

>>> f[:10:2]

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

是以要實作一個完整的__getitem__()還是要很多工作要做的。

Tips:如果把對象看成一個dict,那麼__getitem__()的參數也可能是一個可以作為key的對象如str,與之對應的是__setitem_()方法,把對象視作為list來指派,還有一個__delitem__(),用于删除某個元素。是以我們可以通過定義特殊的方法來使自己定義的類表現得和Python自帶的list、tuple、dict一樣(Python動态語言的“鴨子類型”)。

__getattr__

我們知道正常情況下,當調用類不存在的屬性或方法時,就會報錯:

>>> classStudent(object):pass

>>> s =Student()>>>s.name

Traceback (most recent call last):

File"", line 1, in s.name

AttributeError:'Student' object has no attribute 'name'

我們可以避免這個錯誤,除了加上這屬性外,Python還有另一個機制,那就是實作一個__getattr__()方法,動态傳回一個屬性。修改上面的代碼如下:

>>> classStudent(object):def __getattr__(self, attr):if attr == 'name':return 'Alice'

>>> s =Student()>>>s.name'Alice'

Tips:也可以傳回函數,隻有在沒有找到屬性的情況下,才調用__getattr__,已有的屬性不會在__getattr__中查找。

當我們實作了__getattr__方法後,調用任何執行個體沒有的屬性都會傳回None,這是因為我們定義的__getattr__()方法預設傳回None,要讓類隻響應幾個特點的屬性,對于其他的屬性,我們就要按照約定,抛出AttributeError的錯誤:

>>> classStudent(object):def __getattr__(self, attr):if attr == 'name':return 'Alice'

raise AttributeError('\'Student\' object has no attribute \'%s\'' %attr)>>> s =Student()>>>s.age

Traceback (most recent call last):

File"", line 1, in s.age

File"", line 6, in __getattr__

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

AttributeError:'Student' object has no attribute 'age

這實際上可以把一個類的所有屬性和方法調用全部動态化處理,不需要其他的特殊手段。這種完全動态調用的特性有什麼實際的作用呢?作用就是可以針對完全動态的情況來調用。如要寫SDK,如果給每個URL對應的API都寫一個方法,那太困難了,而且API一旦改變SDK也要改。是以此時可以利用完全動态的__getattr__來寫一個鍊式調用:

classChain(object):def __init__(self, path=''):

self._path=pathdef __getattr__(self, path):return Chain('%s/%s' %(self._path, path))def __str__(self):returnself._path__repr__ = __str__

試試:

>>>Chain().status.user.timeline.list'/status/user/timeline/list'

__call__

我們知道一個對象額執行個體可以有自己的屬性和方法,我們可以用instance.method()來調用,那麼能不能直接在執行個體本身上調用呢?OK,隻需要實作__call__()方法。請看示例:

>>> classStudent(object):def __init__(self, name):

self.name=namedef __call__(self):print('My name is %s' %self.name)>>> s = Student('Alice')>>>s()

My nameis Alice

__call__()還可以定義參數相當于對一個函數進行調用,是以你完全可以把對象看成函數,函數看成對象,這2者本來就沒有根本的差別。如果你把對象看成函數,那麼函數本身也可以在運作期動态地建立出來(類的執行個體都是運作期建立出來的)。

Tips:通過callable()函數,可以判斷一個對象是否是“可調用”對象。

枚舉類

我們應該或多或少都知道一點關于枚舉的知識,枚舉就是列舉一個可數集合的元素。如,人的性别可以看成一個集合,通過枚舉,可以拿到‘男’和‘女’。在Python中,提供了Enum類來實作枚舉這個功能:

from enum importEnum

Sex= Enum('Sex', ('Male', 'Female'))

我們可以直接使用Sex.Male來引用一個常量:

>>>Sex.Male

也可以枚舉出它的所有成員:

>>> for name, member in Sex.__members__.items():print(name, '=>', member, ',', member.value)

Male=> Sex.Male , 1Female=> Sex.Female , 2

注意:value屬性是自動賦給成員的int常量,預設從1開始計數。

我們也可以從Enum派生出自定義類,用于更精确地控制:

from enum importEnum, unique

@unique#@unique裝飾器可以幫我們檢查保證有沒有重複值

classSex(Enum):

Male= 0 #Male的value被設定為0

FeMale = 1

for name, member in Sex.__members__.items():print(name, '=>', member, ',', member.value)'''Male => Sex.Male , 0

FeMale => Sex.FeMale , 1'''

通路這些枚舉類型可以有若幹種方法:

>>> male =Sex.Male>>> print(male)

Sex.Male>>> male = Sex['Male']>>> print(male)

Sex.Male>>> male =Sex(0)>>> print(male)

Sex.Male>>>

Tips:枚舉常量既可以使用成員名稱引用,又可以直接根據value的值擷取。

元類

type()

我們應該知道動态語言和靜态語言最大的不同,就是函數和類的定義不是在編譯時定義的,而是在運作是動态建立的。比如說我們要定義一個Hello的類,就寫一個hello.py的子產品:

classHello(object):def hello(self, name='world'):print('Hello,%s' % name)

當Python解釋器載入hello子產品時,就會依次執行該子產品的所有語句,執行結果就是動态建立出一個Hello的class對象,測試如下:

>>> from hello importHello>>> h =Hello()>>>h.hello()

Hello,world

type()函數可以檢視一個類型或變量的類型,Hello是一個class,它的類型就是type,而h是一個執行個體,它的類型是class Hello:

>>> print(type(Hello))

>>> print(type(h))

我們已經說過了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.繼承的父類集合,如果隻有一個父類,别忘了tuple的單元素寫法 。 3.class的方法的名稱與已定義的函數綁定(這裡,我們把函數fn綁定到hello上)。

Tips:通過type函數建立出來的類和直接寫class是一樣的,本質上都是通過type()函數建立出class。動态語言本身支援運作期動态建立類,而如果要在靜态語言運作期間建立類,必須構造源代碼字元創再調用編譯器,或者借助一下工具生成位元組碼實作,會非常複雜,但本質都是動态編譯。

metaclass(元類)

前面已經說過了type()可以動态地建立類,但除此之外,還可以使用metaclass以控制類的建立行為。什麼是metaclass呢?簡單的解釋就是:類是metaclass建立出來的“執行個體”,metaclass是類的“模闆”。使用metaclass時,就先定義metaclass,再建立類,最後建立出執行個體。

那metaclass到底有什麼用呢?不急,我們先來看一個簡單的例子,先定義出一個簡單的metaclass(用來幹啥?不急,先定義出來。),定義ListMetaclass(元類預設以‘Metaclass’結尾):

#metaclass是類的模闆,是以必須從‘type’類型派生

classListMetaclass(type):def __new__(cls, name, bases, attrs):

attrs['add'] = lambdaself, value: self.append(value)return type.__new__(cls, name , bases, attrs)

有了這個元類,我們再定義一個普通的類,訓示使用元類來定制類(傳入關鍵字參數metaclass):

class MyList(list, metaclass=ListMetaclass):pass

這樣,magic就生效了,它訓示Python解釋器在建立MyList時,要通過ListMetaclass.__new__()來建立。這樣,就可以定制MyList類了,比如加上新的方法add。來說下__new__()方法,該方法一共接收4個參數,分别是:1.目前準備建立類的對象 2.類的名字 3.類繼承的父類集合 4.類的方法集合。

此時,應該明白ListMetaclass代碼的含義了:将需定制類的add方法(沒有就建立)修改為“為執行個體對象添加值”,并傳回給MyList類。來測試下:

>>> L =MyList()>>> L.add(1)>>>L

[1]

好想是很magic,但為什麼要這樣呢?動态修改有什麼意義呢?直接在類定義上add()不是更簡單嗎?正常情況下,确實如此,但是總會遇到需要通過metaclass修改類的定義的,如ORM。

那問題又來了,什麼是ORM?學過資料庫或者用過資料庫的應該知道,ORM全稱“Object Relation Mapping”,即對象-關系映射。簡單的來說,就是把關系資料庫的一行映射成為一個對象,一個類對應成一張表。

是以為了說明metaclass的強大之處,讓我們來嘗試編寫一個ORM架構吧。

1.編寫底層子產品的第一步,就是先把調用接口寫出來。比如,使用者如果使用這個ORM架構,想定義一個User類來操作對應資料表User,使用者應該寫出這樣的代碼來調用:

classUser(Model):#定義類的屬性到列的映射

id = IntegerField('id')

name= StringField('username')

email= StringField('email')

password= StringField('password')#建立一個執行個體:

u = User(id=123, name ='Alice', email='[email protected]', password='xxxx')#儲存到資料庫

u.save()

其中,父類Model和屬性類型StringField、IntegerField由ORM架構提供,剩下的魔術方法如save()全部由metaclas自動完成。這樣metaclass的編寫雖然會比較複雜,但ORM的使用者調用起來卻十分簡單。

2.現在就按照上面的接口定義,來實作該ORM,我們先定義一個最基本的Field類用于儲存資料庫表中的字段名和字段類型:

classField(object):def __init__(self, name, column_type):

self.name=name

self.column_type=column_typedef __str__(self):return '<%s:%s>' % (self.__class__.__name__, self.name)

3.有了最基本的Field定義,我們就可以擴充定義各種類型的Field了,如StringField、IntegerField等。

classStringField(Field):def __init__(self, name):

super(StringField, self).__init__(name,'varchar(100)') #調用父類的__init__方法

classIntegerField(Field):def __init__(self, name):

super(IntegerField, self).__init__(name, 'bigint') #調用父類的__init__方法

4.編寫ModelMetac元類用于定制基類Model及基類:

python對面向對象程式設計完全支援_Python面向對象進階程式設計
python對面向對象程式設計完全支援_Python面向對象進階程式設計

classModelMetaclass(type):def __new__(cls, name, bases, attrs):if name =='Model': #不對Model類進行修改

return type.__new__(cls, name, bases, attrs)print('Found model: %s' %name)

mappings=dict()for k, v in attrs.items(): #在目前類查找出定義的類的所有屬性,儲存到mappings中

ifisinstance(v, Field):print('Found mapping: %s ==> %s' %(k, v))

mappings[k]=vfor k in mappings.keys(): #從類的屬性中删除該Field屬性,否則容易造成運作錯誤(執行個體的屬性會遮住類的同名屬性)

attrs.pop(k)

attrs['__mappings__'] = mappings #儲存屬性和列的映射關系

attrs['__table__'] = name #将表名和類名設定成一樣的

return type.__new__(cls, name, bases, attrs)class Model(dict, metaclass=ModelMetaclass):def __init__(self, **kw):

super(Model, self).__init__(**kw)def __getattr__(self, key):try:returnself[key]exceptKeyError:raise AttributeError(r"'Model' object has no attribute '%s'" %key)def __setattr__(self, key, value):

self[key]=valuedefsave(self):

fields=[]

params=[]

args=[]for k, v in self.__mappings__.items():

fields.append(v.name)

params.append('?')

args.append(getattr(self, k, None))

sql= 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))print('SQL: %s' %sql)print('ARGS: %s' % str(args))

View Code

5.ok,開始調用:

#建立一個執行個體:

u = User(id=12345, name='Michael', email='[email protected]', password='my-pwd')#儲存到資料庫

u.save()'''Found model: User

Found mapping: id ==>

Found mapping: name ==>

Found mapping: email ==>

Found mapping: password ==>

SQL: insert into User (id,username,email,password) values (?,?,?,?)

ARGS: [12345, 'Michael', '[email protected]', 'my-pwd']'''

成功了,好像還可以啊(溜了~~~)。

Tips:metaclass是Python非常具有魔術性的對象,他可以改變類建立時的行為,這麼牛逼的功能使用起來還是要小心點的。