一 组合的相关概念
1 组合:把类的实例化放在一个新类里面,就把旧类的内容组合到了新类里面
2 举例:
1 class Turtle(object): 2 def __init__(self,x): 3 self.num = x 4 5 class Fish(object): 6 def __init__(self,x): 7 self.num = x 8 9 class Pool(object): 10 def __init__(self,x,y): 11 self.turtle = Turtle(x) 12 self.fish = Fish(y) 13 14 def print_num(self): 15 print(f"水池里面总共有乌龟{self.turtle.num}只,小鱼{self.fish.num}条!") 16 17 pool = Pool(2,3) 18 pool.print_num()
3 继承和组合的使用时机
-
“继承与组合”的问题可以归结为试图解决可复用代码的问题。
-
继承通过在基类中创建隐含特性的机制来解决这个问题。
-
组合通过提供模块以及调用其他类中的函数来解决这个问题
-
- 怎么区分使用:
- 无论如何都要避免多重继承,因为它太复杂而且不可靠。如果你被它困住了,那么要准备好了解一下类的层次结构,并花时间找出所有内容的来源。
- 使用组合将代码打包到模块中,这些模块可以用于许多不同的、不相关的地方和情境。
- 只有当存在明显相关的可复用代码片段,并且这些代码片段符合单个通用概念,或者由于你使用了某些东西而别无选择时,你才可以使用继承。
- 关于面向对象编程,需要记住的一点是,它完全是程序员为了打包和共享代码而创建的一种社会约定。因为这是一种社会惯例,并且在 Python 中已经形成了这种惯例,你可能会因为与你一起工作的人而被迫绕过这些规则。在这种情况下,弄明白他们是如何使用每一种东西,然后努力适应这种情况。
二 类、类对象和实例对象
1 实例属性和类属性:
举例:
1 >>> class C(object): 2 ... count = 0 3 ... 4 >>> a = C() 5 >>> b = C() 6 >>> c = C() 7 >>> a.count 8 0 9 >>> b.count 10 0 11 >>> c.count 12 0 13 >>> c.count += 10 14 >>> c.count 15 10 16 >>> a.count 17 0 18 >>> b.count 19 0 20 >>> C.count = 100 21 >>> a.count 22 100 23 >>> b.count 24 100 25 >>> c.count 26 10
- 上述例子中:C是类对象,而a,b,c是实例对象。类属性(类中定义的变量)和类对象是相互绑定的,并不会依赖于实例对象。
- 即“c.count += 10” 只是改变了实例对象c对应的实例属性count的值(包括两个动作:创建一个实例属性,并给它赋值为10),并不会对跟类对象绑定的类属性count的值有任何影响;
- 而“C.count += 100”直接改变了类属性count的值,因为我们之前已经通过“c.count += 10”为实例对象c创建了一个新的实例属性,所以当我们再次执行c.count语句的时候调用的是实例对象C本身的实例属性,其值为10;而另外两个实例对象a,b并没有创建自己的实例属性(进行赋值操作),通过a.count和b.count其实相当于调用类本身的类属性count的值,所以输出为100.
2 属性跟方法
- 举例:
1 >>> class C(object): 2 ... def x(self): 3 ... print("X-man!") 4 ... 5 >>> c = C() 6 >>> c.x() 7 X-man! 8 >>> c.x = 1 9 >>> c.x 10 1 11 >>> c.x() 12 Traceback (most recent call last): 13 File "<stdin>", line 1, in <module> 14 TypeError: 'int' object is not callable
- 常见错误:如果属性名跟方法名相同,属性会覆盖方法
- 解决办法
- 不要试图在一个类里面定义出所有能够想到的特性和方法,应该利用继承和组合机制来进行扩展。
- 用不同的词性命名:如属性名用名词;方法名用动词
3 到底什么是绑定的概念?
- 绑定:Python严格要求方法需要有实例才能被调用,这种限制其实就是python所谓的绑定的概念
- 举例1 :
1 >>> class BB(object): 2 ... def printBB(): 3 ... print("no zuo no die") 4 ... 5 >>> BB.printBB() 6 no zuo no die 7 >>> bb = BB() 8 >>> bb.printBB() 9 Traceback (most recent call last): 10 File "<stdin>", line 1, in <module> 11 TypeError: printBB() takes 0 positional arguments but 1 was given
这个例子中,定义printBB()的时候没有参数,后面通过类名(类对象)调用的时候可以正常打印,但是在为该类创建了一个实例对象bb之后,通过实例对象调用printBB()方法却没办法调用,所以说在定义printBB()方法的时候必须定义一个参数用来将实例名称传给该方法,这个过程即为绑定过程
- 举例2:
1 >>> class CC(object): 2 ... def setXY(self,x,y): 3 ... self.x = x 4 ... self.y = y 5 ... def printXY(self): 6 ... print(self.x,self.y) 7 ... 8 >>> dd = CC() 9 >>> dd.__dict__ 10 {} 11 >>> CC.__dict__ 12 mappingproxy({'__module__': '__main__', 'setXY': <function CC.setXY at 0x00000062CE34B670>, 'printXY': <function CC.printXY at 0x00000062CE34B700>, '__dict__': <attribute '__dict__' of 'CC' objects>, '__weakref__': <attribute '__weakref__' of 'CC' objects>, '__doc__': None})
- dd.__dict__ :实例对象的字典中仅有实例对象的属性,不显示类属性和特殊属性,就式没有显示哪些魔法方法(下划线包围的)
- CC.__dict__:类对象的字典中全部显示,键表示属性名,值表示对应的值
- 绑定的好处:
- dd.__dict__:实例对象中没有显示类属性和类方法,也没有显示魔方方法(下划线包围)
- CC.__dict__:类对象中全部显示了,键表示属性名/方法名,值表示键对应的值
- 总结:相当于把类对象和实例对象完全分开了,之前定义的实例对象拥有独立于类对象存储空间之外的自己的存储空间
- 举例3(补充2的内容):
1 >>> dd.setXY(4,5) 2 >>> dd.__dict__ 3 {'x': 4, 'y': 5} 4 >>> CC.__dict__ 5 mappingproxy({'__module__': '__main__', 'setXY': <function CC.setXY at 0x00000062CE34B670>, 'printXY': <function CC.printXY at 0x00000062CE34B700>, '__dict__': <attribute '__dict__' of 'CC' objects>, '__weakref__': <attribute '__weakref__' of 'CC' objects>, '__doc__': None}) 6 >>> del CC 7 >>> ee = CC() 8 Traceback (most recent call last): 9 File "<stdin>", line 1, in <module> 10 NameError: name 'CC' is not defined 11 >>> dd.printXY() 12 4 5
- 1,2,3,4,5行:类对象的存储空间和实例对象的存储空间完全独立
- 6,7,8,9,10,11行:类对象被删除,类对象的存储空间被销毁,但实例对象的存储空间仍旧存在,只有当程序退出之后,实例对象的存储空间才会被释放,相当于C语言中的静态变量
- 总结:
- 在大多数情况下,在编程中应该考虑使用实例属性,而不要去使用类属性;
- 类属性通常使用来跟踪与类相关的一些值
有关继承和组合的内容,可参考:https://www.cnblogs.com/luoxun/p/13476773.html
有关类、类对象、实例对象的内容,可参考:https://www.cnblogs.com/luoxun/p/13411699.html
有关Mixin编程机制的内容:https://fishc.com.cn/forum.php?mod=viewthread&tid=48888&extra=page%3D1%26filter%3Dtypeid%26typeid%3D403
三 课后作业
测试题部分:
0. 什么是组合(组成)?
答:Python 继承机制很有用,但容易把代码复杂化以及依赖隐含继承。因此,经常的时候,我们可以使用组合来代替。在Python里组合其实很简单,直接在类定义中把需要的类放进去实例化就可以了。例子见class39.py文件
1. 什么时候用组合,什么时候用继承?
答:根据实际应用场景确定。简单的说,组合用于“有一个”的场景中,继承用于“是一个”的场景中。例如,水池里有一个乌龟,天上有一个鸟,地上有一个小甲鱼,这些适合使用组合。青瓜是瓜,女人是人,鲨鱼是鱼,这些就应该使用继承啦。
2. 类对象是在什么时候产生?
答:当你这个类定义完的时候,类定义就变成类对象,可以直接通过“类名.属性”或者“类名.方法名()”引用或使用相关的属性或方法。
3. 如果对象的属性跟方法名字相同,会怎样?
答:如果对象的属性跟方法名相同,属性会覆盖方法。例子见ex39_2.py文件
4. 请问以下类定义中哪些是类属性,哪些是实例属性?n8TkM9
1 class C: 2 num = 0 3 def __init__(self): 4 self.x = 4 5 self.y = 5 6 C.count = 6
答:num 和 count 是类属性(静态变量),x 和 y 是实例属性。大多数情况下,你应该考虑使用实例属性,而不是类属性(类属性通常仅用来跟踪与类相关的值)。1
动动手部分:
0. 思考这一讲我学习的内容,请动手在一个类中定义一个变量,用于跟踪该类有多少个实例被创建(当实例化一个对象,这个变量+1,当销毁一个对象,这个变量自动-1)。
1 class C(object): 2 count = 0 3 4 def __init__(self): 5 C.count += 1 6 7 def __del__(self): 8 C.count -= 1 9 10 a = C() 11 b = C() 12 c = C() 13 print(C.count) 14 15 del a 16 print(C.count) 17 18 del b,c 19 print(C.count)
1. 定义一个栈(Stack)类,用于模拟一种具有后进先出(LIFO)特性的数据结构。至少需要有以下方法:
方法名 | 含义 |
isEmpty() | 判断当前栈是否为空(返回 True 或 False) |
push() | 往栈的顶部压入一个数据项 |
pop() | 从栈顶弹出一个数据项(并在栈中删除) |
top() | 显示当前栈顶的一个数据项 |
bottom() | 显示当前栈底的一个数据项 |
代码:
1 class Stack(object): 2 def __init__(self,start = []): 3 self.stack = [] 4 for x in start: 5 self.push(x) 6 7 def isEmpty(self): 8 return not self.stack 9 10 def push(self,obj): 11 self.stack.append(obj) 12 13 def pop(self): 14 if not self.stack: 15 print("警告:栈为空!") 16 else: 17 return self.stack.pop() 18 19 def top(self): 20 if not self.stack: 21 print("警告:栈为空!") 22 else: 23 return self.stack[-1] 24 25 def bottom(self): 26 if not self.stack: 27 print("警告:栈为空!") 28 else: 29 return self.stack[0] 30 31 32 stack = Stack() 33 obj1 = input("请输入第一个入栈元素:") 34 stack.push(obj1) 35 obj2 = input("请输入第二个入栈元素:") 36 stack.push(obj2) 37 print(f"栈中的元素为{stack.stack}") 38 print(f"判断栈是否为空:{stack.isEmpty()}") 39 print(f"栈顶的数据为:{stack.top()}") 40 print(f"栈底的数据为:{stack.bottom()}")