Python——类的设计

Posted KLeonard

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python——类的设计相关的知识,希望对你有一定的参考价值。

类的设计

这里讨论Python的OOP的设计问题,也就是如何使用类来对有用的对象进行建模。将编写Python中常用的OOP设计模式,例如,继承、组合、委托和工厂。另外介绍一些类设计的概念,例如伪私有属性、多继承等。

================================================================================

Python和OOP

Python的OOP实现与Java类似,可以概括为三个概念:

1.【继承】继承是基于Python中的属性查找(在X.name表达式中)
2.【多态】在X.method方法中,method的意义取决于X的类型
3.【封装】方法和运算符实现行为,数据隐藏默认是一种惯例

================================================================================

OOP和继承:“是一个”关系


举例说明:披萨店团队可以通过文件employees.py中的四个类来定义。最通用的类Employee提供共同行为,例如,加薪(giveRaise)和打印(__repr__)。员工有两种,所以Employee有两个子类:Chef和Server(厨师和服务员)。这两个子类都会覆盖继承的work方法来打印更具体的信息。最后,披萨机器人是由更具体的类来模拟:PizzaRobot是一种Chef,也是一种Employee。以OOP术语来看,这些关系为“是一个”(is-a)链接:机器人是一个主厨,而主厨是一个员工。代码编写如下:

#File employees.py

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(.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
可以看到,bob是一个PizzaRobot,但是打印的时候,依然显示是Employee,从结果看不出他具体属于哪一类员工,这时候可以修改Employee类中的__repr__方法如下:

def __repr__(self):
        return '<%s : name = %s , salary = %s>'%(self.__class__.__name__,self.name,self.salary)
其中,self.__class__.__name__,可以返回实例所属类的名称,修改之后执行结果如下:
<PizzaRobot : name = bob , salary = 50000>
bob makes pizza
<PizzaRobot : name = bob , salary = 60000.0>

Employee does stuff
Chef makes food
Server interfaces with customer
PizzaRobot makes pizza
================================================================================

OOP和组合:“有一个”关系

组合涉及把其他对象嵌入容器对象内,并使用其容器方法。组合反映了各组成部分之间的关系,通常称为“有一个”(has-a)关系。
接着上边的例子,既然我们已经有了员工,就把他们放到我们披萨店。披萨店是一个组合对象,有个烤炉,也有服务员和主厨这些员工。当顾客来店里下单时,店里的组件就会开始行动:服务员接单,主厨制作披萨,等等。看下边的例子pizzashop.py.

#File pizzashop.py

from employees import PizzaRobot,Server

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') #一个名叫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') #模拟Homer的订单
    print('...')
    scene.order('Shaggy') #模拟Shaggy的订单
这里,PizzaShop是类的容器和控制器。当执行这个模块时,我们的披萨店处理了两份订单:一份来自Hommer,另一份来自Shaggy。
Homer orders from <Server : name = Pat , salary = 40000>
Bob makes pizza
oven bakes
Homer pays for item to <Server : name = Pat , salary = 40000>
...
Shaggy orders from <Server : name = Pat , salary = 40000>
Bob makes pizza
oven bakes
Shaggy pays for item to <Server : name = Pat , salary = 40000>
================================================================================
OOP和委托:“包装”对象

所谓“委托”,通常就是指控制器对象内嵌其他对象,而把运算请求传给那些对象。
在Python中,委托通常是以__getattr__钩子方法实现的,因为这个方法会拦截对不存在属性的读取,包装类可以使用__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是表达式,可在运行时计算出字符串,而不是变量。事实上,getattr(X,N)类似于X.__dict__[N]。

你可以使用这个模块包装类的做法,管理任何带有属性的对象的存取:列表、字典甚至是类和实例。在这里,wrapper类只是在每个属性读取时打印跟踪消息,并把属性请求委托给嵌入的对象wrapped
>>> x = wrapper([1,2,3])
>>> x.append(4)
Trace: append
>>> x.wrapped
[1, 2, 3, 4]
>>> 
>>> x = wrapper({'a':1,'b':2})
>>> x.keys()
Trace: keys
dict_keys(['b', 'a'])
实际的效果就是以包装类内额外的代码来增强被包装的对象的整个接口。我们可以利用这种方式记录方法调用,把方法调用转给其他或定制的逻辑等。
================================================================================
类的伪私有属性——变量名压缩

Python支持变量名压缩的概念,让类内的某些变量局部化,但名称压缩并无法阻止类外部代码对它的读取,这种功能主要是为了避免实例内的命名空间的冲突,而不是限制变量名的读取。因此,压缩的变量名最好称为“伪私有”。

当然,伪私有属性是高级且完全可选的功能,但由于你可能在其他人的代码中看见这个功能,所以即使不用,多少还得留意。

【变量名压缩的工作方式是这样的:class语句内开头有两个下划线,但结尾没有两个下划线的变量名,会自动扩张,从而包含了所在类的名称。】
例如,像Spam类内__X这样的变量名会自动变成_Spam__X:原始的变量名会在头部加入一个下划线,然后是所在类的名称。因为修改后的变量名包含了所在类的名称,相当于变得独特。不会和同一层次中其他类所创建的类似变量名冲突。

变量名压缩只发生在class语句内,而且只针对开头有两个下划线的变量名。

为什么要使用伪私有属性,举例如下:
一个程序员编写的一个类,他认为属性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):...
I = C3()
因为C1和C2中的变量名都是X,所以I中只能有一个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()
>>> print(I.__dict__)
{'_C1__X': 88, '_C2__X': 99}
这样,X属性就会扩张,从而包含它的类的名称,然后才加到实例中。查看其命名空间字典可以看到变量变成了_C1__X和_C2__X。

但这并不是真正的私有,你依然可以使用扩张后的变量名,例如,I._C1__X = 77
================================================================================

多重继承:“混合”类

在class语句中,首行括号内可以列出一个以上的超类。当这么做时,就是在使用所谓的【多重继承】。

搜索属性时,Python会由左至右搜索首行中的超类,直到找到相符者。

通常来说,多重继承是建模属于一个集合以上的对象的好办法。例如,一个人可以是工程师、作家、音乐家等,因此,可继承这些集合的特性。

================================================================================

类是对象:通用对象的工厂

>>> def factory(aClass,*args):
	return aClass(*args)

>>> class Spam:
	def doit(self,message):
		print(message)

>>> class Person:
	def __init__(self,name,job):
		self.name = name
		self.job = job

>>> obj1 = factory(Spam)
>>> obj2 = factory(Person,'Guido','gurr')
在这段代码中, 我们定义了一个对象生成器函数,称为factory。它预期传入的是类对象,还有该类构造函数的一个或多个参数。
然后定义了两个类,并将其传给factory函数以产生两者的实例。这就是Python中编写工厂函数所需要做的事。它适用于任何类以及任何构造函数参数。

这类工厂可以将代码和动态配置对象的构造细节隔离开,也许以后会用到。

以上是关于Python——类的设计的主要内容,如果未能解决你的问题,请参考以下文章

Python--类的继承和多态

《Python学习手册 第五版》 -第31章 类的设计

《Python学习手册 第五版》 -第31章 类的设计

python 设计并实现计算不同职称的教师工资(类的练习)

Python面向对象程序设计第一弹

Python中,类的特殊方法与内置函数的关联