结于2021-08-23;
OREILY的书籍,可读性很强,入门类,而且这本书很厚;
本章继续类机制:运算符重载;
运算符重载:在类方法中拦截内置的操作;当类的实例出现在内置操作中,Python自动调用你的方法;
__init__ # 构造函数
__del__ # 析构函数
__add__ # +
__or__ # | 、 or
__repr__, __str__ # 打印 转换
__call__ # 函数调用
__getattr__ # 点号运算
__setattr__ # 属性赋值
__delattr__ # 属性删除
__getattribute__ # 属性获取
__getitem__ # 索引运算(包括切片)
__setitem__ # 索引赋值(包括切片)
__delitem__ # 索引删除(包括切片)
__len__ # len(X)
__bool__ # bool(X)
__contains__ # 成员关系测试 item in X
__index__ # 整数值 如hex(X)、O[X]
__enter__, __exit__ # 环境管理器 with obj as var:
__get__, __set__, __delete__ # 描述符属性 获取 赋值和删除
...
__lt__ # <>
__gt__ # >
__eq__ # ==
__ne__ # !=
对内置对象所能做的事,几乎都有相应的特殊名称的重载方法;多数重载方法只用在需要对象行为表现得像内置类型一样的高级程序中;
# number.py
class Number:
def __init__(self,start):
self.data = start
def __sub__(self, other):
return Number(self.data - other)
from number import Number
X = Number(5)
Y = X - 2
Y.data # 3
#
class Indexer:
def __getitem__(self, index):
return index ** 2
X = Indexer()
X[2] # 4
# 分片边界绑定到一个分片对象
L[2::1]
L[slice(2, None, 1)] # 二者等价
# __getitem__ 也是针对分片(一个分片对象)的调用
def __setitem__(self, index, value):
self.data[index] = value
__getitem__
也可以是Python中一种重载迭代的方式;for循环每次循环都会调用类的该方法,并持续匹配更高的偏移值;
任何会响应索引运算的内置或用户定义的对象,同样会响应迭代;任何支持for循环的类也会自动支持Python所有迭代环境(如:in测试、列表解析、内置函数map、列表元组转换);
虽然使用__getitem__
对迭代也有效,但是__iter__
才是优先级更高的响应方法;前者是对对象进行索引运算,后者则是在支持迭代协议;先找__iter__
,再找__getitem__
;
内置函数iter会尝试寻找__iter__
来实现迭代环境,返回的是一个迭代器对象;之后重复调用迭代对象的next方法(next(I)
与I.__next__()
是相同的);
class Squares:
def __inite__(self, start, stop):
self.value = start - 1
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value ** 2
for i in Squares(1,5): # 这个对象循环一次 之后 就变为空了,每次循环 都需新建一个迭代器对象
print(i, end='')
# 1 4 9 16 25
有时
__iter__
会比___getitem_
难用,迭代器是用来迭代的,不是随机的索引运算;
上边的例子,也可以用生成器函数
编写:
def gsquares(start, stop):
for i in range(start, stop):
yield i ** 2
for i in gsquares(1, 5):
print(i)
单迭代器
多个活跃迭代器
要支持多个迭代器的效果,__iter__
只需迭代器定义新的状态对象,而不是返回self;
# 仔细看这个示例
class SkipIterator:
def __init__(self, wrapped):
self.wrapped = wrapped
self.offset = 0
def __next__(self):
if self.offset >= len(self.wrapped)
raise StopIteration
else:
item = self.wraped[self.offset]
self.offset += 2
return item
class SkipObject:
def __init__(self, wrapped):
self.wrapped = wrapped
def __iter__(self):
return SkipIterator(self.wrapped)
if __name__ == '__main__':
alpha = 'abc'
skipper = SkipObject(alpha)
I = iter(skipper)
print(next(I)) # 0
for x in skipper:
for y in skipper:
print(x+y)
# aa ac ca cc
迭代器是一次产生一个值,而不是把所有值生成后的列表载入内存;节省了实际空间;
在迭代领域,类通常把in
成员关系运算符实现为一个迭代
__iter__
或__getitem__
方法;__contains__
;__contains__
优先于__iter__
__iter__
优先于__getitem__
def __contains__(self, x):
return x in self.data
__getitem__
方法往往更为通用,它拦截了迭代、显式索引、分片(使用分片对象);而在更加现实的场景下__iter__
方法可能更容易被编写,因为它不必管理整数索引;__contains__
更多的是考虑最为一种特殊情况优化成员关系;
拦截属性点号运算:
def __getattr__(self, attrname):
if attrname == 'age':
return 40
else:
raise AttributeError, attrname
对于类不知道如何处理的属性,这个方法会引发内置的AttributeError异常;
__setattr__
方法:
self.attr = value
=> self.__setattr__('attr', value)
;self.__dict__['name'] = x
,而不是self.name = x
;__getattribute__
方法:
class PrivateExc(Exception):pass
# 为私有属性 赋值会触发异常
class Privacy:
def __setattr__(self, attrname, value):
if attrname in self.privates:
raise PrivateExc(attrname, self)
else:
self.__dict__[attrname] = value
class Test2(Privacy):
privates = ['name', 'pay']
def __init__(self):
self.__dict__['name'] = 'Tom'
__str__
方法,在print顶层打印和调用str内置函数时,会优先使用它;此外的其他需要格式化字符串的场景,均使用__repr__
方法;__str__
方法,所有场景均使用__repr__
方法;为了定制所有环境,请编写
__repr__
,而不是__str__
;
仅当+
右侧对象是实例对象,而左侧对象不是时,会调用__radd__
;结合__add__
提供了运算符的交换性;
class Computer:
def __init__(self, val):
self.val = val
def __add__(self, other):
if isinstance(other, Computer):
other = other.val
return Computer(self.val + other)
def __radd__(self, other):
return Computer(other + self.val)
x = Computer(88)
y = Computer(99)
x + 2 # __add__ 结果的是一个新实例
1 + y # __radd__ 结果的是一个新实例
x + y # __add__ 结果的是一个新实例
为实现+=
原处扩展相加,编写__iadd__
和__add__
;前者空缺会使用后者;
def __iadd__(self, other):
self.val += other
return self
每个二元运算符都有类似的右侧和原处重载方法,如
__mul__ __rmul__ __imul__
;
把实例当做函数一样调用,会使用__call__
方法;
class Callee:
def __call__(self, *pargs, **kargs):
pass
C = Callee()
C(1,2,3)
C(*[1,2], **dict(c=3, d=4))
在需要编写遵循函数调用对象,同时又能保留状态信息时,__call__
很有用;比如编写回调函数时,你就可以编写一个这样的实例对象来替代函数进行传递;
Python中偶尔还会用两种其他方式,来把信息和对调函数联系起来:
(lambda color='red': 'turn' + color)
instance.method
到callback参数;关于
__bool__
和__len__
需要注意一点,在进行布尔测试时,如果没有实现__bool__
,就会根据__len__
方法的返回值是否为0,进行判断(非零为真);如果两个方法都没有定义,那么对象将毫无疑义的被看做真;
__init__
__del__
,会自动执行但Python中的析构函数并不常用:
循环引用会阻止垃圾回收,一个可选的循环检测器,是默认可用的,但是只有没有
__del__
时才可用;
在C实现的Python中,不需要在析构函数中关闭由实例打开的文件,他们在回收时会被自动关闭;
后续还会学习其他新的重载方法:__enter__ __exit__ __get__ __set__
注意:有些OOP语言把多态定义成基于参数类型标记的重载函数,但在Python中没有类型声明,因此行不通;多个同名方法仅最后一个会生效;
Python中多态是基于对象接口的,而不是类型;
class Employee:
def __init__(self,name):
self.name = name
klass = Employee
obj = klass(klass.__name__)
相比继承,组合不是集合的成员关系,而是组件,是整体的组成部分;
class PizzaShop:
# 容器 控制器
def __init__(self):
# 嵌入其他对象
self.server = Server('')
self.chef = PizzaRobot('')
self.oven = Oven()
流处理器基于类组合的实现:
def processor(reader, converter, writer):
while 1:
data = reader.read()
if not data:break
data = converter(data)
writer.write(data)
# 类组合
class Processor:
def __init__(self, reader, writer):
self.reader = reader
self.writer = writer
def process(self):
while 1:
data = self.reader.readline()
if not data: break
data = self.converter(data)
self.writer.write(data)
def converter(self, data):
assert False, 'converter must bu defined'
# 该转换器 需子类填充
class Uppercase(Processor):
def converter(self,data):
return data.ipper()
import sys
obj = Uppercase(open('spam.txt', sys.stdout))
obj.process()
在较大型系统中,继承和组合往往很常用,而且是互补的;
import pickle
object = someClass()
file = open(filename, 'wb')
pickle.dump(object, file) # 把内存的对象转换为序列化的字节流(可保存为文件 也可以通过网络请求发出去)
import pickle
file = open(filename, 'rb')
object = pickle.load(file)
import shelve
object = someClass()
dbase = shelve.open('filename')
dbase['key'] = object
import shelve
dbase = shelve.open('filename')
object = dbase['key']
通常指控制器对象内嵌其他对象,而把运算请求传给那些对象;控制器负责管理;
在Python中,委托通常以__getattr__
钩子方法实现,因为这个方法会拦截对不存在属性的获取;
# 包装者类
class wrapper:
def __init__(self, object):
# 被包装对象
self.wrapped = object
def __getattr__(self, attrname):
return getattr(self.wrapped, attrname)
x = wrapper([1,2,3])
x.append(4)
压缩的变量名,有时会被误认为私有,所以也称之为伪私有;它其实是一种把类所创建的变量名局部化的方式而已主要是为避免实例内命名空间冲突,并不能阻止外部对它的读取;
伪私有变量名是可选的功能,更常用的则是用一个下划线编写内部名称,虽然这只是一个非正式的管理,让你知道这是一个不应该修改的名称;
变量名压缩的工作方式:
Spam类的__X => _Spam__X
;伪私有属性使用场景:
class C1:
def meth1(self):self.X = 88
def meth2(self):print(self.X)
class C2:
def metha(self):self.X = 99
def methb(self):print(self.X)
class C3(C1, C2):pass
I = C3()
I.meth1()
I.metha()
I.meth2() # 99
I.methb() # 99
# 使用伪私有属性:避免了潜在的变量名冲突
class C1:
def meth1(self):self.__X = 88
def meth2(self):print(self.__X)
class C2:
def metha(self):self.__X = 99
def methb(self):print(self.__X)
class C3(C1, C2):pass
I = C3()
I.meth1()
I.metha()
print(I.__dict__) # {'_C2__X':99, '_C1__X':88}
I.meth2() # 88
I.methb() # 99
依然可以使用扩张后的变量名,如
I._C1__X = 77
;
如果一个方法倾向于只在一个可能混合到其他类的
类中使用,在前面使用双下划线,以确保该方法不会受到树中的其他名称的干扰,特别是在多继承的环境中;
方法也是对象,并且可以用与其他对象大部分相同的方式来广泛地使用;
无绑定方法对象 VS 绑定方法对象:
class Spam:
def doit(self, message):
print(message)
def selfless(arg1, arg2):
return arg1 + arg2
object1 = Spam()
object1.doit('he')
x = object1.doit # x 是 绑定方法对象
x('he')
t = Spam.doit # x 是 无绑定方法对象
t(object1, 'he') # 注意:这只是一个普通的函数调用
Spam.selfless(3, 4)
绑定方法可以作为一个通用对象处理;
class Number:
def __init__(self, base):
self.base = base
def double(self):
return self.base * 2
x = Number(2)
y = Number(3)
for act in [x.double, y.double]:
# double分别绑定了 对象x和y
act()
# 具有自己的内省信息
bound = x.double
bound.__self__ # 实例对象
bound.__self__.base # 实例对象的base属性
bound.__func__ # 函数对象
绑定方法 作为参数传递,当其调用时,self会引用原始实例对象,也就是绑定方法的对象;在作为回调函数调用时,这个方法就可以读取self实例的属性;如果不用绑定对象方法,而是只用一个简单的函数作为回调参数传入,其他状态信息往往需要通过全局变量保存;
常见的可调用对象:简单函数 lambda 实例继承__call__
绑定实例方法,类也算;
多重继承:类和其他实例继承了列出的所有超类的变量名;
搜索属性,会从左到右搜索类首行中的超类;Python3中,属性搜索处理沿着树层级、以更加广度优先的方式进行;
通常:多重继承是建模属于一个集合以上的对象的好方法;
class ListInstance:
def __str__(self):
return '' % (
self.__class__.__name__,
id(self), # 内存地址
self.__attrnames()
)
# 通过扩展属性名包括类名,从而吧这样的名称本地化到包含的类中(对类属性和实例属性均如此)
def __attrnames(self):
result = ''
for attr in sorted(self.__dict__):
result += '\tname %s=%s\n' % (attr, self.__dict__[attr])
return result
通过把ListInstance添加到一个类头部的超类列表中(混合进去),就可以让类在继承自己超类的同时,获得__str__
;
__dict__
字典(只有实例属性);def __attrnames(self):
result = ''
for attr in dir(self):
if attr[:2] == '__' and attr[-2:] == '__':
result += '\tname %s=<>\n' % attr
else:
# getattr使用了继承搜索协议
result += '\tname %s=%s\n' % (attr, getattr(self,attr))
return result
# 还会打印从隐式超类object 继承的很多属性
注意:这里还会显示继承的方法,必须使用
__str__
,如果使用__repr__
,这段代码将会循环;因为__repr__
中试图显示
一个方法的值,会再次触发__repr__
;
遍历整个类树,显示附加到每个对象上的属性:从一个实例的__class__
到其类,然后递归地从类的__bases__
到其所有超类,一路扫描对象的__dict__
;
class ListTree:
def __str__(self):
self.__visited = {}
return '' .format(
# 实例对象 类名
self.__class__.__name__
# 实例对象内存地址
id(self),
# 对象属性
self.__attrname(self, 0),
# 类属性
self.__listclass(self.__class__, 4)
)
def __listclass(self, aClass, indent):
dots = '.' * indent
if aClass in self.__visited:
return '\n{0}\n' .format(
dots,
aClass.__name__,
id(aClass)
)
else:
self.__visited[aClass] = True # 类对象 做字典键 避免两次列出同样的类对象
genabove = (self.__listclass(c, indent + 4) for c in aClass.__bases__) # 超类打印的信息字符串
return '\n{0}\n' .format(
dots,
aClass.__name__,
id(aClass),
self.__attrname(aClass, indent),
''.join(genabove),
dots
)
def __attrname(self, obj, indent):
spaces = ' '*(indent + 4)
result = ''
for attr in sorted(obj.__dict__):
if attr.startwith('__') and attr.endwith('__'):
result += spaces +'{0}=<>\n'.format(attr)
else:
# getattr使用了继承搜索协议
result += spaces +'{0}={1}\n'.format(attr, getattr(obj,attr))
return result
class MyButton(ListTree, Button): pass
print(MyButton(text='spam'))
注意:这里打印属性,扫描的是实例字典,不能直接支持存储在
slot
中的属性;
# 把类传给会产生任意种类对象的函数
def fatory(aClass, *args, **kargs):
return aClass(*args, **kargs)
工厂可以将代码和动态配置对象的构造细节隔离开;
与设计相关的高级话题:
这是OOP讨论的最后一部分:
以支持更另类的数据结构:
type(I)
内置函数返回一个实例对应的类,通常是和I.__class__
相同的;type类
的实例,type对象产生类作为自己的实例;所有的类都继承自object;Python中基于委托的类:__getattr__
与内置函数
__getattr__
和__getattribute__
方法不再针对内置运算的隐式属性获取而运行;即不再针对__X__
等运算符重载方法而调用;例如,打印所使用的__str__
方法,不会调用__getattr__
;__X__
这样的方法必须在包装类中重定义,要么手动、要么使用工具,或者在超类中重新定义;type([1,2,3]) #
type(list) #
list.__class__ #
class C(object):pass
I = C()
type(I) #
I.__class__ #
c1, c2 = C(), C()
type(c1) == type(c2) # True
type(C) #
isinstance(C, object) # true
类就是类型,一个实例的类型就是该实例的类;
每个类都由一个元类生成,元类是这样的一个类,要么是type自身,要么是从type扩展的子类;
实际上,类型自身派生自object,并且object派生自type,二者是不同对象:一个循环关系覆盖了对象模型,因而:类型是生成类的类;
# 所有的类对象的类都是type
type(type) #
type(object) #
# 所有类都派生自object
isinstance(type, object) # True
# object 派生自type
isinstance(object, type) # True
# 二者是不同对象
type is object # False
多重继承树钻石模式:
# A.attr .meth
# B C.meth
# D
# 有上述菱形继承结构
x = D()
x.attr # 检索方式为 D B C A
# 如果想要选择meth方法的搜索路径,可以在D类中明确选择
# 选择C的meth
class D(B,C):
meth = C.meth
y = D()
y.meth # D C
# 选择B的meth
class D(B,C):
meth = B.meth
z = D()
z.meth # D B A
如果你想要左侧超类的一部分以及右侧超类的一部分,可能就需要在子类中明确使用赋值语句,告诉Python要选择哪个同名属性;
slots实例:
__slots__
类属性;__slots__
;实际上,有些带有slots的实例根本没有__dict__
实行字典,某些情况下,两种属性源都需要查询以确保完整性;
getattr setattr dir等内置函数,相对slots和__dict__
的属性,是比较中立的工具;
比如slots
中的属性,可以是使用通用工具通过名称进行访问和设置:
class C:
__slots__ = ['a','b']
X = C()
X.a = 1
X.a
X.__dict__ # slots中没有这个属性,因而会报错
getattr(X, 'a')
setattr(X, 'b', 2)
'a' in dir(X) # True
'b' in dir(X) # True
上边这里例子中,我们没有在slots中定义一个属性命名空间字典__dict__
,因而不可能给不再slots列表中名称的赋值为实例的属性;
解决方法是让slots包含__dict__
,从而考虑到一个属性空间字典的需求:
class D:
__slots__ = ['a', 'b', '__dict__']
c = 3 # 类属性是可以正常工作的
def __init__(self):
self.d = 4
x = D()
x.d # 4
x.__dict__ # {'d': 4}
x.a # 会报错,因为此时a属性 还没有被赋值
x.a = 1
getattr(X, 'a'),getattr(X, 'c'),getattr(X, 'd')
# (1, 3, 4)
想要通用地列出所有实例属性的代码,需要考虑两种存储形式:
# 实例属性
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
print(attr, '=>', getattr(X, attr))
上述说明了一个实例继承其最低slots属性(实例的类,不包括继承树中的其他超类)中的slot名称:
class E:
__slots__ = ['c', 'd']
class D(E):
__slots__ = ['c', 'd']
X = D()
X.a = 1
X.b = 2
X.c = 3
E.__slots__ # ['c', 'd']
D.__slots__ # ['a', '__dict__']
X.__slots__ # ['a', '__dict__']
X.__dict__ # {'b': 2}
# 打印实例属性 不包括超类的slots
for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
print(attr, '=>', getattr(X, attr))
# b => 2
# a => 1
# __dict__ => {'b': 2}
dir(X) # 几乎所有属性,包括所有的slot names
property机制:
特性是一种对象,赋值给类属性名称;
name = property(...)
,对类属性的读取,回传给property的一个读取方法;__getattr__ __setattr__
的替代做法;class newprops(object):
def getage(self):
return 40
def setage(self, value):
self._age = value
age = property(getage, setage, None, None) # get set del docs
x = newprops()
x.age # 40
x.age = 42
x._age # 42
x.job = 'work'
x.job # 'work'
相比于特性,之前应用过的__getattr__
和__setattr__
在某些应用依然更为动态和通用;
class classic:
def __getattr__(self, name):
if name == 'age':
return 40
else:
raise AttibuteError
def __setattr__(self, name, value):
if name == 'age':
self.__dict__['_age'] = value
else:
self.__dict__[name] = value
x = classic()
x.age # 40
x.age = 42
x._age # 42
x.job = 'work'
x.job # 'work'
后续还将看到 使用函数装饰器语法来编写特性是可能的;
__getattribute__
可以让类拦截所有属性的引用;
Python支持 属性描述符:
__get__ __set__
方法的类;元类 是子类化了type对象,并且拦截类创建调用的类;同时还为管理和扩展类对象 提供了定义良好的钩子;(后续详细介绍)
无实例参数
函数类似;一个类参数
而不是一个实例参数;在Python3中,无self的方法,将被看作是简单函数,通过类调用有效,从实例调用无效;
class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances = Spam.numInstances + 1
# 无self的方法,具有静态方法特性;该方法仅能通过类名调用
def printNumInstances():
print(Spam.numInstances)
这种方法也不理想:
上边这种方式,我们实际是在通过类 调用类的函数属性,它通过实例去调用是无效的;但如果我们想要使用实例调用也有效,就要使用静态方法
和类方法
:
class Methods:
# 实例方法
def imeth(self, x):
pass
# 无self的简单函数,显式类名称调用
def smeth(x):
pass
# 无self的简单函数,显式类名称调用
def cmeth(cls, x):
pass
# 静态方法
smeth = staticmethod(smeth)
# 类方法
cmeth = classmethod(cmeth)
obj = Methods()
# 实例方法调用方式
obj.imeth(1)
Methods.imeth(obj,2)
# 静态方法 可以通过类和实例调用
Methods.smeth(3)
obj.smeth(4)
# 类方法:自动把类(非实例)传入第一个参数,无论调用者是类还是实例
Methods.cmeth(5)
obj.cmeth(6)
注意:
由于类方法总是接收一个实例树中的最低类:
class Spam:
numInstances = 0
def count(cls):
cls.numInstances += 1
def __init__(self):
self.count()
count = classmethod(count)
class Sub(Spam):
numInstances = 0
def __init__(self):
Spam.__init__(self)
class Other(Spam):
numInstances = 0
x = Spam()
y1 = Sub()
y2 = Sub()
z1 = Other()
z2 = Other()
z3 = Other()
x.numInstances, y1.numInstances, z1.numInstances # (1,2,3)
Spam.numInstances, Sub.numInstances, Other.numInstances # (1,2,3)
装饰器语法:把一个函数应用于另一个函数的一种方法,可以让静态方法和类方法的设计更简单;
函数装饰器:提供一种方式,替函数明确了特定的运算模式,也就是包了一层逻辑实现;
Python提供了一些内置函数装饰器,来做一些运算,如可以标识静态方法;还可以编写任意的装饰器;(用户自定义的也通常写成类,原始函数和其他数据作为状态信息)
语法上:
@
符号及紧跟着的元函数组成;# 对前面的静态方法 使用函数装饰器改写
class C:
@staticmethod:
def meth():
pass
结果:调用方式时,会触发staticmethod
装饰器效果,调用增加的一层逻辑,原始函数会在额外的逻辑层间接执行;(classmethod
装饰器也有同样的效果)
staticmethod仍然是一个内置函数;它可以用于装饰器语法中,只是因为它接受一个函数并返回一个可调用对象;事实上,任何这样的函数都能这样用;
Python提供的很多内置函数,都可用作装饰器,我们也可以自己定制装饰器;(后续会详细介绍)
来看一个简单的装饰器例子:
# 这个例子展示了:装饰器可用于包裹携带任意数目参数的任何信息
class tracer:
def __init__(self, func):
self.calls = 0
self.func = func
def __call__(self, *args):
self.calls += 1
print(self.calls, self.func.__name__)
self.func(*args) # *语法 解包参数
@tracer
def spam(a,b,c):
print(a,b,c)
spam(1,2,3) # 1 spam \n 1 2 3
# 结果:在原始函数上 新增了一层逻辑
上边的例子没有返回装饰器函数结果,也没有处理关键参数,也不能装饰类方法函数;
类装饰器 类似于 函数装饰器;就像下边这样:
# 最终映射的逻辑是这样的
def decorator(aClass):
pass
class C:
pass
C = decorator(C)
# 对应的装饰器语法 如下
def decorator(aClass):
@decorator
class C:
pass
# 一个例子
def count(aClass):
aClass.numInstances = 0
return aClass
@count
class Spam:
pass
元类:
class Meta(type):
def __new__(meta, classname, supers, classdict):
...
class C(metaclass=Meta):
...
元类通常重新定义type类的__new__
或__init__
方法,以实现对一个新的对象的创建和初始化的控制;
类装饰器 和 元类:都可以用来扩展一个类 或 返回一个任意的对象来替代它,几乎拥有无限可能、基于类的可能性的一种协议;
《Python 学习手册(3)》有关于抽象类的描述,就使用了元类;
在class语句外,修改类属性,会修改每个实例对象从类继承的该属性;
由于类属性由所有实例共享,所以如果一个类属性引用一个可变对象,那么从任何实例来原处修改该对象都会like影响到所有实例;
__getattr__
与内置函数参考【第31章 新式类变化】一节;
不要“过渡包装”,把程序代码包裹很多层,不易理解,尽量避免代码过于抽象,除非有不得不这样做的理由;
后续: