面向对象基础
- 面向对象编程:把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)和泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派。
- 简单地说,类是对象的蓝图和模板,对象是类的实例。
- python中可以使用class关键字定义类,在类中通过函数定义方法,代码如下。
class Student(object):
# __init__是一个特殊方法用于在创建对象时进行初始化操作
# 通过这个方法我们可以为学生对象绑定name和age两个属性
def __init__(self, name, age):
self.name = name
self.age = age
def study(self, course_name):
print('%s正在学习%s.' % (self.name, course_name))
# PEP 8要求标识符的名字用全小写多个单词用下划线连接
# 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
def watch_movie(self):
if self.age < 18:
print('%s只能观看《熊出没》.' % self.name)
else:
print('%s正在观看岛国爱情大电影.' % self.name)
- 当定义好类后,可以创建对象并给对象发消息。
def main():
# 创建学生对象并指定姓名和年龄
stu1 = Student('骆昊', 38)
# 给对象发study消息
stu1.study('Python程序设计')
# 给对象发watch_av消息
stu1.watch_movie()
stu2 = Student('王大锤', 15)
stu2.study('思想品德')
stu2.watch_movie()
if __name__ == '__main__':
main()
- 在python中,属性和方法的访问权限只有两种:公开和私有。如果希望属性是私有的,在给属性命名时可以用两个下划线作为开头,代码如下。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
# AttributeError: 'Test' object has no attribute '__bar'
test.__bar()
# AttributeError: 'Test' object has no attribute '__foo'
print(test.__foo)
if __name__ == "__main__":
main()
- 但事实上,python并未从语法上严格保证其私有性,而只是给私有的属性和方法换了个名字妨碍访问,如果了解更换名字的规则就可以访问到,代码如下。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
test._Test__bar()
print(test._Test__foo)
if __name__ == "__main__":
main()
面向对象进阶
- 在实际开发中,并不建议将属性设为私有,这会导致子类无法访问。但如果直接将属性暴露给外界也是有问题的,建议是将属性名以单下划线开头,这样既不会设为私有,又能够表示其是受保护的,在访问该属性时就要保持慎重。
如果想访问该类属性,可以通过属性的getter(访问器)和setter(修改器)方法进行相应的操作。
可以使用@property包装器来包装getter和setter方法,使得对属性的访问既安全又方便。
class Person(object):
def __init__(self, name, age):
self._name = name
self._age = age
# 访问器 - getter方法
@property
def name(self):
return self._name
# 访问器 - getter方法
@property
def age(self):
return self._age
# 修改器 - setter方法
@age.setter
def age(self, age):
self._age = age
def play(self):
if self._age <= 16:
print('%s正在玩飞行棋.' % self._name)
else:
print('%s正在玩斗地主.' % self._name)
def main():
person = Person('王大锤', 12)
person.play()
person.age = 22
person.play()
# person.name = '白元芳' # AttributeError: can't set attribute
if __name__ == '__main__':
main()
- python是一门动态语言,允许在程序运行时给对象绑定新的属性或方法,也可对已绑定的进行解绑。但如果需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义_slots_变量来完成,_slots_的限定只对当前类的对象生效,对子类不起作用。
class Person(object):
# 限定Person对象只能绑定_name, _age和_gender属性
__slots__ = ('_name', '_age', '_gender')
def main():
person = Person('王大锤', 22)
person._gender = '男'
# AttributeError: 'Person' object has no attribute '_is_gay'
# person._is_gay = True
- 在类中定义的方法包括对象方法、静态方法和类方法等。
- 对象方法是发送给对象的消息,而静态方法只要定义了类,不必建立类的实例就可使用,属于类本身而非类的某个对象。
from math import sqrt
class Triangle(object):
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
@staticmethod # 静态方法
def is_valid(a, b, c):
return a + b > c and b + c > a and a + c > b
def perimeter(self):
return self._a + self._b + self._c
def area(self):
half = self.perimeter() / 2
return sqrt(half * (half - self._a) *
(half - self._b) * (half - self._c))
def main():
a, b, c = 3, 4, 5
# 静态方法和类方法都是通过给类发消息来调用的
if Triangle.is_valid(a, b, c):
t = Triangle(a, b, c)
print(t.perimeter())
# 也可以通过给类发消息来调用对象方法但是要传入接收消息的对象作为参数
# print(Triangle.perimeter(t))
print(t.area())
# print(Triangle.area(t))
else:
print('无法构成三角形.')
if __name__ == '__main__':
main()
- 类方法第一个参数约定名为cls,它代表当前类相关信息的对象(类本身也是一个对象,也称为类的元数据对象),通过该参数可以获取和类相关的信息且创建类的对象。
from time import time, localtime, sleep
class Clock(object):
"""数字时钟"""
def __init__(self, hour=0, minute=0, second=0):
self._hour = hour
self._minute = minute
self._second = second
@classmethod # 类方法
def now(cls):
ctime = localtime(time())
return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)
def run(self):
"""走字"""
self._second += 1
if self._second == 60:
self._second = 0
self._minute += 1
if self._minute == 60:
self._minute = 0
self._hour += 1
if self._hour == 24:
self._hour = 0
def show(self):
"""显示时间"""
return '%02d:%02d:%02d' % \
(self._hour, self._minute, self._second)
def main():
# 通过类方法创建对象并获取系统时间
clock = Clock.now()
while True:
print(clock.show())
sleep(1)
clock.run()
if __name__ == '__main__':
main()
- 类与类之间有三种关系:is-a、has-a和use-a。
is-a关系即继承或泛化,如学生-人。
has-a关系即关联,如部门-员工。如果是整体和部分的关联,即聚合关系;如果整体进一步负责了部分的生命周期(整体和部分是不可分割的,同时同在也同时消亡),即为合成关系,属于最强的关联关系。
use-a关系即依赖,如司机的驾驶方法,其中参数用到了汽车。 - 面向对象三大特性:封装、继承和多态。
封装:隐藏一切可以隐藏的实现细节,只向外界提供简单接口。在创建对象后,只需要调用方法就可以执行,只需要知道方法的名字和传入的参数,而不需要知道方法内部的实现细节。
继承:让一个类从另一个类那里将属性和方法直接继承下来,减少重复代码的编写。提供信息的为父类,又称超类或基类;继承信息的为子类,又称派生或衍生类。子类不仅可以继承父类的属性和方法,还可以定义自己的,在实际开发中,通常会用子类对象替换父类对象,即里氏替换原则。
多态:子类对父类的方法给出新的实现版本,称为方法重写(override),从而让父类的同一行为在子类中拥有不同版本。当调用这个经过子类重写的方法时,不同子类对象会表现出不同的行为,即多态(poly-morphism)。
from abc import ABCMeta, abstractmethod
class Pet(object, metaclass=ABCMeta):
"""宠物"""
def __init__(self, nickname):
self._nickname = nickname
@abstractmethod
def make_voice(self):
"""发出声音"""
pass
class Dog(Pet):
"""狗"""
def make_voice(self):
print('%s: 汪汪汪...' % self._nickname)
class Cat(Pet):
"""猫"""
def make_voice(self):
print('%s: 喵...喵...' % self._nickname)
def main():
pets = [Dog('旺财'), Cat('凯蒂'), Dog('大黄')]
for pet in pets:
pet.make_voice()
if __name__ == '__main__':
main()
- 上面代码里,将Pet类处理为抽象类,即不能够创建对象,只为让其他类来继承。python从语法层面没有提供对抽象类的支持,但可以通过abc模块的ABCMeta元类和abstractmethod包装器来达到抽象类的效果,如果一个类中存在抽象方法它就不能实例化。Dog和Cat子类分别对Pet类中的make_voice抽象方法进行了不同地重写,在main函数中调用该方法时,就表现出了多态行为。
整数比较
- 在python中比较两个整数时有两种运算符:==和is:is比较的是两者id值是否相等,即是否指向同一个地址;==比较两者内容是否相等,实际上调用了_eq_()方法。
- 矛盾1代码如下。
def main():
x = y = -1
while True:
x += 1
y += 1
if x is y:
print('%d is %d' % (x, y))
else:
print('Attention! %d is not %d' % (x, y))
break
x = y = 0
while True:
x -= 1
y -= 1
if x is y:
print('%d is %d' % (x, y))
else:
print('Attention! %d is not %d' % (x, y))
break
if __name__ == '__main__':
main()
- 矛盾1解释:python出于对性能的考虑,会将一些频繁使用的整数对象缓存到一个叫small_ints的链表中,任何需要使用这些整数对象的时候,都不需要再重新创建,而是直接引用缓存。缓存的区间为[-5,256],当使用is进行比较时,在该范围内的指向同一地址,而超出该范围则为重新创建的,就会得到257 is 257结果为false的情况。
- 矛盾2代码如下:
import dis
a = 257
def main():
b = 257 # 第6行
c = 257 # 第7行
print(b is c) # True
print(a is b) # False
print(a is c) # False
if __name__ == "__main__":
main()
- 矛盾2解释:代码块是程序的最小执行单位,在上述代码中,a=257和main属于两个代码块。python为进一步提高性能,规定在同一个代码块中创建的整数对象,即便超出[-5,256]的范围,只要存在有值相同的整数对象,后续创建就直接引用。该规则对不在small_ints范围内的负数和负数值浮点数并不适用,但对非负浮点数和字符串都适用。因而c引用了b的257,而a与b不在同一个代码块内,才会得出注释中的执行结果。
- 导入dis模块并在main()方法下添加“dis.dis(main)”一句,可进行反汇编,从字节码的角度来看该代码。运行结果中,代码第6、7行的257,是从同一位置加载的,而第9行的a则是从不同地方加载的,因此引用的是不同的对象。
嵌套列表
- 把列表作为列表中的元素,即为嵌套列表,可以模拟现实中的表格、矩阵和棋盘等,但需谨慎使用,否则会出现问题,代码如下。
def main():
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
subjs = ['语文', '数学', '英语']
scores = [[0] * 3] * 5
for row, name in enumerate(names):
print('请输入%s的成绩' % name)
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
- 上述代码原本打算录入五位同学的三门成绩,但最终输出结果却发现,每个学生三门课程的成绩是一样的,而且就是最后录入的那个学生的成绩。解决此问题,需要区分开对象和对象的引用两个概念,这就涉及到内存中的栈和堆。
- 程序中可以使用的内存从逻辑上可以分为五个部分,按照地址从高到低依次是:栈、堆、数据段、只读数据段和代码段。
其中,栈用来存储局部、临时变量,以及函数调用时保存和恢复现场需要用到的数据,这部分内存在代码块执行开始时自动分配,结束时自动释放,通常由编译器自动管理。
堆的大小不固定,可以动态地分配和回收,如果程序中有大量数据需要处理,通常都放在堆上。如果堆空间没有被正确释放,会引发内存泄露的问题,像Python、Java等都使用了垃圾回收机制(自动回收不再使用的堆空间),来实现自动化的内存管理。 - 因此以如下代码为例,变量a并不是真正的对象,而是对象的引用,相当于记录了对象在堆空间中的地址;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象。
a = object()
b = ['apple', 'pitaya', 'grape']
- 再看最初的程序,对列表进行[[0]*3]*5操作时,仅仅是将[0,0,0]这个列表的地址进行了复制,并没有创建新的列表对象。所以容器中虽然有五个元素,却都引用的是同一个列表对象,每次输入都相当于对该列表对象进行了修改,因此最终显示的即为最后一个学生的成绩,正确的代码如下。
def main():
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
subjs = ['语文', '数学', '英语']
scores = [[]] * 5 # 或scores = [[0] * 3 for _ in range(5)]
for row, name in enumerate(names):
print('请输入%s的成绩' % name)
scores[row] = [0] * 3
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()