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'