python死磕三之類與對象
面向對象編程,說起來很抽象,也許一百個人有一百種答案,最基本的三大概念無疑就是:封裝,繼承和多態,python是一種強類型動態性語言,默認是支持多態的,也就是在對象調用方法時,python會自動檢查該對象是否有我們想要調用的方法,不用寫特殊的接口類取指定,也不用事先指定該對象的類型。
面向對象相對於面向過程編程,最大的好處就是使項目的耦合性降低,不再使我們的項目牽一發而動全身,將過程解耦,使我們只要單獨的修改過程即可。
弄清以下幾個問題,會對我們面向對象的思想會有自己的答案。
一、你想改變對象實例的打印或顯示輸出,讓它們更具可讀性。
之前思路:在類中定義__str__方法。
遺漏點:__repr__()和__str__()有什麽區別呢,分別什麽時候使用。
class Pair: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return ‘Pair({0.x!r}, {0.y!r})‘.format(self) def __str__(self): return ‘({0.x!s}, {0.y!s})‘.format(self)
__str__() 一般用與str()和print()方法中,他會調用次方法來顯示我們的對象。
__repr__() 在交互式編程中會調用,如果沒有定義__str__(),會默認調用此方法。
二、你想讓你的對象支持上下文管理協議(with語句)。
為了讓一個對象兼容 with
語句,你需要實現 __enter__()
和 __exit__()
方法。 例如,考慮如下的一個類,它能為我們創建一個網絡連接:
from socket import socket, AF_INET, SOCK_STREAM class LazyConnection: def __init__(self, address, family=AF_INET, type=SOCK_STREAM): self.address= address self.family = family self.type = type self.sock = None def __enter__(self): if self.sock is not None: raise RuntimeError(‘Already connected‘) self.sock = socket(self.family, self.type) self.sock.connect(self.address) return self.sock def __exit__(self, exc_ty, exc_val, tb): self.sock.close() self.sock = None
這個類的關鍵特點在於它表示了一個網絡連接,但是初始化的時候並不會做任何事情(比如它並沒有建立一個連接)。 連接的建立和關閉是使用 with
語句自動完成的,我們在實例化時調用了__init__方法,用with時,他會自己調用__enter__, 結束後會調用__exit__,例如:
from functools import partial conn = LazyConnection((‘www.python.org‘, 80)) # Connection closed with conn as s: # conn.__enter__() executes: connection open s.send(b‘GET /index.html HTTP/1.0\r\n‘) s.send(b‘Host: www.python.org\r\n‘) s.send(b‘\r\n‘) resp = b‘‘.join(iter(partial(s.recv, 8192), b‘‘)) # conn.__exit__() executes: connection closed
編寫上下文管理器的主要原理是你的代碼會放到 with
語句塊中執行。 當出現 with
語句的時候,對象的 __enter__()
方法被觸發, 它返回的值(如果有的話)會被賦值給 as
聲明的變量。然後,with
語句塊裏面的代碼開始執行。 最後,__exit__()
方法被觸發進行清理工作。
不管 with
代碼塊中發生什麽,上面的控制流都會執行完,就算代碼塊中發生了異常也是一樣的。 事實上,__exit__()
方法的第三個參數包含了異常類型、異常值和追溯信息(如果有的話)。__exit__()
方法能自己決定怎樣利用這個異常信息,或者忽略它並返回一個None值。 如果 __exit__()
返回 True
,那麽異常會被清空,就好像什麽都沒發生一樣, with
語句後面的程序繼續在正常執行。
三、你的程序要創建大量(可能上百萬)的對象,導致占用很大的內存。
遺漏點:對於主要是用來當成簡單的數據結構的類而言,你可以通過給類添加 __slots__
屬性來極大的減少實例所占的內存。
class Date: __slots__ = [‘year‘, ‘month‘, ‘day‘] def __init__(self, year, month, day): self.year = year self.month = month self.day = day
當你定義 __slots__
後,Python就會為實例使用一種更加緊湊的內部表示。 實例通過一個很小的固定大小的數組來構建,而不是為每個實例定義一個字典,這跟元組或列表很類似。 在 __slots__
中列出的屬性名在內部被映射到這個數組的指定小標上。 使用slots一個不好的地方就是我們不能再給實例添加新的屬性了,只能使用在 __slots__
中定義的那些屬性名。
為了給你一個直觀認識,假設你不使用slots直接存儲一個Date實例, 在64位的Python上面要占用428字節,而如果使用了slots,內存占用下降到156字節。 如果程序中需要同時創建大量的日期實例,那麽這個就能極大的減小內存使用量了。
註意:關於 __slots__
的一個常見誤區是它可以作為一個封裝工具來防止用戶給實例增加新的屬性。 盡管使用slots可以達到這樣的目的,但是這個並不是它的初衷。 __slots__
更多的是用來作為一個內存優化工具。
四、為什麽要將某些類裏面的屬性設置成靜態屬性。
遺漏點:當我們建立一個用property裝飾的類的方法func時,我們可以設置@func.setter,@func.deleter來裝飾func函數,使他具備設置和刪除的函數功能。
class Person: def __init__(self, first_name): self.first_name = first_name # Getter function @property def first_name(self): return self._first_name # Setter function @first_name.setter def first_name(self, value): if not isinstance(value, str): # 可以在裏面設置檢查 raise TypeError(‘Expected a string‘) self._first_name = value # Deleter function (optional) @first_name.deleter def first_name(self): # 限制某些操作 raise AttributeError("Can‘t delete attribute")
註意:在__init__方法中定義的是self.first_name,後面的操作變量都是self._first_name,我們用@property裝飾的都是已經存在的實例屬性,他會返回一個新變量給setter方法,所以在初始化的時候也可以進行檢查。
五、你想定義一個接口或抽象類,並且通過執行類型檢查來確保子類實現了某些特定的方法
遺漏點:使用 abc
模塊可以很輕松的定義抽象基類:
from abc import ABCMeta, abstractmethod class IStream(metaclass=ABCMeta): @abstractmethod def read(self, maxbytes=-1): pass @abstractmethod def write(self, data): pass
抽象類的一個特點是它不能直接被實例化,比如你想像下面這樣做是不行的:
a = IStream() # TypeError: Can‘t instantiate abstract class # IStream with abstract methods read, write
抽象類的目的就是讓別的類繼承它並實現特定的抽象方法:
一旦子類繼承了抽象類的方法,這個子類必須包含IStream的所有方法,否則會報錯
class SocketStream(IStream): def read(self, maxbytes=-1): pass def write(self, data): pass
a = SocketStream() 無錯 # 如果 class IStream(metaclass=ABCMeta): @abstractmethod def read(self, maxbytes=-1): pass @abstractmethod def write(self, data): pass @abstractmethod def text(self,sentence): pass SocketStream並沒有text方法,實例化則會報錯
六、屬性的代理訪問
簡單來說,代理是一種編程模式,它將某個操作轉移給另外一個對象來實現。 最簡單的形式可能是像下面這樣:class A: def spam(self, x): pass def foo(self): pass class B1: """簡單的代理""" def __init__(self): self._a = A() def spam(self, x): # Delegate to the internal self._a instance return self._a.spam(x) def foo(self): # Delegate to the internal self._a instance return self._a.foo() def bar(self): pass
我們可以通過B1的實例化去訪問到A類,如果僅僅就兩個方法需要代理,那麽像這樣寫就足夠了。但是,如果有大量的方法需要代理, 那麽使用 __getattr__()
方法或許或更好些:
class B2: """使用__getattr__的代理,代理方法比較多時候""" def __init__(self): self._a = A() def bar(self): pass # Expose all of the methods defined on class A def __getattr__(self, name): """這個方法在訪問的attribute不存在的時候被調用 the __getattr__() method is actually a fallback method that only gets called when an attribute is not found""" return getattr(self._a, name)
另外一個代理例子是實現代理模式,
class Proxy: def __init__(self, obj): self._obj = obj # Delegate attribute lookup to internal obj def __getattr__(self, name): print(‘getattr:‘, name) return getattr(self._obj, name) # Delegate attribute assignment def __setattr__(self, name, value): if name.startswith(‘_‘): super().__setattr__(name, value) else: print(‘setattr:‘, name, value) setattr(self._obj, name, value) # Delegate attribute deletion def __delattr__(self, name): if name.startswith(‘_‘): super().__delattr__(name) else: print(‘delattr:‘, name) delattr(self._obj, name)
class Spam: def __init__(self, x): self.x = x def bar(self, y): print(‘Spam.bar:‘, self.x, y) # Create an instance s = Spam(2) # Create a proxy around it p = Proxy(s) # Access the proxy print(p.x) # Outputs 2 p.bar(3) # Outputs "Spam.bar: 2 3" p.x = 37 # Changes s.x to 37
我們可以將一個類的實例化傳入到另一個類中,再次實例化就可以達到屬性代理的訪問模式。通過自定義屬性訪問方法,你可以用不同方式自定義代理類行為(比如加入日誌功能、只讀訪問等)。
七、你想創建一個實例,但是希望繞過執行 __init__()
方法。
遺漏點:可以通過 __new__()
方法創建一個未初始化的實例。例如考慮如下這個類:
class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day
如果Date實例的屬性year還不存在,所以你需要手動初始化:
>>> d = Date.__new__(Date) >>> d <__main__.Date object at 0x1006716d0> >>> d.year Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: ‘Date‘ object has no attribute ‘year‘ >>>
>>> data = {‘year‘:2012, ‘month‘:8, ‘day‘:29} >>> for key, value in data.items(): ... setattr(d, key, value) ... >>> d.year 2012 >>> d.month 8 >>>
八、你想實現一個狀態機或者是在不同狀態下執行操作的對象,但是又不想在代碼中出現太多的條件判斷語句。
class Connection: """普通方案,好多個判斷語句,效率低下~~""" def __init__(self): self.state = ‘CLOSED‘ def read(self): if self.state != ‘OPEN‘: raise RuntimeError(‘Not open‘) print(‘reading‘) def write(self, data): if self.state != ‘OPEN‘: raise RuntimeError(‘Not open‘) print(‘writing‘) def open(self): if self.state == ‘OPEN‘: raise RuntimeError(‘Already open‘) self.state = ‘OPEN‘ def close(self): if self.state == ‘CLOSED‘: raise RuntimeError(‘Already closed‘) self.state = ‘CLOSED‘
這樣寫有很多缺點,首先是代碼太復雜了,好多的條件判斷。其次是執行效率變低, 因為一些常見的操作比如read()、write()每次執行前都需要執行檢查。
一個更好的辦法是為每個狀態定義一個對象:
class Connection1: """新方案——對每個狀態定義一個類""" def __init__(self): self.new_state(ClosedConnectionState) def new_state(self, newstate): self._state = newstate # Delegate to the state class def read(self): return self._state.read(self) def write(self, data): return self._state.write(self, data) def open(self): return self._state.open(self) def close(self): return self._state.close(self) # Connection state base class class ConnectionState: @staticmethod def read(conn): raise NotImplementedError() @staticmethod def write(conn, data): raise NotImplementedError() @staticmethod def open(conn): raise NotImplementedError() @staticmethod def close(conn): raise NotImplementedError() # Implementation of different states class ClosedConnectionState(ConnectionState): @staticmethod def read(conn): raise RuntimeError(‘Not open‘) @staticmethod def write(conn, data): raise RuntimeError(‘Not open‘) @staticmethod def open(conn): conn.new_state(OpenConnectionState) @staticmethod def close(conn): raise RuntimeError(‘Already closed‘) class OpenConnectionState(ConnectionState): @staticmethod def read(conn): print(‘reading‘) @staticmethod def write(conn, data): print(‘writing‘) @staticmethod def open(conn): raise RuntimeError(‘Already open‘) @staticmethod def close(conn): conn.new_state(ClosedConnectionState)
讓我們跟著演示走一下流程,你會清晰一點。
>>> c = Connection() # 實例化 init方法執行了new_state(ClosedConnectionState) --> _state = ClosedConnectionState >>> c._state # 查看類 <class ‘__main__.ClosedConnectionState‘> >>> c.read() #在ClosedConnectionState類中如果直接read會raise RuntimeError(‘Not open‘) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "example.py", line 10, in read return self._state.read(self) File "example.py", line 43, in read raise RuntimeError(‘Not open‘) RuntimeError: Not open >>> c.open() # 執行了ClosedConnectionState類open方法 --> _state=OpenConnectionState >>> c._state # 在OpenConnectionState類中可以調用相關方法 <class ‘__main__.OpenConnectionState‘> >>> c.read() reading >>> c.write(‘hello‘) writing >>> c.close() >>> c._state <class ‘__main__.ClosedConnectionState‘> >>>
註意:在狀態類中,我們要調用靜態方法修飾的目的是可以傳遞self參數,也就是Connection類,這樣我們可以切換不同的狀態。在不同的狀態中方法可以寫不同的業務邏輯。設計模式中有一種模式叫狀態模式,這一部分算是一個初步入門!
九、實現訪問者模式
假設你要寫一個表示數學表達式的程序,1 + 2 * (3 - 4) / 5,那麽你可能需要定義如下的類:
class Node: pass class UnaryOperator(Node): def __init__(self, operand): self.operand = operand class BinaryOperator(Node): def __init__(self, left, right): self.left = left self.right = right class Add(BinaryOperator): pass class Sub(BinaryOperator): pass class Mul(BinaryOperator): pass class Div(BinaryOperator): pass class Negate(UnaryOperator): pass class Number(Node): def __init__(self, value): self.value = value
然後利用這些類構建嵌套數據結構,如下所示:
# Representation of 1 + 2 * (3 - 4) / 5 t1 = Sub(Number(3), Number(4)) t2 = Mul(Number(2), t1) t3 = Div(t2, Number(5)) t4 = Add(Number(1), t3)
這樣做的問題是對於每個表達式,每次都要重新定義一遍,有沒有一種更通用的方式讓它支持所有的數字和操作符呢。 這裏我們使用訪問者模式可以達到這樣的目的:
class NodeVisitor: def visit(self, node): methname = ‘visit_‘ + type(node).__name__ meth = getattr(self, methname, None) if meth is None: meth = self.generic_visit return meth(node) def generic_visit(self, node): raise RuntimeError(‘No {} method‘.format(‘visit_‘ + type(node).__name__))
為了使用這個類,可以定義一個類繼承它並且實現各種 visit_Name()
方法,其中Name是node類型。 例如,如果你想求表達式的值,可以這樣寫:
class Evaluator(NodeVisitor): def visit_Number(self, node): return node.value def visit_Add(self, node): return self.visit(node.left) + self.visit(node.right) def visit_Sub(self, node): return self.visit(node.left) - self.visit(node.right) def visit_Mul(self, node): return self.visit(node.left) * self.visit(node.right) def visit_Div(self, node): return self.visit(node.left) / self.visit(node.right) def visit_Negate(self, node): return -node.operand
使用示例,內部過程有點繞,讓我們再過一下流程:
>>> e = Evaluator() # 實例化 >>> e.visit(t4) # 執行了visit方法,t4是ADD類,所以methname=visit_add,利用反射meth=getattr(self,visit_add,None)
--> Evaluator.visit_Add(t4) --> 在根據visit_Add的表達式來分解,一直遞歸。 0.6
訪問者模式本質就是根據對象的不同,執行不同的訪問方法。
總結一下,我們定義的功能類,要繼承訪問者的處理類NodeVisitor,並且還要根據不同的訪問方法來定義不同的功能函數,這個功能函數會用到NodeVisitor裏的visit方法,可以根據字符串處理繼續遞歸訪問下去。
十、你想讓某個類的實例支持標準的比較運算(比如>=,!=,<=,<等),但是又不想去實現那一大丟的特殊方法。
Python類對每個比較操作都需要實現一個特殊方法來支持。 例如為了支持>=操作符,你需要定義一個 __ge__()
方法。 盡管定義一個方法沒什麽問題,但如果要你實現所有可能的比較方法那就有點煩人了。
遺漏點:裝飾器 functools.total_ordering
就是用來簡化這個處理的。 使用它來裝飾一個來,你只需定義一個 __eq__()
方法, 外加其他方法(__lt__, __le__, __gt__, or __ge__)中的一個即可。 然後裝飾器會自動為你填充其它比較方法。
作為例子,我們構建一些房子,然後給它們增加一些房間,最後通過房子大小來比較它們:
from functools import total_ordering class Room: def __init__(self, name, length, width): self.name = name self.length = length self.width = width self.square_feet = self.length * self.width @total_ordering class House: def __init__(self, name, style): self.name = name self.style = style self.rooms = list() @property def living_space_footage(self): return sum(r.square_feet for r in self.rooms) def add_room(self, room): self.rooms.append(room) def __str__(self): return ‘{}: {} square foot {}‘.format(self.name, self.living_space_footage, self.style) def __eq__(self, other): return self.living_space_footage == other.living_space_footage def __lt__(self, other): return self.living_space_footage < other.living_space_footage
這裏我們只是給House類定義了兩個方法:__eq__()
和 __lt__()
,它就能支持所有的比較操作:
h1 = House(‘h1‘, ‘Cape‘) h1.add_room(Room(‘Master Bedroom‘, 14, 21)) h1.add_room(Room(‘Living Room‘, 18, 20)) h1.add_room(Room(‘Kitchen‘, 12, 16)) h1.add_room(Room(‘Office‘, 12, 12)) h2 = House(‘h2‘, ‘Ranch‘) h2.add_room(Room(‘Master Bedroom‘, 14, 21)) h2.add_room(Room(‘Living Room‘, 18, 20)) h2.add_room(Room(‘Kitchen‘, 12, 16)) h3 = House(‘h3‘, ‘Split‘) h3.add_room(Room(‘Master Bedroom‘, 14, 21)) h3.add_room(Room(‘Living Room‘, 18, 20)) h3.add_room(Room(‘Office‘, 12, 16)) h3.add_room(Room(‘Kitchen‘, 15, 17)) houses = [h1, h2, h3] print(‘Is h1 bigger than h2?‘, h1 > h2) # prints True print(‘Is h2 smaller than h3?‘, h2 < h3) # prints True print(‘Is h2 greater than or equal to h1?‘, h2 >= h1) # Prints False print(‘Which one is biggest?‘, max(houses)) # Prints ‘h3: 1101-square-foot Split‘ print(‘Which is smallest?‘, min(houses)) # Prints ‘h2: 846-square-foot Ranch‘
python死磕三之類與對象