第四章 面向对象-类
面向对象有三个核心,封装,继承,多态
4.1 封装
程序模拟事物的时候,通常用抽象的方法,把现实中的事物归类,
某一类事物,具有相同的行为和属性。
对应到程序中,就是自定义一种数据类型,这种数据类型同样有对应的行为和属性
行为在程序中对应的是一个方法,就是函数
属性对应的是一个变量
比如我们想把人在程序中定义出来。

这就是面向对象的一个编程方法,归纳现实事物的属性和行为,然后用编程语言来实现
如果只是用函数来实现就不够好了,因为用多个函数实现后还是一个散装的代码,
我们希望把这人相关的代码封装在一个比较集中的空间里。
这就是面向对象的第一个特性,封装
# 类的定义 class Person: ''' 这是一个关于人的类 ''' def __init__(self, name, age, height): self.name = name self.age = age self.height = height def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") wfg = Person("王富贵", 18, 172) wfg.say() wfg.eat() print("name", wfg.name) print("age", wfg.age) print('height', wfg.height) print(help(Person))
__init__是一个特殊的函数,在类进行初始化的时候自动被调用
__init__方法是不允许有返回值的
类中方法的第一个参数是固定的,代表着类的本身,虽然可以自己起名字,但是不要搞野路子,就用self
其它的,也没什么好说了,这样定义一个类之后 ,这个类封装了事物的属性和方法。
虽然类中的方法和函数的用法是一样的,但面向对象中不叫函数,就叫方法,这是一个术语上的区别
类的定义和使用(实例化)是两回事,类似于函数的定义和调用
在其它语言的封装特性中可能会有一个叫私有属性和私有方法的东西
就是在类实例化后,不希望类的使用者去直接访问的
在Python里,如果我们不希望类外访问属性和变量我们就以两个__开头
# 类的定义 class Person: def __init__(self, name, age, height): self.name = name self.age = age self.height = height # 属性和方法不想让别人在类外使用 # 以下划线开头 self.__age = age def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.__age, self.height)) # 以下划线开头 # 类外无法访问 def __eat(self): print("食不言,寝不语, en.. en.. 好吃!") zxj = Person("赵小姐", 26, 162) zxj.say() # 无法访问 # zxj.__eat() print("name", zxj.name) # 无法访问 # print("age", zxj.__age) print('height', zxj.height) # 硬访问 zxj._Person__eat()
4.2 继承
现实事物很多类型是相似的,并且有一个继承的关系
比如 定义了一个类型人,那我再定义一个 程序员的类型
程序员本身也是一个人,所以类型人的属性和行为, 程序员应该都有
程序员又多了一个属性,使用的语言,还多了一个行为,开发
那我们定义程序员这种类型的时候,完全没必要再把他属于人的那些属性和行为定义一遍,直接继承人的属性
使用类型人已经定义好的那些属性和行为就可以了,这就是继承
Person这种被继承的类叫父类,基类,祖先等
Coder这种继承的类叫子类,派生类,子孙等

# 继承 class Person: ''' 这是一个关于人的类 ''' def __init__(self, name, age, height): self.name = name self.age = age self.height = height def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") class Coder(Person): ''' 程序员类,继承人 ''' def __init__(self, name, age, height, language, *args, **kwargs): # 调用被继承类的方法 super().__init__(name, age, height, *args, **kwargs) self.language = language def develop(self): print("我来开发程序了,我使用的语言是%s" % self.language) def say(self): super().say() print('其实我还是一个程序员') coder = Coder('王富贵', 18, 178, 'python') coder.say() coder.eat() coder.develop()
也可以同时继承多个类,继承多个类时要注意,如果继承的父类之间有相同的属性和方法,继承顺序前面的将会覆盖后面的
# 多重继承 class Person: ''' 这是一个关于人的类 ''' def __init__(self, name, age, height): self.name = name self.age = age self.height = height def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") class Machine: '''这是一个关于机器的类''' def say(self): print('这台机器生产于2050年3月') def charging(self): print('充电中。。。') class Robot(Person, Machine): pass r = Robot('佳佳', 1, 175) r.say() r.charging() print(r.name)
4.3 多态
多态是指对不同的类型执行相同的操作,这些操作都能神奇的运行
比如我们学过的 字符串,列表,元组,我们用len方法去运算他们,无论他们具体是字符串,还是元组,列表还是字典
我们都可以求出他们的长度。
其实多态是我们在设计一组类时,我们希望他们能有相同的操作,这样,我们在使用这一组类的时候,
不用关心他们具体是什么类,只要他们属于这一组类,我们就可以对他们执行一些通用的操作
在其它语言当中,会有一种叫接口的东西,接口中定义了通用的方法,就是创建类的时候 ,我们可以指定这个类是否属于这个接口,
一但属于这个接口,就必须实现接口中的所有方法。
在我们Python中,是通过一种抽象类的东西来代替接口
我们需要把一些通用的操作,定义在一个基类当中,然后把基类设置成抽象类
这样在设计其它类时,如果继承了这个基类,那就必须实现这个基类中预先定义好的所有方法
from abc import ABC, abstractmethod class Worker(ABC): @abstractmethod def work(self): pass class Coder(Worker): def work(self): print('写代码') class Tester(Worker): def work(self): print("写用例") employee_list = [] for i in range(10): if i %2 == 0: employee = Coder() else: employee = Tester() employee_list.append(employee) # 使用时不关心具体类型 ,调用相同方法 for employee in employee_list: employee.work()
4.4 类的命名空间
类和函数也一样,也有自己的作用域
类的作用域中所有的实例都可以访问,而且访问的是同一块内存空间
这块内存在类的定义时创建并初始化
相当于该类所有对象的全局变量
# 类的命名空间 class Person: # 定义一个全局变量 count = 0 def __init__(self, name, age, height): self.name = name self.age = age self.height = height Person.count += 1 def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") wfg = Person('王富贵', 18, 172) print(wfg.count) zxj = Person('赵小姐', 26, 162) print(zxj.count) #在实例中定义了一个变量 wfg.count = 100 print(wfg.count) print(zxj.count) print(Person.count)
注意:类的全局变量,最好用类名去访问
如果通过实例去访问是不能修改类的全局变量的
类作用域中定义的方法,可以分为三类
静态方法:就是在类的作用域,一个很普通的函数
类方法: 在类的作用域,第一个参数固定为类本身的方法
实例方法: 默认的方法,第一个参数固定为类实例的方法
# 类方法 和静态方法 from datetime import datetime class Person: # 定义一个全局变量 count = 0 def __init__(self, name, age, height): self.name = name self.age = age self.height = height Person.count += 1 # 类方法 @classmethod def get_count(cls): print('当前类的实例数为%s' % cls.count) # 静态方法 @staticmethod def now(): print(datetime.now()) # 实例方法 def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") wfg = Person('王富贵', 18, 172) zxj = Person('赵小姐', 26, 162) wfg.count = 1000 wfg.get_count() zxj.get_count() wfg.now() wfg.say()
正常调用类中定义的方法时,要加(),类中还支持一种,把方法变成属性的操作
# 方法包装成属性 class Rectange: def __init__(self, length, width): self.length = length self.width = width @property def area(self): return self.length * self.width r = Rectange(10, 20) print(r.area)
4.5 实用函数
判断一个类是不是另一个类的子类
issubclass
判断一个对象是不是一个类的实例
isinstance
判断一个对象是否有一个属性
hasattr
获取一个对象的指定属性
getattr
设置一个对象的属性
setattr
判断一个对象是否是可调用的
callable
显示对象的所有属性
dir
# 实用函数 class Person: ''' 这是一个关于人的类 ''' def __init__(self, name, age, height): self.name = name self.age = age self.height = height def say(self): print("大家好,我叫%s, 我今年%s岁了, 我身高%s" % (self.name, self.age, self.height)) def eat(self): print("食不言,寝不语, en.. en.. 好吃!") class Coder(Person): ''' 程序员类,继承人 ''' def __init__(self, name, age, height, language, *args, **kwargs): # 调用被继承类的方法 super().__init__(name, age, height, *args, **kwargs) self.language = language def develop(self): print("我来开发程序了,我使用的语言是%s" % self.language) def say(self): super().say() print('其实我还是一个程序员') wfg = Coder('王富贵', 18, 172, 'python') zxj = Person('赵小姐', 26, 162) print('Coder 是不是Person的子类', issubclass(Coder, Person)) print('wfg 是不是Coder的实例', isinstance(wfg, Coder)) print('wfg 是不是Person的实例', isinstance(wfg, Person)) print('zxj 是不是Coder的实例', isinstance(zxj, Coder)) print('zxj 是不是Person的实例', isinstance(zxj, Person)) print('wfg 是否有develop属性', hasattr(wfg, 'develop')) print('zxj 是否有develop属性', hasattr(zxj, 'develop')) develop = getattr(wfg, 'develop') develop() develop = getattr(zxj, 'develop', None) print(develop) print('develop是否可以调用', callable(wfg.develop)) print('Person是否可以调用', callable(Person)) print('name是否可以调用', callable(wfg.name)) setattr(wfg, 'salary', 2000) print(wfg.salary)
print(dir(wfg))
Coder 是不是Person的子类 True
wfg 是不是Coder的实例 True
wfg 是不是Person的实例 True
zxj 是不是Coder的实例 False
zxj 是不是Person的实例 True
wfg 是否有develop属性 True
zxj 是否有develop属性 False
我来开发程序了,我使用的语言是python
None
develop是否可以调用 True
Person是否可以调用 True
name是否可以调用 False
2000
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'develop', 'eat', 'height', 'language', 'name', 'salary', 'say']
4.6 异常
我们编写了一些代码,大家肯定遇到过,程序运行过程中产生错误时,会报错并停止执行
程序运行过程中产生的错误,就是异常。通常产生了异常,python解释器就停止执行后面的代码,并把异常信息给我们打印出来
比如下面两个觉的异常
# 异常 print(x)
NameError Traceback (most recent call last) <ipython-input-79-7d8dbe33b1af> in <module> 1 # 异常 2 ----> 3 print(x) NameError: name 'x' is not defined
1/0
ZeroDivisionError Traceback (most recent call last) <ipython-input-80-9e1622b385b6> in <module> ----> 1 1/0 ZeroDivisionError: division by zero
一个是运行时找不到x变量的定义 ,一个是用1/0引发的错误
我们也同样可以手动触发一个异常,当我们预见了一个错误时,就可以使用raise语句自己触发异常
x = 1 y = 0 if y == 0: raise ZeroDivisionError("被除数是0") else: x/y
ZeroDivisionError Traceback (most recent call last) <ipython-input-82-6c267fdbdce1> in <module> 2 y = 0 3 if y == 0: ----> 4 raise ZeroDivisionError("被除数是0") 5 else: 6 x/y ZeroDivisionError: 被除数是0
rasie触发异常,必须是内置的异常类或是我们自己定义的异常类
python中所有异常的父类是Exception, 各种异常都是继承Exception的
下面是一些常见的异常
Exception 几乎所有的异常类都是从它派生而来的
AttributeError 引用属性或给它赋值失败时引发
OSError 操作系统不能执行指定的任务(如打开文件)时引发,有多个子类
IndexError 使用序列中不存在的索引时引发,为LookupError的子类
KeyError 使用映射中不存在的键时引发,为LookupError的子类
NameError 找不到名称(变量)时引发
SyntaxError 代码不正确时引发
TypeError 将内置操作或函数用于类型不正确的对象时引发
ValueError 将内置操作或函数用于这样的对象时引发:其类型正确但包含的值不合适
ZeroDivisionError 在除法或求模运算的第二个参数为零时引发
我们自己定义一个异常,同样也要继承Exception
# 自定义异常 class CaseFailed(Exception): pass expected = False if expected == False: raise CaseFailed('用例测试失败')
有时候我们可能不希望程序一出现异常就停止 ,毕竟出现错误应该是少部分情况
那我就就需要捕获异常,并处理他们
# 异常捕获 try: 1/1 except ZeroDivisionError as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
捕获异常之后 程序就不会停止 运行,打印出错误 信息之后 ,经过我们的处理,程序会继续往下执行
指定捕获异常,只针对那一种异常才生效,其它的异常产生后,程序仍然会停止运行
try: #要测试的代码 1/1 print(adaga) except ZeroDivisionError as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
无论如何都要执行这里 --------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-88-2ebc31cf6e2a> in <module> 2 #要测试的代码 3 1/1 ----> 4 print(adaga) 5 except ZeroDivisionError as e: 6 print(e) NameError: name 'adaga' is not defined
我们可以用多种方法来捕获多个异常
# 多个异常 try: #要测试的代码 1/1 print(adaga) except ZeroDivisionError as e: print(e) except NameError as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
# 多个异常 try: #要测试的代码 1/1 print(adaga) except (ZeroDivisionError, NameError) as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
# 用异常的父类代替异常的子类 try: #要测试的代码 1/1 print(adaga) # 可以捕获所有的异常 except Exception as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
# 子类不能代替父类 try: #要测试的代码 raise Exception('excetipon') except ZeroDivisionError as e: print(e) else: print('everything is ok') finally: print('无论如何都要执行这里')
捕获异常的对象,直接打印出来的信息可能不够详细,我们需要引入python提供的功能
把整个异常的详细信息拿到
import traceback try: #要测试的代码 1/0 except ZeroDivisionError as e: # print(e) print(traceback.format_exc() ) print('处理过了')
异常的传递机制有点类似于在作用域中寻找变量,一个异常产生了之后,
先在函数内部寻找是否有该异常的处理,如果没有,就往上抛一层,
如果一直抛到了全局仍没有人处理,程序就会停止运行
# 异常的传递机制 def error(): raise Exception("这里产生了一个异常") def handle(): try: error() except: print('在handle里处理') handle() try: error() except: print("全局处理一下")
4.7 魔法方法
魔法方法是批一些特殊的方法,在适当的时候被Python解释器自动调用,
比如我们前面看到的__init__函数,在我们实例对象的时候自动调用的。
其它的魔法方法也和这个类似,以两个下划线开头和以两个下划线结尾的
魔法方法有很多,这里面我们学几个最常用的
class Employee(dict): def __init__(self): super().__init__() print("init初始化") self.name = "员工字典" def __getitem__(self, key): print("获取一个item, key:%s" % key) return super().__getitem__(key) def __setitem__(self, key, value): print("设置一个员工,名字:%s, 值:%s"%(key, value)) return super().__setitem__(key, value) def __delitem__(self, key): print("删除%s"% key) return super().__delitem__(key) def __len__(self): print("员工的数量是公司机密,不能显示") return 0 def __str__(self): ret = '' for key, value in self.items(): ret += '*'*80 + '\n' ret += "姓名:%s\n" % key ret += "年龄:%s, 身高:%s\n" % value return ret def __getattr__(self, attr): print("尝试获取attr:%s, 但是没有" % attr) def __setattr__(self, attr, value): print("设置属性%s:%s" % (attr, value)) return super().__setattr__(attr, value) # 实例化对象,会调用__init__ e = Employee() # 这种设置会调用 __setitem__ e['王富贵'] = (18, 172) # 其它的更新操作不会调用 setitem e.update({'赵小姐':(26, 160)}) # 这种方法会调用__getitem__ e['王富贵'] # 这种方式访问不会调用 e.get('王富贵') print(e) # 这种删除会调用__delitem__ del e['王富贵'] # 其它的删除不会调用__delitem__ e.popitem() print(e) # 调用__len__方法 print(len(e)) # 获取已有属性,不会调用 __getattr__ e.name # 获取没有的属性,才会调用 __getattr__ e.aaa e.bbb = 'hello' e.bbb
4.8 迭代器
可以被for循环遍历的就是迭代器,我们也可以自己定义一个迭代器,只需要使用__iter__和__next__魔法方法
python解释器在试图把一个对象转换成迭代器时,会调用 对象的 __iter__方法,由__iter__方法返回一个迭代器
一个迭代器是指有一个__next__方法的对象,每次调用next就可以拿到下一个值
比如我们创建一个可迭代的斐波那契数列
# 迭代器 class Fibos: def __init__(self): self.first = 0 self.second = 1 def __next__(self): self.first, self.second = self.second, self.first + self.second return self.second def __iter__(self): return self fibs = Fibos() for f in fibs: print(f) # 如果这里不加 判断跳出循环,将会无止尽的迭代一去 if f > 100: break
上面的代码让我们自定义了一个可以用for循环遍历的可迭代对象
他还有一点不足,就是一般被for遍历的数据是有终止的,遍历完后就停下来,这样才比较实用
我们要在__next__方法引发一个StopIteration的异常来结束迭代
# 迭代器 class Fibos: def __init__(self, end): self.first = 0 self.second = 1 self.end = end def __next__(self): self.first, self.second = self.second, self.first + self.second if self.second > self.end: # 报异常后终止迭代,这个异常由python解释器处理 raise StopIteration return self.second def __iter__(self): return self for f in Fibos(1000): print(f)
4.9 生成器
生成器也是一个迭代器,不过生成器是用函数来实现的
# 生成器 def fibos(end): first = 0 second = 1 while True: first, second = second, first + second if second > end: break yield second for f in fibos(1000): print(f)
使用yield关键字让函数 返回一个值 ,函数返回一个值后,函数没有立即从栈中销毁,而是挂载起来,保存在栈中
等下一次迭代的时候,再回到函数中yield语句处继续执行,每一次迭代会从yield语句中获取一个值
直到函数不再产生yield语句 。
也可以用一行代码来实现生成器,类似于列表生成器
gen = (x**2 for x in range(1,11) ) for item in gen: print(item)
4.10 面向对象总结
1. 将相关的东西放在一起,如果设计的函数需要操作一个全局变量,最好设计一个类,把他们作为属性和方法
2. 不要让对象之间过于亲密,方法只关心其所属实例的属性,对于其它实例的状态,让它创新自己去管理。
3. 慎用继承,尤其是多重继承。有些情况下继承或带来复杂性。
4. 保持简单,让方法短小紧凑,一身而言大多数方法都能在30秒内读完并理解。至少将方法的代码长度控制在一页或一个屏幕内。

浙公网安备 33010602011771号