5-6-类的设计
1. Python 和 OOP
Python 和 OOP 实现可以概括为三个概念:
- **继承。**基于 Python 中的属性查找(在 X.name 表达式中)。
- **多态。**在 X.method 方法中,method 的意义取决于 X 的类型(类)。
- **封装。**方法和运算符实现行为,数据隐藏默认是一种惯例。
2. OOP 和 继承:“Is-a” 关系
以制作比萨的机器人为例。“是一个”(is-a)关系:机器人是一个主厨,主厨是一个员工:
class Employee:
def __init__(self, name, salary=0):
self.name = name
self.salary = salary
def giveRaise(self, percent):
self.salary = self.salary + (self.salary * percent)
def work(self):
print(self.name, "does stuff")
def __repr__(self):
return "<Employee: name=%s, salary=%s>" % (self.name, self.salary)
class Chef(Employee):
def __init__(self, name):
Employee.__init__(self, name, 50000)
def work(self):
print(self.name, "makes food")
class Server(Employee):
def __init__(self, name):
Employee.__init__(self, name, 40000)
def work(self):
print(self.name, "interfaces with customer")
class PizzaRobot(Chef):
def __init__(self, name):
Chef.__init__(self, name)
def work(self):
print(self.name, "makes pizza")
if __name__ == "__main__":
bob = PizzaRobot('bob')
print(bob)
bob.work()
bob.giveRaise(0.2)
print(bob);print()
for klass in Employee, Chef, Server, PizzaRobot:
obj = klass(klass.__name__)
obj.work()
<Employee: name=bob, salary=50000>
bob makes pizza
<Employee: name=bob, salary=60000.0>
Employee does stuff
Chef makes food
Server interfaces with customer
PizzaRobot makes pizza
3. OOP 和 组合:“Has-a” 关系
组合类一般都提供自己的接口,并通过内嵌的对象来实现接口。比萨店是一个组合对象,有烤炉,也有服务生和主厨。当顾客来店下单时,店里的组件就会开始行动:
class Customer:
def __init__(self, name):
self.name = name
def order(self, server):
print(self.name, "orders from", server)
def pay(self, server):
print(self.name, "pays for item to", server)
class Oven:
def bake(self):
print("oven bakes")
class PizzaShop:
def __init__(self):
self.server = Server('Pat') # 嵌入其他对象
self.chef = PizzaRobot('Bob')
self.oven = Oven()
def order(self, name):
customer = Customer(name) # 激活其他对象
customer.order(self.server)
self.chef.work()
self.oven.bake()
customer.pay(self.server)
if __name__ == "__main__":
scene = PizzaShop() # 进行组合
scene.order('Homer')
print('...')
scene.order('Shaggy')
Homer orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Homer pays for item to <Employee: name=Pat, salary=40000>
...
Shaggy orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Shaggy pays for item to <Employee: name=Pat, salary=40000>
PizzaShop 类是容器和控制器,其构造函数会创建上一节所编写的员工类实例并将其嵌入。
4. OOP 和委托:“包装”代理对象
委托通常就是指控制器对象内嵌其他对象,而把运算请求传给那些对象。控制器负责管理工作。在 Python 中,委托通常是以 __getattr__
hook 方法实现的,因为这个方法会拦截对不存在属性的读取,“包装”类可以使用 __getattr__
把任意读取转发给被包装的对象。包装类包有被包装对象的接口,而且自己也可以增加其他运算。
class Wrapper:
def __init__(self, object):
self.wrapped = object # 保存对象
def __getattr__(self, attrname):
print('Trace: ' + attrname) # 追踪获取
return getattr(self.wrapped, attrname) # 代理获取
__getattr__
会获得属性名称字符串,利用 getattr 内置函数,以变量名字符串从包裹对象取出属性:getattr(X, N) 就像是 X.N,只不过 N 是表达式,可在运行时计算出字符串,而不是变量。
x = Wrapper([1, 2, 3]) # 包装一个列表
x.append(4) # 委托一个列表方法
Trace: append
x.wrapped
[1, 2, 3, 4]
x = Wrapper({'a': 1, 'b': 2})
list(x.keys())
Trace: keys
['a', 'b']
在这里 wrapper 类只是在每个属性读取时打印跟踪消息,并把属性请求委托给嵌入的 wrapped 对象。
5. 类的伪私有属性
Python 支持变量名压缩(mangling)的概念,让类内某些变量局部化。压缩后的变量名有时会被误认为是“私有属性”,但这只是一种把类所创建的变量名局部化的方式而已。这种功能主要是为了避免实例内的命名空间的冲突,而不是限制变量名的读取。
Python 程序员用一个单个的下划线来编写内部名称,这只是一个非正式的惯例。
5.1 变量名压缩概览
class 语句内开头有两个下划线,但结尾没有两个下划线的变量名,会自动扩张,从而包含了所在类的名称。例如,Spam 类内 __X
这样的变量名会自动变成 _Spam__X
:原始的变量名会在头部加入一个下划线,然后是所在类名称。因为修改后的变量名包含了所在类的名称,不会和同一层次中其他类所创建的类似变量名相冲突。
变量名压缩只发生在 class 语句内,而且只针对开头有两个下划线的变量名。
5.2 为什么使用伪私有属性
伪私有属性功能是为了缓和与实例属性储存方式有关的问题。在 Python 的类方法内,每当方法赋值 self 的属性时(例如,self.attr = value),就会在该实例内修改或创建该属性(继承搜索只发生在引用时,而不是赋值时)。如果在这个层次中有多个类赋值相同的属性,有可能发生冲突。
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.meth2()
88
I.metha()
I.methb()
99
self.X 所得到的值取决于最后一个赋值是哪个类。所有对 self.X 的赋值语句都是引用一个相同实例,而 X 属性只有一个(I.X),无论有多少类使用了这个属性名。
为了保证属性会属于使用它的类,可在类中任何地方使用,将变量名前加上两个下划线:
class C1:
def meth1(self): self.__X = 88
def meth2(self): print(self.__X) # 变成 _C1__X
class C2:
def metha(self): self.__X = 99
def methb(self): print(self.__X) # 变成 _C2__X
class C3(C1, C2): pass
I = C3()
I.meth1(); I.metha()
print(I.__dict__)
I.meth2(); I.methb()
{'_C1__X': 88, '_C2__X': 99}
88
99
在属性赋值后查看命名空间字典,就会看见扩张后的变量名不是 X。
这个技巧可避免实例中潜在的变量名冲突,但并不是真正的私有。如果知道所在类的名称,依然可以使用扩张后的变量名(I._C__X = 77
),在能够引用实例的地方,读取这些属性。
伪私有属性在较大的框架或工具中,既可以避免引入可能在类树中某处偶然隐藏定义的新的方法,也可以减少内部方法被在树的较低处定义的名称替代的机会。
6. 方法是对象:绑定或无绑定
类方法可以从一个实例或一个类访问,它们实际上在 Python 中有两种形式:
- **无绑定类方法对象:无 self。**通过对类进行点号运算从而获取类的函数属性,会传回无绑定方法对象。调用该方法时,必须明确提供实例对象作为第一个参数。一个无绑定方法和一个简单的函数是相同的,可以通过类名来调用。
- **绑定实例方法对象:self + 函数对。**通过对实例进行全运算从而获取类的函数属性,会传回绑定方法对象。Python 在绑定方法对象中自动把实例和函数打包,所以不用传递实例去调用该方法。
class Spam:
def doit(self, message):
print(message)
object1 = Spam()
object1.doit('hello world')
hello world
绑定方法对象是在过程中产生的,就在方法调用的括号前。我们可以获取绑定方法,而不用实际进行调用。object.name 点号结合运算是一个对象表达式。
# 传回绑定方法对象,把实例 object1 和方法函数 Spam.doit 打包起来
# 可以把这个绑定方法赋值给另一个变量名,然后像简单函数那样进行调用
object1 = Spam()
x = object1.doit # 绑定方法对象:实例 + 函数
x('hello world')
hello world
如果对类进行点号运算来获得 doit,就会得到无绑定方法对象,也就是函数对象的引用值。要调用这类方法时,必须传入实例作为最左侧参数:
object1 = Spam()
t = Spam.doit # 非绑定方法对象
t(object1, 'howby')
howby
6.1 在 Python 3.X 中,无绑定方法是函数
在 Python 3.X 中,已经删除了无绑定方法的概念。
在 Python 3.X 中,不使用一个实例而调用一个方法没有问题,只要这个方法不期待一个实例,并且通过类调用它而不是通过一个实例调用它。只有对通过实例调用,Python 3.X 才会向方法传递一个实例。当通过一个类调用时,只有在方法期待一个实例的时候,才必须手动传递一个实例:
class Selfless:
def __init__(self, data):
self.data = data
def selfless(arg1, arg2): # 一个简单函数
return arg1 + arg2
def normal(self, arg1, arg2): # 调用时期待一个实例
return self.data + arg1 +arg2
X = Selfless(2)
X.normal(3, 4) # 自动传递实例给 self
9
Selfless.normal(X, 3, 4) # 方法期待一个 self:手动传递
9
Selfless.selfless(3, 4) # 无实例
7
X.selfless(3, 4) # 通过实例调用,自动把一个实例传递给一个并不期待实例的方法
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-8-abf884cf093e> in <module>()
----> 1 X.selfless(3, 4)
TypeError: selfless() takes 2 positional arguments but 3 were given
Selfless.normal(3, 4) # 通过类调用,不会把一个实例传递给期待一个实例的方法
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-7f4067e03b4b> in <module>()
----> 1 Selfless.normal(3, 4)
TypeError: normal() missing 1 required positional argument: 'arg2'
6.2 绑定方法和其他可调用对象
绑定方法可以作为一个通用对象处理,可以任意地在一个程序中传递:
class Number:
def __init__(self, base):
self.base = base
def double(self):
return self.base * 2
def triple(self):
return self.base * 3
x = Number(2)
y = Number(3)
z = Number(4)
x.double()
4
acts = [x.double, y.double, y.triple, z.double]
for act in acts:
print(act())
4
6
9
8
绑定方法对象拥有自己的内省信息,包括让它们配对的实例对象和方法函数访问的属性:
bound = x.double
bound.__self__, bound.__func__
(<__main__.Number at 0x1939dffab00>, <function __main__.Number.double(self)>)
bound.__self__.base
2
bound()
4
7. 类是对象:通用对象工厂
有时候,基于类的设计要求要创建的对象来响应条件,而这些条件是在编写程序的时候无法预料的。工厂设计模式允许这样的一种延迟方法。
可以将类传递给函数,这种函数在 OOP 设计中有时称为工厂。
def factory(aClass, *pargs, **kargs):
return aClass(*pargs, **kargs)
class Spam:
def doit(self, message):
print(message)
class Person:
def __init__(self, name, job=None):
self.name = name
self.job = job
object1 = factory(Spam)
object2 = factory(Person, 'Arthur', 'King')
object3 = factory(Person, name='Brain')
定义了一个对象生成函数 factory,期待传入一个类对象和多个参数。函数使用特殊的“varargs”调用语法调用函数并返回一个实例。
这是唯一一个需要在 Python 中编写的工厂函数,它对任意类和任意构建参数都有效。
object1.doit(99)
99
object2.name, object2.job
('Arthur', 'King')
object3.name, object3.job
('Brain', None)
7.1 为什么使用工厂函数?
通常来说,工厂函数可以使代码和动态配置对象构建的细节隔离开来。
在动态世界,我们可能不能硬编码流接口对象,而是根据配置文件的内容在运行时创建。
8. 多重继承:“混合”类
在 class 语句中,首行括号内可以列出一个以上的超类。当这么做时,就是在使用所谓的多重继承。
搜索属性时,Python 会由左至右搜索类首行中的超类,直到找到相符者。
当多个超类中有相同方法名时,会产生冲突,默认的继承通过 self.method() 会选择第一个出现的属性;也可以显式选择一个属性,通过引用类名 superclass.method(self)。
8.1 编写混合显示类
运算符重载中可以提供一个 __str__
或 __repr__
方法,以实现制定后的字符串表达形式。如果不在每个想打印的类中编写 __repr__
,可以在一个通用工具类中编写一次,然后在所有的类中继承。
用 __dict__
列出实例属性
# lister.py
class ListInstance:
"""
Mix-in class that provides a formatted print() or str() of instances via
inheritance of __str__ coded here; displays instance attrs only; self is
instance of lowest class; __X names avoid clashing with client's attrs
"""
def __attrnames(self):
result = ''
for attr in sorted(self.__dict__):
result += '\t%s=%s\n' %(attr, self.__dict__[attr])
return result
def __str__(self):
return '<Instance of %s, address %s:\n%s>' % (
self.__class__.__name__,
id(self), # id 内置函数显示实例的内存地址
self.__attrnames())
可以在仍然继承已有超类的同时自由地获得 __str__
。
# testmixin.py
# 通用自测试代码
import importlib
def tester(listerclass, sept=False):
class Super:
def __init__(self):
self.data1 = 'Spam'
def ham(self):
pass
class Sub(Super, ListInstance):
def __init__(self):
Super.__init__(self)
self.data2 = 'eggs'
self.data3 = 42
def spam(self):
pass
instance = Sub()
print(instance)
if sept: print('-' * 80)
def testByNames(modname, classname, sept=False):
modobject = importlib.import_module(modname)
listerclass = getattr(modobject, classname)
tester(listerclass, sept)
if __name__ == '__main__':
testByNames('listinstance', 'ListInstance', True)
testByNames('listinherited', 'ListInherited', True)
testByNames('listtree', 'ListTree', False)
使用 dir 列出继承的属性
# lister.py
class ListInherited:
"""
Use dir() to collect both instance attrs and names inherited from
its classes; Python 3.X shows more names than 2.X because of the
implied object superclass in the new-style class model; getattr()
fetches inherited names not in self.__dict__; use __str__, not
__repr__, or else this loops when printing bound methods!
"""
def __attrnames(self):
result = ''
for attr in dir(self):
if attr[:2] == '__' and attr[-2:] == '__': # 跳过内部名称
result += '\t%s\n' % attr
else:
result += '\t%s=%s\n' % (attr, getattr(self, attr))
return result
def __str__(self):
return '<Instance of %s, address %s:\n%s>' % (
self.__class__.__name__,
id(self),
self.__attrnames())
列出类树中每个对象的属性
# listtree.py
class ListTree:
"""
Mix-in that returns an __str__ trace of the entire class tree and all
its objects' attrs at and above self; run by print(), str() returns
constructed string; uses __X attr names to avoid impacting clients;
recurses to superclasses explicitly, uses str.format() for clarity;
"""
def __attrnames(self, obj, indent):
spaces = ' ' * (indent + 1)
result = ''
for attr in sorted(obj.__dict__):
if attr.startswith('__') and attr.endswith('__'):
result += spaces + '{0}\n'.format(attr)
else:
result += spaces + '{0}={1}\n'.format(attr, getattr(obj, attr))
return result
def __listclass(self, aClass, indent):
dots = '.' * indent
if aClass in self.__visited:
return '\n{0}<Class {1}:, address {2}: (see above)>\n'.format(
dots,
aClass.__name__,
id(aClass))
else:
self.__visited[aClass] = True
here = self.__attrnames(aClass, indent)
above = ''
for super in aClass.__bases__:
above += self.__listclass(super, indent+4)
return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n'.format(
dots,
aClass.__name__,
id(aClass),
here, above,
dots)
def __str__(self):
self.__visited = {}
here = self.__attrnames(self, 0)
above = self.__listclass(self.__class__, 4)
return '<Instance of {0}, address {1}:\n{2}{3}>'.format(
self.__class__.__name__,
id(self),
here, above)