1. 程式人生 > >python死磕三之類與對象

python死磕三之類與對象

too 調用 all kit text ... 好處 根據 業務

  面向對象編程,說起來很抽象,也許一百個人有一百種答案,最基本的三大概念無疑就是:封裝,繼承和多態,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(bGET /index.html HTTP/1.0\r\n)
    s.send(bHost: 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死磕三之類與對象