PythonCookbook第八章(类与对象)【马虎完结】
为了对类有更加深入的理解,继续学习类相关知识。
8.1修改实例的字符串标识
class Pair: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): # !r标志__repr__输出 return 'Pair({0.x!r}), ({0.y!r})'.format(self) def __str__(self): # !s标志__str__输出 return '({0.x!s}), ({0.y!s})'.format(self)
讨论理解
__repr__()的标准做法是让它产生的字符串文本能够满足eval(repx(x)) == x。
或者产生一个一段有帮助意义的文本
>>> f = open('t8_1_2.py') >>> f <_io.TextIOWrapper name='t8_1_2.py' mode='r' encoding='UTF-8'> >>>
没有定义__str__会用__repr__输出来备份
最后写一种%强制__repr__格式化输出
def __repr__(self): # !r标志__repr__输出 return 'Pair (%r, %r)' % (self.x, self.y)
8.2自定义字符串的输出格式
像让对象通过foramt()函数和字符串方法支持自定义的输出格式。
_format = { 'ymd':'{d.year}-{d.month}-{d.day}', 'mdy':'{d.month}/{d.day}/{d.year}', 'dmy':'{d.day}/{d.month}/{d.year}', } class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day def __format__(self, format_spec): # 默认format函数没有输入参数为空字符 if format_spec == '': format_spec = 'ymd' fmt = _format[format_spec] # 通过.format的形式,通过关键字的形式,属性取值格式化输出 return fmt.format(d=self) ''' format(instance,[arg]),第二个为传入给__format__的参数 '{:xxx}'.format(d)等同与format(d, xxx),冒号后面为传入给format的参数 '''
shijianzhongdeMacBook-Pro:chapter_8 shijianzhong$ python3 -i t8_2_2.py >>> d = Date(2012, 12, 21) >>> format(d) '2012-12-21' >>> format(d,'dmy') '21/12/2012' >>>
>>> 'The Date is {:mdy}'.format(d)
'The Date is 12/21/2012'
>>> 'The Date is {}'.format(d)
'The Date is 2012-12-21'
>>> 'The Date is {:dmy}'.format(d)
'The Date is 21/12/2012'
讨论
__format__()方法在Python的字符串格式化功能中提供了一个钩子。需要强调的是,对格式化代码的解释完全取决与类本身。因此,格式化代码几乎可以为任何形式
In [76]: d =date(2020,12,12) In [77]: d Out[77]: datetime.date(2020, 12, 12) In [78]: format(d) Out[78]: '2020-12-12' In [79]: format(d,'%A, %B %dm %Y') Out[79]: 'Saturday, December 12m 2020' In [80]: 'The end is {:%d -%b -%y}.'.format(d) Out[80]: 'The end is 12 -Dec -20.' In [81]:
8.3让对象支持上下问管理协议
我们想让对象支持上下文管理协议(context-management protocol, 通过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_type, exc_val, exc_tb): # 三个参数分别为异常类型,值,和对挂起异常的追溯 print(exc_type, exc_val, exc_tb, sep='\n') self.sock.close() self.sock = None from functools import partial # 初始化类 conn = LazyConnection(('www.python.org', 80)) # 通过with激活上下文管理 with conn as s: 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') # 通过iter'哨符',当接收到空字符结束 resp = b''.join(iter(partial(s.recv, 8129),b'')) # 输出响应 print(resp.decode()) print('over')
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_3_2.py HTTP/1.1 301 Moved Permanently Server: Varnish Retry-After: 0 Location: https://www.python.org/index.html Content-Length: 0 Accept-Ranges: bytes Date: Mon, 03 Feb 2020 08:29:25 GMT Via: 1.1 varnish Connection: close X-Served-By: cache-hnd18739-HND X-Cache: HIT X-Cache-Hits: 0 X-Timer: S1580718565.404911,VS0,VE0 Strict-Transport-Security: max-age=63072000; includeSubDomains None None None over Process finished with exit code 0
讨论:
上下文管理器最常用于需要管理类似文件、网络链接和锁这样的资源程序中。
通过对前面的代码进行修改,可以实现嵌套式的with语句一次创建多个链接。
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.connections = [] def __enter__(self): sock = socket(self.family, self.type) sock.connect(self.address) # 将一个链接装入容器列表 self.connections.append(sock) return sock def __exit__(self, exc_type, exc_val, exc_tb): # 三个参数分别为异常类型,值,和对挂起异常的追溯 print(exc_type, exc_val, exc_tb, sep='\n') # 弹出连接,并将它关闭 self.connections.pop().close()
这样的情况下,可以在with里面嵌套式使用,一次创建多个连接。
8.4当创建大量实例时如何节省内存
问题:
当创建百万级别的实例,为此占用了大量的内存
解决方案:
可以在类里面增加__slot__属性,减少对内存的使用,这样每个实例不在创建一个__dict__的字典属性,而且围绕着一个固定长度的小型数组来构建,
__slot__中列出的属性名会在内部映射到这个数组特定的索引上,但使用了这个也就不能为实例添加新的属性了。
class Date:
# 书中用的列表,流畅的Python建议用元祖,我个人认为元祖更加合适。
__slots__ = ('year', 'month', 'day')
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
class Date2:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
if __name__ == '__main__':
from memory_profiler import profile
@profile
def run1():
[Date('2010', '10', '10') for i in range(10000)]
@profile
def run2():
[Date2('2010', '10', '10') for i in range(10000)]
run1()
run2()
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_4_2.py Filename: /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_4_2.py Line # Mem usage Increment Line Contents ================================================ 20 35.9 MiB 35.9 MiB @profile 21 def run1(): 22 36.8 MiB 0.1 MiB [Date('2010', '10', '10') for i in range(10000)] Filename: /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_4_2.py Line # Mem usage Increment Line Contents ================================================ 23 36.1 MiB 36.1 MiB @profile 24 def run2(): 25 37.9 MiB 0.0 MiB [Date2('2010', '10', '10') for i in range(10000)] Process finished with exit code 0
根据我实际的测试结果来看,内存的差距不大,可能数据标本不够大,书中的要求,要百万级别的实例
讨论
__slot__一般用的比较少,除非实例很多。__slot__可以阻止给用户实例添加新的属性,但这是__slot__带来的副作用,主要还时用来优化用的
8.5 将名称封装到类中
问题:我们想将'私有'数据分装到类的实例上,但是又需要考虑到Python缺乏对属性的访问控制问题
解决方案
我们一般单下划线的名称被认为属于内部使用,双下划线的名称会被重复名,是为了继承这样的属性不能通过继承而覆盖。
class A: def __init__(self): # 类内部使用 self._internal = 0 self.public = 1 def public_method(self): ... # 内部方法 def _internal_method(self): ... class B: def __init__(self): self.__private = 0 def __private_method(self): print('B__private_method') # 公开方法下执行的__私有方案,可以被子类继承,实例使用 def public_method(self): self.__private_method() class C(B): def __init__(self): super(C, self).__init__() self.__private = 1 def __private_method(self): ... if __name__ == '__main__': b = B() c = C() print(vars(b)) print(vars(c)) print(dir(b)) print(dir(c)) # 调用父类的公开方法,执行父类公开方法里面的私有方案 c.public_method()
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_5_2.py {'_B__private': 0} {'_B__private': 0, '_C__private': 1} ['_B__private', '_B__private_method', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'public_method'] ['_B__private', '_B__private_method', '_C__private', '_C__private_method', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'public_method'] B__private_method Process finished with exit code 0
讨论:
一般我们命名私有方法属性,只要_单下划线就可以了,但如果设计到子类化处理,而且有些内部属性应该对子类进行影藏,那么此时就应该使用双下划线开头。
单变量名与保留字冲突可以在名字后面加下划线
8.6 创建可管理的属性
在对实例属性的获取和设定上,我们希望增加一些额外的处理过程。
解决方案
通过propety把方法当做属性来用
class Person: def __init__(self, first_name): self.first_name = first_name @property def first_name(self): # 通过__dict__取值不会激活property装饰方法运行 return self.__dict__['first_name'] @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') # self._first_name = value self.__dict__['first_name'] = value @first_name.deleter def first_name(self): raise AttributeError("Can't delete attribute") if __name__ == '__main__': p = Person('sidian') print(p.first_name) p.first_name = 'wudian' print(p.first_name) del p.first_name
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_6_2.py sidian wudian Traceback (most recent call last): File "/Users/shijianzhong/study/PythonCookbook/chapter_8/t8_6_2.py", line 26, in <module> del p.first_name File "/Users/shijianzhong/study/PythonCookbook/chapter_8/t8_6_2.py", line 19, in first_name raise AttributeError("Can't delete attribute") AttributeError: Can't delete attribute Process finished with exit code 1
刚刚测试中发现,使用特性的话,不能重写父类的__getattribute__方法,要不然self.xxx执行特性会变成属性赋值。
class Person2: def __init__(self, first_name): # 书中的写法 self.set_first_name(first_name) # 我认为的写法 self.name = first_name def get_first_name(self): # 通过__dict__取值不会激活property装饰方法运行 # return self.__dict__['first_name'] return self._first_name def set_first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value # self.__dict__['first_name'] = value def del_first_name(self): raise AttributeError("Can't delete attribute") # 创建特性 name = property(get_first_name, set_first_name, del_first_name)
这是通过调用property复制类属性创建特性。
讨论
property属性实际上就时把一系列方法绑定在一起
>>> Person.first_name.fget <function Person.first_name at 0x107c218c0> >>> dir(Person.first_name) ['__class__', '__delattr__', '__delete__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__isabstractmethod__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__set__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'deleter', 'fdel', 'fget', 'fset', 'getter', 'setter'] >>>
但当我们访问property属性时会自动触发这些方法的调用。
如果对属性的读取与复制没有处理任何任务,不要写成下面的方式。
class Person: def __init__(self, first_name): self.first_name = first_name @property def first_name(self): return self._first_name @first_name.setter def first_name(self, value): self._first_name = value
有几处不好,第一让代码变的啰嗦,第二让程序变换很多。
特别是如果稍后要对莫个属性增加额外的处理步骤的时候,在不修改代码的情况下,把特性名修成与实例属性名一样,这样访问一个属性的语法并不会改改变(即,访问普通属性与访问property属性的代码一样)
简单的在一个方法上加上@property,都能够简单的通过属性的方法对方法进行调用。
不要重复大量的出现重复性的property,可以通过流畅的Python书中介绍的,通过描述符或者特性工厂函数完成同样的任务。
8.7 调用父类中的方法
我们想调用一个父类中的方法,这个方法在子类中已经被覆盖。
解决方案
调用父类(超类)中的方法,可以使用super()函数完成。
class A: def spam(self): print('A.spam') class B(A): def spam(self): print('B.spam') # 执行父类方法 super().spam() class C: def __init__(self): self.x = 0 class D(C): def __init__(self): # 执行父类的初始化 super().__init__() self.y = 0 # 常用用途,覆盖Python中的特殊方法 class Proxy: def __init__(self, obj): self._obj = obj # 获取_obj对象的item属性 def __getattr__(self, item): return getattr(self._obj, item) def __setattr__(self, key, value): # 待下划线的属性正常赋值 if key.startswith('_'): # 执行父类的方法,给对象属性赋值 super().__setattr__(key, value) else: # 符合条件给_obj赋值,(调用self的__setattr__) setattr(self._obj, key, value) if __name__ == '__main__': b = B() b.spam() d = D() print(vars(d)) pro = Proxy(b) pro.name = 'sidian' print(pro.name) print(vars(pro))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_7_2.py B.spam A.spam {'x': 0, 'y': 0} sidian {'_obj': <__main__.B object at 0x101ef87d0>} Process finished with exit code 0
讨论
super函数我前面写过,书中也写了,多重继承的时候,super比较好的是,就只能父类一次,而且是根据__mro__表执行。
当使用super()函数时,Python会继续从MRO中的写一个类开始搜索。只要每一个重新定义过的方法(也就是覆盖方法)都使用了super(),并且只调用了它一次,那么控制流最终就可以遍历整个MRO列表,并且让每个方法只会被调用一次。
书中下面的有一个案例非常有意思。下面上代码
In [83]: class A: ...: def spam(self): ...: print('A.spam') ...: super().spam() ...: In [84]: a= A() In [85]: a.spam() A.spam --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-85-dc5100c4b7e0> in <module> ----> 1 a.spam() <ipython-input-83-5385bab65838> in spam(self) 2 def spam(self): 3 print('A.spam') ----> 4 super().spam() 5 AttributeError: 'super' object has no attribute 'spam' In [86]: class B: ...: def spam(self): ...: print('B.spam') ...: In [88]: class C(A, B): ...: pass ...: ...: In [89]: c= C() In [90]: c.spam() A.spam B.spam In [91]: C.__mro__ Out[91]: (__main__.C, __main__.A, __main__.B, object) In [92]:
A与B类完全没有关系,但A中的super居然调用到了B的spam,这一切就是MRO列表能解释
由于super()可能会调用到我们不希望调用的方法,那么这里有一些基本准则。
首相,确保在继承体系中所有相同的方法都有可兼容 调用签名(参数数量想同,参数名称也想用)。
如果super()尝试去调用非直接父类的方法,那么这就可以确保不会遇到麻烦。其次,确保最顶层的类实现了这个方法通常是个好主意。这个沿着MRO列表展开的查寻链会因为最终找到了事件的方法而终止。
8.8在子类中扩展属性
我们想在子类中扩展莫个属性的功能,而这个属性在父类已经被特性或者描述符定义了。
解决方案
class Person: def __init__(self, name): self.name = name @property def name(self): return self._name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._name = value @name.deleter def name(self): raise AttributeError("Can't delete attribute") class SubPerson(Person): @property def name(self): # 这种也可以 # return super(SubPerson, SubPerson).name.__get__(self) # 书中的方式 return super().name @name.setter def name(self, value): print('Setting name to', value) # 通过父类调用类属性name内置的__set__方法传入参数 super(SubPerson, SubPerson).name.__set__(self, value) # 本来想直接调用父类实例属性赋值的方法,激活特性赋值,但报错了,特性赋值不能直接继承 # super(SubPerson, self).name = value @name.deleter def name(self): print('Deleting name') super(SubPerson, SubPerson).name.__delete__(self) if __name__ == '__main__': s = SubPerson('sidian') print(s.name) s.name = 'wudian' print(s.name)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_8_2.py Setting name to sidian sidian Setting name to wudian wudian Process finished with exit code 0
讨论
因为属性被定义为(getter,setter,delete)的集合,而不仅仅时单独的方法,需要搞清楚时定义所有的方法还时定义其中的一个方法。
上面的例子是定义了全部方法。为了调用setter函数之前的实现,唯一能调用到这个方法的方式就时以类变量而不是实例变量的方式去访问。
后面通过描述符的写法更加好理解。
如果只想扩展属性中的一个方法,可以用下面的方法,而且这么做,前面定义过 属性方法都会拷贝过来。
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
@name.deleter
def name(self):
raise AttributeError("Can't delete attribute")
class SubPerson(Person):
'''
可以三种方法全部写,也可以写其中的一种,比写property方便。
因为写property继承的话,需要三个读写,才能全部继承过来
'''
# @Person.name.getter
# def name(self):
# print('Getting name')
# return super(SubPerson, self).name
#
# @Person.name.setter
# def name(self, value):
# if not isinstance(value, str):
# raise TypeError('Expected a string')
# super(SubPerson, SubPerson).name.__set__(self, value)
@Person.name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
if __name__ == '__main__':
s = SubPerson('sidian')
print(s.name)
s.name = 'wudian'
print(s.name)
# del s.name
最后通过描述符的实例赋值类属性来看父类调用,就更加容易理解了。
# 创建描述符 class String: def __init__(self, name): self.name = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, str): raise TypeError('Expected a string') instance.__dict__[self.name] = value class Person: # 描述符初始化 name = String('name') def __init__(self, name): self.name = name class SubPerson(Person): @property def name(self): print('Getting name') return super(SubPerson, self).name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') # 调用父类的的类属性也就时描述符实例的方法,使用描述符实例的方法 super(SubPerson, SubPerson).name.__set__(self, value) @name.deleter def name(self): print('Deleting name') super(SubPerson, SubPerson).name.__delete__(self) if __name__ == '__main__': s = SubPerson('sidian') print(s.name) s.name = 'wudian' print(s.name) del s.name
再次强调本章的关键:为了调用setter函数之前的实现,唯一能调用到这个方法的方式就时以父类变量而不是实例变量的方式去访问。
8.9创建一种新形式的类属性或实例属性。
问题:
想创建一种新形式的实例属性,可以拥有一些额外的功能,比如类型检查
解决方案。用描述符实例添加类属性。
class Integer: def __init__(self, name): self.name = name def __get__(self, instance, owner): # 类调用属性返回实例本身 if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, int): raise TypeError('Expected an int') # 托管类实例属性赋值 instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] class Point: x = Integer('x') y = Integer('y') def __init__(self, x, y): self.x = x self.y = y if __name__ == '__main__': p = Point(2, 3)
shijianzhongdeMacBook-Pro:chapter_8 shijianzhong$ python3 -i t8_9_2.py >>> p = Point(2, 3) >>> p.x 2 >>> p.x = '2' Traceback (most recent call last): File "<stdin>", line 1, in <module> File "t8_9_2.py", line 15, in __set__ raise TypeError('Expected an int') TypeError: Expected an int >>>
讨论
描述符都提供了底层的魔法,包括@classmethod @staticmethod @property __slot__
描述符是高级程序库和框架的作者们锁使用 的最为重要的工具之一。
下面一个大神写的通过装饰器工厂函数,批量给类上属性,属性为描述符实例。
class Typed: def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, instance, owner): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, self.expected_type): raise TypeError('Expected' + str(self.expected_type)) instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] #带参数的装饰器,装饰器完成运行后,直接返回cls def typeassert(**kwargs): # 传入对象,这里时类本事 def decorate(cls): for name, expected_type in kwargs: setattr(cls,name, Typed(name, expected_type)) return cls return decorate # 主要靠这个带参数的装饰器,完成托管类的描述符类属性赋值 @typeassert(name=str, shares=int, price=float) class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price
高手就时高手,通过了带参数的装饰器,传入参数,然后给类属性赋值,很好的学习到了
8.10 让属性具有惰性求值的能力
问题
想将一个只读的属性定义为property属性方式,只有在访问它时才参与计算。但一旦访问了该属性,希望把值缓存到对象属性,
解决方案
这里其实用到了描述符只有__get__属性的时候,会被同名的自身属性覆盖,流畅的Python书中有更加详细的介绍。流畅的Python书中没有案例,当时我自己能力有限,没能想出实现方式,这里有了。
class lazyproperty: def __init__(self, func): self.func = func def __get__(self, instance, owner): if instance is None: return self else: value = self.func(instance) # 托管实例,属性赋值 instance.__dict__[self.func.__name__] = value return value # 有了__set__就是不可覆盖的描述符了 # def __set__(self, instance, value): # raise TypeError() import math class Circle: def __init__(self, radius): self.radius = radius @lazyproperty def area(self): print('Computing area') return math.pi * self.radius ** 2 @lazyproperty def perimeter(self): print('Computing perimeter') return 2 * math.pi * self.radius if __name__ == '__main__': c = Circle(4.0) print(c.area) print(c.area) c.area = 50 print(vars(c))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_10_2.py Computing area 50.26548245743669 50.26548245743669 {'radius': 4.0, 'area': 50} Process finished with exit code 0
通过测试来看,这个很想官方的property去掉了__set__与__delete__版本,当我加上了__set__以后,就成为了不可覆盖的类型,每次读取该属性,都会调用描述符。
讨论
由于前面的属性计算出来以后,可以被修改,这样会非常的不安全,书中还有一种计算属性赋值以后,就不会被修改的装饰器,我没看懂,现在测试看看。
# 装饰器函数,返回一个特性,特性只能被读取。 def lazyproperty(func): name = '_lazy_' + func.__name__ @property # 调用读取特性内部的函数,去查寻对象是否赋值属性 def lazy(instance): if hasattr(instance, name): return getattr(instance, name) else: value = func(instance) setattr(instance, name, value) return value # 返回只带有__get__的特性 return lazy import math class Circle: def __init__(self, radius): self.radius = radius # 调用装饰器函数 @lazyproperty def area(self): print('Computing area') return math.pi * self.radius ** 2 @lazyproperty def perimeter(self): print('Computing perimeter') return 2 * math.pi * self.radius if __name__ == '__main__': c = Circle(4.0) print(c.area) print(c.area) c.area = 50 print(vars(c))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_10_2.py Traceback (most recent call last): File "/Users/shijianzhong/study/PythonCookbook/chapter_8/t8_10_2.py", line 38, in <module> c.area = 50 AttributeError: can't set attribute Computing area 50.26548245743669 50.26548245743669 Process finished with exit code 1
上面我已经对书中的一些代码做了解释,应该89不离十正确,
# 装饰器函数,返回一个特性,特性只能被读取。 def lazyproperty(func): name = '_lazy_' + func.__name__ # @property # 调用读取特性内部的函数,去查寻对象是否赋值属性 def lazy(instance): if hasattr(instance, name): return getattr(instance, name) else: value = func(instance) setattr(instance, name, value) return value # 返回只带有__get__的特性 lazy = property(lazy) return lazy
这样写也可以,就更加明显了,返回了一个特性,特性里面的fget去执行具体逻辑。
8.11 简化数据结果的初始化过程
问题
我们编写了很多类,把它们当做数据结构来用。但是我们厌倦了编写高度重复且样式想同的__init__()函数
解决方案
class Structure: _fields = [] def __init__(self, *args): if len(self._fields) != len(args): raise TypeError('Expected {} argument'.format(len(self._field))) # 通过循环给实例赋值属性 for name, value in zip(self._fields, args): setattr(self, name, value) if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): # __slots__ = ('x', 'y') _fields = ['x', 'y'] p = Point(1, 2) print(vars(p))
通过一个父类,继承以后初始化实例属性,这种写法在Django里面好像看到过。
加入实例化参数的时候,输入的时候,有关键字传参,书中也有了完美的解决的方案。
高手就是高手
class Structure: _fields = [] def __init__(self, *args, **kwargs): # 如果默认参数大于属性数量,那直接报错误了 if len(self._fields) < len(args): raise TypeError('Expected {} argument'.format(len(self._fields))) # 通过循环给实例赋值属性 for name, value in zip(self._fields, args): setattr(self, name, value) # 产看默认参数的传值数量,是否有空缺 for name in self._fields[len(args):]: # 关键字参数赋值 setattr(self, name, kwargs.pop(name)) # 如果字典有值,说明关键字参数多了 if kwargs: raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs))) if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): # __slots__ = ('x', 'y') _fields = ['x', 'y'] p = Point(1, 2) print(vars(p))
还有另外一种可能,通过利用关键字传入额外的属性。
class Structure: _fields = [] def __init__(self, *args, **kwargs): # 如果默认参数不等于属性数量,那直接报错误了 if len(self._fields) != len(args): raise TypeError('Expected {} argument'.format(len(self._fields))) # 通过循环给实例赋值属性 for name, value in zip(self._fields, args): setattr(self, name, value) # 差集算出来 extra_args = kwargs.keys() - set(self._fields) for name in extra_args: # 关键字参数赋值 setattr(self, name, kwargs.pop(name)) # 如果字典有值,说明添加的属性与默认属性冲突了 if kwargs: raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs))) if __name__ == '__main__': class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): # __slots__ = ('x', 'y') _fields = ['x', 'y'] p = Point(1, 2, name='sidian') print(vars(p))
讨论
如果编写的程序中有大量的小型数据结构,那么定义一个通用性的__init__()方法会特别有用。
对于属性的赋值,书中说明了为什么不能
class Structure: _fields = [] def __init__(self, *args): if len(self._fields) != len(args): raise TypeError('Expected {} argument'.format(len(self._fields))) # 通过属性字典赋值 self.__dict__.tupdate(zip(self._fields, args))
因为如果子类有property属性验证的化,这样不安全,如果子类有__slot__的属性的话,拿直接报错了。
这个技术一个潜在的缺点就是会影响到IDE(集成开发环境)的文档和帮助功能。
help(Stock)
class Stock(Structure) | Stock(*args) | | Method resolution order: | Stock | Structure | builtins.object | | Data and other attributes defined here: | | _fields = ['name', 'shares', 'price'] | | ---------------------------------------------------------------------- | Methods inherited from Structure: | | __init__(self, *args) | Initialize self. See help(type(self)) for accurate signature. | | ---------------------------------------------------------------------- | Data descriptors inherited from Structure: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) (END)
需要编写一个功能函数,逻辑还是很简单的,就是通过读取初始化函数里面的参数。
def init_fromlocals(self): import sys # 获取调用方函数的局部变量里面的参数 locs = sys._getframe(1).f_locals # 去掉self本身 for k, v in locs.items(): if k != 'self': setattr(self, k, v)
8.12定义一个接口或抽象基类。
问题
定义一个抽象基类,可以执行类型检查或者确保子类实现特定方法。
解决方案
import abc class IStream(abc.ABC): # 书中的老式写法 # class IStream(metaclass=abc.ABCMeta): # 抽象方法,子类必须继承,然后复写 @abc.abstractmethod def read(self, maxbytes = -1): ... @abc.abstractmethod def wirite(self, data): ... class SocketStream(IStream): def read(self, maxbytes = -1): ... def wirite(self, data): ... class A(abc.ABC): # 可以抽象基类方法添加到静态方法,类方法,property上,但abstractmethod必须就在方法上面 @property @abc.abstractmethod def name(self): pass @name.setter @abc.abstractmethod def name(self): pass if __name__ == '__main__': s = SocketStream() import io # 注册为虚拟子类 IStream.register(io.IOBase) f = open('t8_11_2.py') print(f) print(isinstance(f, IStream))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_12_2.py <_io.TextIOWrapper name='t8_11_2.py' mode='r' encoding='UTF-8'> True Process finished with exit code 0
书中还讲到了虚拟子类,这个其实流畅的Python书中讲的更加仔细,但我差不多忘了很多了,晕死
讨论
Python的collections模块中定义了很多容器很迭代器相关的抽象基类,numbers库中定义了和数值相关的抽象基类。io库中定义了和I/O处理相关的抽象基类。
尽管抽象基类使得类型检查变得更容易,但不应该在程序中过度使用它。Python的核心在于它使一种动态语言,它带来了极大的灵活性。如果处处都强制实行类型约束,则会使代码变得更加复杂,而这是不应该的。我们应该拥抱Python的灵活性。
8.13实现一种数据模型或类型系统
我们想定义各种各样的数据结构,但是对于某些特定属性,我们想对允许赋给它们的值添加一些限制
解决方法
书中三种方法,一种通过描述符类的继承,类装饰器,和元类
class Descriptor: # 类似与基类 def __init__(self, name=None, **kwargs): # 初始化描述符实例参数 self.name = name for key, value in kwargs.items(): setattr(self, key, value) def __set__(self, instance, value): # 通过托管类实例__dict__属性赋值,不会激活__set__避免死循环 instance.__dict__[self.name] = value class Typed(Descriptor): # 定义一个类属性,给实例调用。 expected_type = type(None) # 重写父类方法 def __set__(self, instance, value): if not isinstance(value, self.expected_type): raise TypeError('expected' + str(self.expected_type)) super().__set__(instance, value) class Unsigned(Descriptor): def __set__(self, instance, value): if value < 0: raise ValueError('Expected >= 0') super().__set__(instance, value) class MaxSized(Descriptor): def __init__(self, name=None, **kwargs): if 'size' not in kwargs: raise TypeError('missing size option') super().__init__(name, **kwargs) def __set__(self, instance, value): if len(value) >= self.size: raise ValueError('size must be < ' + str(self.size)) super(MaxSized, self).__set__(instance, value) class Integer(Typed): expected_type = int class UnsignedInteger(Integer, Unsigned): ... class Float(Typed): expected_type = float class UnsignedFloat(Float, Unsigned): ... class String(Typed): expected_type = str class SizedString(String, MaxSized): ... if __name__ == '__main__': class Stock: name = SizedString('name', size=8) shares = UnsignedInteger('shares') price = UnsignedFloat('price') def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price # s = Stock('sidian', 15, 18.1) def check_attributes(**kwargs): def decorate(cls): for key, value in kwargs.items(): if isinstance(value, Descriptor): value.name = key setattr(cls,key, value) else: setattr(cls, key, value()) return cls return decorate # 类装饰器工厂函数,传入参数,返回类本身 @check_attributes(name=SizedString(size=8),shares=UnsignedInteger, price=UnsignedFloat) class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price # 元类的方式 class CheckMeta(type): def __new__(cls, clsname, bases, methods): for key, value in methods.items(): if isinstance(value, Descriptor): value.name = key # 这里的super调用里面的参数cls还需要手动填写,可以直接把super换成type return super().__new__(cls, clsname, bases, methods) class Stock2(metaclass=CheckMeta): name = SizedString(size=8) shares = UnsignedInteger() price = UnsignedFloat() def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price s2 = Stock2('sdiian', 1, 2.0) print(vars(s2))
三种方式如果描述符用的少的化,我觉的第一种对方便,如果用的多,用元类也可以,第二种的装饰器写法写的感觉不好,通过装饰器给类属性赋值描述符实例有点麻烦。
讨论
书中的方法说,类装饰器可以提供最大的灵活性和稳健性,第一,这种解决方案步依赖与任何高级的机制,比如说元类。第二,装饰器可以很容易地根据需要在类定义上添加或者删除。
最后书中用了类装饰器的解决方案取代mixin、多重继承以及super()函数的使用。
class Descriptor: # 类似与基类 def __init__(self, name=None, **kwargs): # 初始化描述符实例参数 self.name = name for key, value in kwargs.items(): setattr(self, key, value) def __set__(self, instance, value): # 通过托管类实例__dict__属性赋值,不会激活__set__避免死循环 instance.__dict__[self.name] = value def Typed(expected_type, cls=None): # 这个比较巧妙,为了使用装饰器工厂,内部必须还要一个函数 # 这里书中默认用了匿名函数,我卡了很长事件,其实装饰器工厂,第一个函数 # 的参数是工厂函数的填写参数,内部第二个参数为被装饰的部分(这里就是类) # 最后返回类 if cls is None: # 我可以把匿名函数改成普通函数理解起来更加方便 # return lambda cls: Typed(expected_type, cls) def wapper(cls): return Typed(expected_type, cls) return wapper # 整个装饰器的逻辑还是比较简单的,通过工厂函数传入参数 # 读取类属性,修改需要修改的属性,重新赋值类属性。 super_set = cls.__set__ def __set__(self, instance, value): if not isinstance(value, expected_type): raise TypeError('expected' + str(self.expected_type)) super_set(self, instance, value) cls.__set__ = __set__ return cls def Unsigned(cls): super_set = cls.__set__ def __set__(self, instance, value): if value < 0: raise ValueError('Expected >= 0') super_set(self, instance, value) cls.__set__ = __set__ return cls def MaxSized(cls): super_init = cls.__init__ def __init__(self, name=None, **kwargs): if 'size' not in kwargs: raise TypeError('missing size option') super_init(self, name, **kwargs) cls.__init__ = __init__ super_set = cls.__set__ def __set__(self, instance, value): if len(value) >= self.size: raise ValueError('size must be < ' + str(self.size)) super_set(self, instance, value) cls.__set__ = __set__ return cls @Typed(int) class Integer(Descriptor): ... @Unsigned class UnsignedInteger(Integer): ... @Typed(float) class Float(Descriptor): ... @Unsigned class UnsignedFloat(Float): ... @Typed(str) class String(Descriptor): ... @MaxSized class SizedString(String): ... if __name__ == '__main__': class Stock: name = SizedString('name', size=8) shares = UnsignedInteger('shares') price = UnsignedFloat('price') def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price s = Stock('sidian', 15, 18.1) print(vars(s))
说实话,这个代码,我要是看到会骂人,明明继承都够用了,用了匿名函数,还要用装饰器工厂,这是装逼大法的最高进阶了。
但,这个类装饰器的方案要比采用mixin的方案快几乎一倍以上。
8.14实现自定义的容器
问题
我们想实现一个自定义的类,用来模仿普通的内建容器类型比较列表或者字典的行为,但是我们不知道需要定义什么方法实现。
解决方案
可以去继承然后实例一个collections.abc下面的基类,按照提示要求重写基类方法
In [36]: from collections import abc In [37]: dir(abc) Out[37]: ['AsyncGenerator', 'AsyncIterable', 'AsyncIterator', 'Awaitable', 'ByteString', 'Callable', 'Collection', 'Container', 'Coroutine', 'Generator', 'Hashable', 'ItemsView', 'Iterable', 'Iterator', 'KeysView', 'Mapping', 'MappingView', 'MutableMapping', 'MutableSequence', 'MutableSet', 'Reversible', 'Sequence', 'Set', 'Sized', 'ValuesView', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
In [38]: class Call(abc.Callable): ...: ... ...: In [39]: c= Call() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-39-3845c3e9a3fb> in <module> ----> 1 c= Call() TypeError: Can't instantiate abstract class Call with abstract methods __call__ In [40]: isinstance(Call, abc.Callable) Out[40]: True In [41]:
这个collections.abc里面的基类还是非常丰富的,我随便拿了个测试玩玩。
按照书上的例子,继承父类定义一个Sequence,序列
In [41]: class Seq(abc.Sequence): ...: ... ...: In [42]: s= Seq() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-42-2335791f0e4a> in <module> ----> 1 s= Seq() TypeError: Can't instantiate abstract class Seq with abstract methods __getitem__, __len__ In [43]:
看来只要定义了__getitem__和__len__就够了
from collections import abc import bisect class SortedItems(abc.Sequence): def __init__(self, initial=None): self._item = sorted(initial) if initial is not None else [] def __getitem__(self, index): return self._item[index] def __len__(self): return len(self._item) # 定义一个自己的插入方法 def add(self, item): bisect.insort(self._item, item) if __name__ == '__main__': items = SortedItems([5, 1, 3]) print(items[0]) # 排序后插入数据,使用了bisect.insort items.add(0) for i in items: print(i)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_14_2.py 1 0 1 3 5 Process finished with exit code 0
序列书中测试,其实包含了索引、迭代、len、in甚至分片
讨论
从collections.abc里面定义的基类可以很好的用于检查,关于各种基类的关系,可以在流畅的Python书P268页查看各种基类具体的关系
书中最后定义了一个可变序列,我按照书中的样式进行抄写,熟悉。
from collections import abc import bisect class SortedItems(abc.MutableSequence): def __init__(self, initial=None): self._item = sorted(initial) if initial is not None else [] def __getitem__(self, index): print('Getting:', index) return self._item[index] def __delitem__(self, key): del self._item[key] def __setitem__(self, key, value): self._item[key] = value def insert(self, index: int, value: object) -> None: self._item.index(index, value) def __len__(self): return len(self._item) # 定义一个自己的插入方法 def add(self, item): bisect.insort(self._item, item)
定义了可变序列,几乎支持可变列表所有的核心方法。
8.15委托属性访问
问题
我们想在访问实例的属性时能够将其委托到一个内部持有的对象上,这可以作为继承的代替方案或者是为了实现一种代理机制
解决方案
解决方案就是简单的情况下,将一个实例赋值给另一个类的初始化属性,复杂一点可以通过定义__getattr__实现
先上最简单的模式:
class A: def spam(self, x): ... def foo(self): ... class B: def __init__(self): self._a = A() def spam(self, x): return self._a.spam(x) def foo(self): return self._a.foo() def bar(self): ...
在上通过__getattr__,如果没有这个属性的时候,使用该方法
class A: def spam(self, x): ... def foo(self): ... class B: def __init__(self): self._a = A() def __getattr__(self, item): return getattr(self._a, item) def bar(self): ...
相对复杂一点点的操作,可以通过委托给对象赋值属性,删除属性,读取属性
class Proxy: def __init__(self, obj): self._obj = obj def __getattr__(self, item): return getattr(self._obj, item) def __setattr__(self, key, value): # 下划线开头属性不操作 if key.startswith('_'): super(Proxy, self).__setattr__(key, value) else: print('setattr', key, value) setattr(self._obj, key, value) def __delattr__(self, item): if item.startswith('_'): super(Proxy, self).__delattr__(item) else: print('delattr:', item) delattr(self._obj, item) class Spam: def __init__(self, x): self.x = x def bar(self, y): print('Spam.bar', self.x, y) if __name__ == '__main__': s = Spam(2) p = Proxy(s) p.bar(3) # 通过__setattr__给实例复制属性 p.x = 28 p.bar(3)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_15_2_c.py Spam.bar 2 3 setattr x 28 Spam.bar 28 3
讨论
委托有时候可以做诶继承的代替方案,但更多的时候,委托跟继承是不同的
有时候直接使用继承可能没多大意思,或者我们想跟多的控制对象之间的关系(列如只暴露特定的方法、实现接口等),此时使用委托会很有用.
同样需要强调的是__getattr__()方法通常不实用与大部分名称以双下划线开头和结果的特殊方法。
class ListLike: def __init__(self): self._item = list() def __getattr__(self, item): return getattr(self._item, item) if __name__ == '__main__': l = ListLike() # 实际使用的应该为__setitem__ l.append(2) # 后台调用的应该是__getitem__ print(l[0])
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_15_3.py Traceback (most recent call last): File "/Users/shijianzhong/study/PythonCookbook/chapter_8/t8_15_3.py", line 13, in <module> print(l[0]) TypeError: 'ListLike' object is not subscriptable Process finished with exit code 1
实际应该写成这样
class ListLike: def __init__(self): self._item = list() def __getattr__(self, item): return getattr(self._item, item) def __getitem__(self, item): return self._item[item] def __setitem__(self, key, value): self._item[key] = value def __len__(self): return len(self._item) if __name__ == '__main__': l = ListLike() # 实际使用的应该为__setitem__ l.append(2) # 后台调用的应该是__getitem__ print(l[0])
8.16 在类中定一个多个构建函数
讨论
我们正在编写一个类,但是想让用户能够以多种方式创建实例,而不是局限与__init__()提供的这一种
解决方案
使用类方法,用类装饰器,因为里面第一个参数为类本身
import time class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def today(cls): t = time.localtime() return cls(t.tm_year, t.tm_mon, t.tm_mday) def __repr__(self): return '_'.join(map(str,(self.year, self.month, self.day))) if __name__ == '__main__': print(Date(1998,1,1)) print(Date.today())
讨论
类方法可以继承给子类,这是非常好用的
还有就是定义一个有着多个构建函数的类时,应该让__init__()函数尽可能的简单,除了给属性赋值之外什么都不做。如果有其他需求,可以在其他备选的构造函数中选择更高级的操作。
import time class Date: def __init__(self, *args): if len(args) == 0: t = time.localtime() args = t.tm_year, t.tm_mon, t.tm_mday self.year, self.month, self.day = args def __repr__(self): return '_'.join(map(str,(self.year, self.month, self.day))) if __name__ == '__main__': print(Date(1998,1,1)) print(Date())
这个也可以实现同样的效果,但再运行中,表达不清楚,代码维护也会不清晰
8.17 不通过调用init来创建实例
我们需要创建一个实例,但是处于某写原因想绕过__init__()方法,用别的方式来创建。
解决方法
通过__new__方法来实现,__new__方法时不用加类方法装饰器,第一个参数为类的方法
In [49]: class Date: ...: ...: def __init__(self, year, month, day): ...: self.year = year ...: self.month = month ...: self.day = day ...: In [50]: d = Date.__new__(Date) In [51]: d Out[51]: <__main__.Date at 0x111e01fd0> In [52]: date = dict(year=2019,month=12,day=12) In [53]: for k,v in date.items(): ...: setattr(d,k,v) ...: In [54]: d.year Out[54]: 2019 In [55]:
讨论
这个可以反序列化,前面就是把JSON数据转换成模型了。
通过setattr比通过__dict__方式写去属性要好,因为__slots__、property情况下,通过__dict__写入属性会报错,第二个会忽悠筛选。
8.18 用Mixin技术来扩展类定义。
问题:
我们由一些十分有用的方法,希望用他们来扩展其他类的功能。但是,需要添加方法的这些类之间并不一定属于继承关系。因此,没法将这些方法直接关联到一个共同的基类上。
解决方法
主要提供一个基础类以及一些可选的定制化方法。
下面上书中的Minix类
from collections import defaultdict, OrderedDict class LoggedMappingMixin: # 定义不设置属性 __slots__ = () def __getitem__(self, item): print('Getting ' + str(item)) return super().__getitem__(item) def __setitem__(self, key, value): print('Setting {} = {!r}'.format(key, value)) return super().__setitem__(key, value) def __delitem__(self, key): print('Deleting ' + str(key)) return super(LoggedMappingMixin, self).__delitem__(key) class SetOnceMappingMixin: # key只能设置一次 __slots__ = () def __setitem__(self, key, value): # 假如已经在字典的keys里面 if key in self: raise KeyError(str(key), ' already set') else: super(SetOnceMappingMixin, self).__setitem__(key, value) class StringKeyMappingMixin: # key只能是str格式 __slots__ = () def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('keys must be strings') return super(StringKeyMappingMixin, self).__setitem__(key, value) class LoggedDict(LoggedMappingMixin, dict): ... class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict): ... class StringOrderDict(StringKeyMappingMixin, SetOnceMappingMixin, OrderedDict): ...
下面是一些简单的运行结果。
shijianzhongdeMacBook-Pro:chapter_8 shijianzhong$ python3 -i t8_18_2.py >>> logger = Logged LoggedDict( LoggedMappingMixin( >>> logger = Logged LoggedDict( LoggedMappingMixin( >>> logger = LoggedDict() >>> logger['x'] = 23 Setting x = 23 >>> logger['x'] Getting x 23 >>> del logger['x'] Deleting x shijianzhongdeMacBook-Pro:chapter_8 shijianzhong$ python3 -i t8_18_2.py >>> once_d = SetOnceDefaultDict() >>> once_d['x'] = 12 >>> once_d = SetOnceDefaultDict(list) >>> once_d['x'] = 12 >>> once_d['x'] 12 >>> once_d['b'].append(12) >>> once_d['b'] [12] >>> once_d['b'] = 12 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "t8_18_2.py", line 29, in __setitem__ raise KeyError(str(key), ' already set') KeyError: ('b', ' already set') >>> d3 = StringOrderDict(x=2,b=3) >>> d3 StringOrderDict([('x', 2), ('b', 3)]) >>> d3['x'] = 12 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "t8_18_2.py", line 40, in __setitem__ return super(StringKeyMappingMixin, self).__setitem__(key, value) File "t8_18_2.py", line 29, in __setitem__ raise KeyError(str(key), ' already set') KeyError: ('x', ' already set') >>> d3[15] = 10 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "t8_18_2.py", line 39, in __setitem__ raise TypeError('keys must be strings') TypeError: keys must be strings
上面就可以看到mixin类与其他已有的类的混合。
讨论:
mixin类绝对不是为了直接实例化而创建的,mixin没有__init__,说明一般是没有状态属性的。
如果一定要定义一个拥有__init__()方法以及实例变量的的mixin类,有极大的风险,最好用*args, 关键字参数,**kwargs传参。
书中实例。
class RestrictKeysMixin: # key只能是str格式 def __init__(self, *args, _restrict_key_type, **kwargs): self._restrict_key_type = _restrict_key_type # 这步很关键,可以根据__mro__去寻找继承表里面的下一个__init__方法,列子中就是dict super(RestrictKeysMixin, self).__init__(*args, **kwargs) def __setitem__(self, key, value): if not isinstance(key, self._restrict_key_type): raise TypeError('keys must be ', str(self._restrict_key_type)) return super(RestrictKeysMixin, self).__setitem__(key, value) class RDict(RestrictKeysMixin, dict): ... if __name__ == '__main__': d1 = RDict(_restrict_key_type=str) d2 = RDict([['name','Dave'], ['n', 37]], _restrict_key_type=str) d3 = RDict(name='Dvae', n=37, _restrict_key_type=str) print('pass') d1[12] = '12'
最后书中用了类装饰器,原理也比较简单,进去一个类,处理一个类,然后修改一些类定义的方法。
def LoggerMapping(cls): # 读取类属性的函数 cls_getitem = cls.__getitem__ cls_setitem = cls.__setitem__ cls_delitem = cls.__delitem__ # 定义一些函数 def __getitem__(self, key): print('Getting ', str(key)) return cls_getitem(self, key) def __setitem__(self, key ,value): print(f'Setting {key} = {value!r}') cls_setitem(self, key, value) def __delitem__(self, key): print(f'Deleting ' + str(key)) cls_delitem(self, key) # 覆盖类属性原来的函数 cls.__getitem__ = __getitem__ cls.__setitem__ = __setitem__ cls.__delitem__ = __delitem__ return cls @LoggerMapping class LoogedDict(dict): ... looged = LoogedDict(a=1, b=2) looged['a'] = 5 del looged['a'] looged['b'] print(looged)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_18_3_a.py Setting a = 5 Deleting a Getting b {'b': 2} Process finished with exit code 0
其实有点问题,在初始化的时候,没有激活__setitem__
8.19实现带有状态的对象或状态机
问题:
我们想实现一个状态机,或者让对象可以在不同的状态中进行操作,但是我们并步希望代码里会因此出现大量的条件判断
解决方案:
其实这个解决方案,我是真心看的好累,用一个类定义一种状态是书中的方法。
下面是书中觉的使用if条件下的状态。
class Connection: def __init__(self): self.state = 'CLOSED' def read(self): if self.state != 'OPEN': raise TimeoutError('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' ''' 这个代码不好的地方是引入了许多针对状态的条件检查,代码变得很复杂。其次,程序的性能下降了。 因为普通的操作如读和写总在在处理浅先检查状态 '''
书中用了一种更加优雅的方式,将每一种状态定义成一个单独的类,然后在Connection类中使用这些状态类
class ConnectionState: @staticmethod def read(con): raise NotImplementedError() @staticmethod def write(con): raise NotImplementedError() @staticmethod def open(con): raise NotImplementedError() @staticmethod def close(con): raise NotImplementedError() class ClosedConnectionState(ConnectionState): @staticmethod def read(con): raise RuntimeError('Not Open') @staticmethod def write(con, data): raise RuntimeError('Not Open') @staticmethod def open(con): con.new_state(OpenConnectionState) @staticmethod def close(con): raise RuntimeError('Already closed') class OpenConnectionState(ConnectionState): @staticmethod def read(con): print('reading') @staticmethod def write(con, data): print('writing') @staticmethod def open(con): raise RuntimeError('Already open') @staticmethod def close(con): con.new_state(ClosedConnectionState) class Connection: def __init__(self): self.new_state(ClosedConnectionState) # 通过一个方法给实例的一个私有变量赋值一个状态类。 def new_state(self, newstate): self._state = newstate 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) ''' 定义的类中的静态方法中的一个参数con就是Connection的实例,self 需要通过self,才能读取到状态。 ''' if __name__ == '__main__': c = Connection() print(c._state) c.open() c.read() c.close() c.read()
代码是比较优雅,但逻辑量是多了不少,后面还有更加骚的操作。
讨论
书中尽然通过修改实例的__class__修改实例所在的类,但实例换了所属于的类,它的实例方法都将进行改变。
class Connection: def __init__(self): self.new_state(ClosedConnectionState) # 通过一个方法给实例的一个私有变量赋值一个状态类。 def new_state(self, newstate): # 实例更改类 self.__class__ = newstate # 由于实例的类已经更改,这里的方法不会被用到 def read(self): raise NotImplementedError() def write(self, data): raise NotImplementedError() def open(self): raise NotImplementedError() def close(self): raise NotImplementedError() class ClosedConnectionState(Connection): def read(con): raise RuntimeError('Not Open') def write(con, data): raise RuntimeError('Not Open') def open(con): con.new_state(OpenConnectionState) def close(con): raise RuntimeError('Already closed') class OpenConnectionState(Connection): def read(con): print('reading') def write(con, data): print('writing') def open(con): raise RuntimeError('Already open') def close(con): con.new_state(ClosedConnectionState) if __name__ == '__main__': c = Connection() print(c) c.open() c.read() c.close() c.read()
我下面写一个小测试。
class A: def __init__(self): self.name = 'a' def a1(self): print('a1') def a2(self): print('a2') class B: def __init__(self): self.age = 'b' def b1(self): print('b1') def b2(self): print('b2') if __name__ == '__main__': a = A() print(vars(a)) print([i for i in dir(a) if not i.startswith('_')]) a.__class__ = B print(vars(a)) print([i for i in dir(a) if not i.startswith('_')])
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t19_3_t.py {'name': 'a'} ['a1', 'a2', 'name'] {'name': 'a'} ['b1', 'b2', 'name'] Process finished with exit code 0
经过这样的测试,所以在状态切换的时候,可以通过self.__class__的切换,让self拥有不同的类的实例方法。
8.20调用对象上的方法,方法名以字符串的形式输出
问题:
我们想调用对象上的莫个方法,现在这个方法名保存在字符串中,我们想过它来调用
解决方案:
一个是getattr,一个是operator.methodcaller
getattr相对比较熟悉,取出属性,是方法的话,就是可调用的属性,执行方法,填入参数。
operator.methodcaller('方法名',参数)(实例)
import math import operator class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f'Point({self.x!r}, {self.y!r})' def distance(self, x, y): return math.hypot(self.x - x , self.y - y) if __name__ == '__main__': p = Point(5, 6) print(p) # 读取方法 method = getattr(p, 'distance') print(method) # 执行方法 print(method(0, 0)) # 先传入方法名,然后是参数,最后调用传入实例。 other_method = operator.methodcaller('distance', 0, 0) print(other_method) print(other_method(p)) # 排序 points = [ Point(1, 2), Point(3, 4), Point(2, 4), Point(9, 2), Point(1, 4), Point(6, 4), ] points.sort(key=operator.methodcaller('distance', 0, 0)) print(points)
后面还有5个小结,我粗粗看了一下,感觉还是比较难的。
8.21 实现访问者模式
问题
我们需要编写代码来处理或遍历一个由许多不同类型的对象组成的复杂数据结构,每种类型的对象处理的方式都不同。列如遍历一个树结构,根据遇到的树节点的类型执行不同的操作。
解决方法
书中使用了一个案例,一个嵌套的数学运算程序
class Node: pass class UnaryOperator(Node): # 一元表达式 def __init__(self, operator): self.operator = operator class BinaryOperator(Node): # 两元表达式 def __init__(self, lift, right): self.lift = lift self.right = right class Add(BinaryOperator): ... class Sub(BinaryOperator): ... class Mul(BinaryOperator): ... class Div(BinaryOperator): ... class Negate(UnaryOperator): ... class Number(Node): def __init__(self, value): self.value = value # 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)
from t8_21_2_a import t4 class NodeVisitor: def visit(self, node): # 读取对象的类,组成名字 methname = 'visit_' + type(node).__name__ # 调用该方法 meth = getattr(self, methname, None) # 有方法执行,没方法报错 if meth is None: meth = self.generic_visit(node) return meth(node) def generic_visit(self, node): raise RuntimeError('NO {} method'.format('visit' + type(node).__name__)) class Evaluator(NodeVisitor): def visit_Number(self, node): return node.value def visit_Add(self, node): return self.visit(node.lift) + self.visit(node.right) def visit_Sub(self, node): return self.visit(node.lift) - self.visit(node.right) def visit_Mul(self, node): return self.visit(node.lift) * self.visit(node.right) def visit_Div(self, node): return self.visit(node.lift) / self.visit(node.right) def visit_Negate(self, node): return -node.operator if __name__ == '__main__': e = Evaluator() res = e.visit(t4) print(res) ''' 这是一个层层递归的操作,先判断对象的行为,进行操作,再把对象里面的元素拆分出来进行递归操作 递归我是最繁的,但确实很厉害,这样的操作很骚 '''
通过递归的方式,剥洋葱一样,一层一层剥开。书中还有一个堆栈机的代码,看书不是很理解,先上上来看看。
class Node: pass class UnaryOperator(Node): # 一元表达式 def __init__(self, operator): self.operator = operator class BinaryOperator(Node): # 两元表达式 def __init__(self, left, right): self.left = left self.right = right class Add(BinaryOperator): ... class Sub(BinaryOperator): ... class Mul(BinaryOperator): ... class Div(BinaryOperator): ... class Negate(UnaryOperator): ... class Number(Node): def __init__(self, value): self.value = value # 1 + 2 * ( -2 - 4 ) / 5 t0 = Number(2) t1 = Sub(Negate(t0), Number(4)) t2 = Mul(Number(2), t1) t3 = Div(t2, Number(5)) t4 = Add(Number(1), t3)
from t8_21_2_a import t4 class NodeVisitor: def visit(self, node): # 读取对象的类,组成名字 methname = 'visit_' + type(node).__name__ # 调用该方法 meth = getattr(self, methname, None) # 有方法执行,没方法报错 if meth is None: meth = self.generic_visit(node) return meth(node) def generic_visit(self, node): raise RuntimeError('NO {} method'.format('visit' + type(node).__name__)) class StackCode(NodeVisitor): def generic_code(self, node): # 对象入口 self.instructions = [] self.visit(node) return self.instructions def visit_Number(self, node): self.instructions.append(('PUSH', node.value)) def binop(self, node, instruction): # 进入递归,左边先处理 self.visit(node.left) self.visit(node.right) # 这个要最后出来了 self.instructions.append((instruction,)) def visit_Add(self, node): self.binop(node, 'ADD') def visit_Sub(self, node): self.binop(node, 'SUB') def visit_Div(self, node): self.binop(node, 'DIV') def visit_Mul(self, node): self.binop(node, 'MUL') def unaryop(self, node, instruction): self.visit(node.operator) self.instructions.append((instruction,)) def visit_Negate(self, node): self.unaryop(node, 'NEG') if __name__ == '__main__': s = StackCode() print(s.generic_code(t4)) ''' 这是一个层层递归的操作,先判断对象的行为,进行操作,再把对象里面的元素拆分出来进行递归操作 递归我是最繁的,但确实很厉害,这样的操作很骚 '''
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_8/t8_21_2_c.py [('PUSH', 1), ('PUSH', 2), ('PUSH', 2), ('NEG',), ('PUSH', 4), ('SUB',), ('MUL',), ('PUSH', 5), ('DIV',), ('ADD',)]
讨论
本节涵盖了两个核心思想。首先是设计策略,既把操作复杂数据结构的代码和数据结构本身进行了解耦。也就是说,本节中没有任何一个Node类的实现有对数据进行操作。
相反,所有对数据的处理都放在特定的NodeVisito类中实现。这种隔离使得代码变得非常通用。
第二核心思想在于对访问者类本身的实现。通过一些小技巧再类中定义各种方法,讲层层包裹中的各种情况都囊括了,如果没有匹配就报异常。
8.22实现非递归的访问者模式
本章节是我看到现在最累的一个章节,用了到生成器函数,并通过列表对数据进行压栈与弹栈,看了一天了,其实还是有一些不明白,准备放弃中。
问题:
我们使用访问者模式来遍历一个深度嵌套的树结构,但由于超出了Python的递归限制而奔溃。我们想要去掉递归,但依旧保持访问者模式的编程风格。
解决方案:
import types from t8_22_2_a import t4, Node class NodeVisitor: def visit(self, node): stack = [ node ] last_result = None while stack: try: # 便签一个对象用来判断用。 last = stack[-1] if isinstance(last, types.GeneratorType): stack.append(last.send(last_result)) last_result = None # 非生成器的运行模式 elif isinstance(last, Node): # 进入递归模式,压入弹出的对象的结果。 stack.append(self._visit(stack.pop())) # 这个只可能是数值的时候运行 else: last_result = stack.pop() # 当一个生成器执行到了最后,清楚了这个生成器 except StopIteration: stack.pop() return last_result def _visit(self, node): # 读取对象的类,组成名字 methname = 'visit_' + type(node).__name__ # 调用该方法 meth = getattr(self, methname, None) # 有方法执行,没方法报错 if meth is None: meth = self.generic_visit(node) # 返回生成器函数,或者函数。 return meth(node) def generic_visit(self, node): raise RuntimeError('NO {} method'.format('visit' + type(node).__name__)) class Evaluator(NodeVisitor): def visit_Number(self, node): return node.value def visit_Add(self, node): yield (yield node.left) + (yield node.right) def visit_Sub(self, node): yield (yield node.left) - (yield node.right) def visit_Mul(self, node): yield (yield node.left) * (yield node.right) def visit_Div(self, node): yield (yield node.left) / (yield node.right) def visit_Negate(self, node): yield -(yield node.operator) if __name__ == '__main__': e = Evaluator() res = e.visit(t4) print(res) # res1 = e.visit(a) # print(res1) ''' 这是一个层层递归的操作,先判断对象的行为,进行操作,再把对象里面的元素拆分出来进行递归操作 递归我是最繁的,但确实很厉害,这样的操作很骚 '''
讨论:
这里代码展示了如何利用生成器和协程来控制程序的执行流。
首先,再有关遍历树结构的问题中,为了避免使用递归,常见的策略就是利用栈或者列队来实现算法。
第二个要点在于生成器中yield语句的行为。
最后一个需要考虑的是如何传递结果。
其实对于这种设计我真的看的很晕,虽然感觉很高大上,但就我现在的水平,确实让我很晕,有机会回头再看。
8.23 在环装数据结构中管理内存
问题
我们的程序创建了环装的数据结构,但是再内存管理上遇到了麻烦。
解决方案
weakref.ref弱引用,这个函数可以引用变量,但不影响变量的引用数。
import weakref class Node: def __init__(self, value): self.value = value self._parent = Node self.children = [] def __repr__(self): return 'Node({!r})'.format(self.value) @property def parent(self): return self._parent if self._parent is Node else self._parent() @parent.setter def parent(self, node): # 弱引用 self._parent = weakref.ref(node) def add_child(self, child): self.children.append(child) # 父节点给字属性赋值 child.parent = self
>>> root = Node('parent') >>> cl = Node('child') >>> root.add_child(c1) >>> c1.parent Node('parent') >>> del ro root round( >>> del root >>> c1.parent Node('parent') >>> c1.parent Node('parent') >>> gc.collect() 0 >>> c1.parent >>>
这是一个我弱引用的测试,在测试中,经常发现,变量已经删除,但弱引用里面的对象还存在,并不是马上释放弱引用里面的对象。
讨论
对于一般的环状数据,可以通过gc.collect(),手动清楚,但一般也没必要,因为Python也会定期清楚环状数据。
class Data: def __del__(self): print('Data.__del__') class Node: def __init__(self): self.value = Data() self._parent = Node self.children = [] def add_child(self, child): self.children.append(child) # 父节点给字属性赋值,定制环状数据结构 child.parent = self
shijianzhongdeMacBook-Pro:chapter_8 shijianzhong$ python3 -i t8_23_3.py >>> a = Data() >>> del a Data.__del__ >>> a = Node() >>> del a Data.__del__ >>> a = Node() >>> a.add_child(Node()) >>> del a >>> import gc >>> gc.collect() # 删除了两个实例,所以输出两次 Data.__del__ Data.__del__ 8
8.24 让类支持比较操作
问题
我们想使用标准的比较操作符(>,<)等在类实例之间进行比较,但是又不想编写大量的特殊方法
解决问题
用了一个functools里面的total_ordering装饰器
一般我们要用大于大于什么的,需要在里面定义__eq__,__lt__,__le__,__ge__,__gt__等特殊方法。
用了装饰器方便多了。
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 __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 if __name__ == '__main__': h1 = House('h1', 'Cape') h1.add_room(Room('M_room', 12, 30)) h2 = House('h2', 'Ranch') h2.add_room(Room('Kitchen',30 ,12)) print(h1 == h2) print(h2 > h1)
讨论
其实这个装饰器就帮你写了这么几句话
__le__ = lambda self, other: self < other or self == other
__gt__ = lambda self, other: not(self < other or self == other)
__ge__ .....
__ne__.....
8.25创建缓存实例
问题
当创建类实例时,我们想返回一个缓存引用,让其指向上一个用同样参数(如果有的话)创建出的类实例。
解决方法
其实一个字典就可以解决的,但为了缓存引用,就用到了weakref.WeakValueDictionary
class Spam: def __init__(self, name): self.name = name import weakref _spam_cache = weakref.WeakValueDictionary() def get_spam(name): if name not in _spam_cache: s = Spam(name) # 弱引用字典赋值,必须外部变量接收 _spam_cache[name] = s else: s = _spam_cache[name] return s
>>> a = get_spam('foo') >>> b = get_spam('bar') >>> list(_spam_cache) ['foo', 'bar'] >>> del a >>> list(_spam_cache) ['bar'] >>> del b >>> list(_spam_cache) []
讨论:
创建一个工厂函数,方便的创建弱引用类,缓存。
class Spam: def __init__(self): raise RuntimeError("Can't instantiate directly") @classmethod def _new(cls, name): self = cls.__new__(cls) self.name = name return self import weakref class CacheSpamManager: def __init__(self): self.cache = weakref.WeakValueDictionary() def get_spam(self, name): if name not in self.cache: s = Spam._new(name) # 弱引用字典赋值,必须外部变量接收 self.cache[name] = s else: s = self.cache[name] return s @property def my_caches(self): return list(self.cache) if __name__ == '__main__': cache = CacheSpamManager() sd = cache.get_spam('sidian') lei = cache.get_spam('lei') leige = cache.get_spam('lei') print(sd is lei) print(leige is lei) del sd print(cache.my_caches)
False True ['lei']
整个类这一章节结束了,除了那个遍历的树结构通过yield与列表进行弹栈与压栈比较难,另外的相对还是比较容易理解的。
下一个章节,将是元编程,这是一个难度不小的骨头,希望能够再10天能够啃完。