Python面向对象进阶学习


Posted in Python onMay 21, 2019

在前面的章节我们已经了解了面向对象的入门知识,知道了如何定义类,如何创建对象以及如何给对象发消息。为了能够更好的使用面向对象编程思想进行程序开发,我们还需要对Python中的面向对象编程进行更为深入的了解。

@property装饰器

之前我们讨论过Python中属性和方法访问权限的问题,虽然我们不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效。我们之前的建议是将属性命名以单下划线开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的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()

__slots__魔法

我们讲到这里,不知道大家是否已经意识到,Python是一门动态语言。通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义__slots__变量来进行限定。需要注意的是__slots__的限定只对当前类的对象生效,对子类并不起任何作用。

class Person(object):

 # 限定Person对象只能绑定_name, _age和_gender属性
 __slots__ = ('_name', '_age', '_gender')

 def __init__(self, name, age):
  self._name = name
  self._age = age

 @property
 def name(self):
  return self._name

 @property
 def age(self):
  return self._age

 @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('王大锤', 22)
 person.play()
 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()

和静态方法比较类似,Python还可以在类中定义类方法,类方法的第一个参数约定名为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关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。

我们可以使用一种叫做UML(统一建模语言)的东西来进行面向对象建模,其中一项重要的工作就是把类和类之间的关系用标准化的图形符号描述出来。关于UML我们在这里不做详细的介绍,有兴趣的读者可以自行阅读《UML面向对象设计基础》一书。

Python面向对象进阶学习

Python面向对象进阶学习

利用类之间的这些关系,我们可以在已有类的基础上来完成某些操作,也可以在已有类的基础上创建新的类,这些都是实现代码复用的重要手段。复用现有的代码不仅可以减少开发的工作量,也有利于代码的管理和维护,这是我们在日常工作中都会使用到的技术手段。

继承和多态

刚才我们提到了,可以在已有类的基础上创建新类,这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则。下面我们先看一个继承的例子。

class Person(object):
 """人"""

 def __init__(self, name, age):
  self._name = name
  self._age = age

 @property
 def name(self):
  return self._name

 @property
 def age(self):
  return self._age

 @age.setter
 def age(self, age):
  self._age = age

 def play(self):
  print('%s正在愉快的玩耍.' % self._name)

 def watch_av(self):
  if self._age >= 18:
   print('%s正在观看爱情动作片.' % self._name)
  else:
   print('%s只能观看《熊出没》.' % self._name)


class Student(Person):
 """学生"""

 def __init__(self, name, age, grade):
  super().__init__(name, age)
  self._grade = grade

 @property
 def grade(self):
  return self._grade

 @grade.setter
 def grade(self, grade):
  self._grade = grade

 def study(self, course):
  print('%s的%s正在学习%s.' % (self._grade, self._name, course))


class Teacher(Person):
 """老师"""

 def __init__(self, name, age, title):
  super().__init__(name, age)
  self._title = title

 @property
 def title(self):
  return self._title

 @title.setter
 def title(self, title):
  self._title = title

 def teach(self, course):
  print('%s%s正在讲%s.' % (self._name, self._title, course))


def main():
 stu = Student('王大锤', 15, '初三')
 stu.study('数学')
 stu.watch_av()
 t = Teacher('骆昊', 38, '老叫兽')
 t.teach('Python程序设计')
 t.watch_av()


if __name__ == '__main__':
 main()

子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写(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从语法层面并没有像Java或C#那样提供对抽象类的支持,但是我们可以通过abc模块的ABCMeta元类和abstractmethod包装器来达到抽象类的效果,如果一个类中存在抽象方法那么这个类就不能够实例化(创建对象)。上面的代码中,Dog和Cat两个子类分别对Pet类中的make_voice抽象方法进行了重写并给出了不同的实现版本,当我们在main函数中调用该方法时,这个方法就表现出了多态行为(同样的方法做了不同的事情)。

综合案例

案例1:奥特曼打小怪兽

from abc import ABCMeta, abstractmethod
from random import randint, randrange


class Fighter(object, metaclass=ABCMeta):
 """战斗者"""

 # 通过__slots__魔法限定对象可以绑定的成员变量
 __slots__ = ('_name', '_hp')

 def __init__(self, name, hp):
  """初始化方法

  :param name: 名字
  :param hp: 生命值
  """
  self._name = name
  self._hp = hp

 @property
 def name(self):
  return self._name

 @property
 def hp(self):
  return self._hp

 @hp.setter
 def hp(self, hp):
  self._hp = hp if hp >= 0 else 0

 @property
 def alive(self):
  return self._hp > 0

 @abstractmethod
 def attack(self, other):
  """攻击

  :param other: 被攻击的对象
  """
  pass


class Ultraman(Fighter):
 """奥特曼"""

 __slots__ = ('_name', '_hp', '_mp')

 def __init__(self, name, hp, mp):
  """初始化方法

  :param name: 名字
  :param hp: 生命值
  :param mp: 魔法值
  """
  super().__init__(name, hp)
  self._mp = mp

 def attack(self, other):
  other.hp -= randint(15, 25)

 def huge_attack(self, other):
  """究极必杀技(打掉对方至少50点或四分之三的血)

  :param other: 被攻击的对象

  :return: 使用成功返回True否则返回False
  """
  if self._mp >= 50:
   self._mp -= 50
   injury = other.hp * 3 // 4
   injury = injury if injury >= 50 else 50
   other.hp -= injury
   return True
  else:
   self.attack(other)
   return False

 def magic_attack(self, others):
  """魔法攻击

  :param others: 被攻击的群体

  :return: 使用魔法成功返回True否则返回False
  """
  if self._mp >= 20:
   self._mp -= 20
   for temp in others:
    if temp.alive:
     temp.hp -= randint(10, 15)
   return True
  else:
   return False

 def resume(self):
  """恢复魔法值"""
  incr_point = randint(1, 10)
  self._mp += incr_point
  return incr_point

 def __str__(self):
  return '~~~%s奥特曼~~~\n' % self._name + \
   '生命值: %d\n' % self._hp + \
   '魔法值: %d\n' % self._mp


class Monster(Fighter):
 """小怪兽"""

 __slots__ = ('_name', '_hp')

 def attack(self, other):
  other.hp -= randint(10, 20)

 def __str__(self):
  return '~~~%s小怪兽~~~\n' % self._name + \
   '生命值: %d\n' % self._hp


def is_any_alive(monsters):
 """判断有没有小怪兽是活着的"""
 for monster in monsters:
  if monster.alive > 0:
   return True
 return False


def select_alive_one(monsters):
 """选中一只活着的小怪兽"""
 monsters_len = len(monsters)
 while True:
  index = randrange(monsters_len)
  monster = monsters[index]
  if monster.alive > 0:
   return monster


def display_info(ultraman, monsters):
 """显示奥特曼和小怪兽的信息"""
 print(ultraman)
 for monster in monsters:
  print(monster, end='')


def main():
 u = Ultraman('骆昊', 1000, 120)
 m1 = Monster('狄仁杰', 250)
 m2 = Monster('白元芳', 500)
 m3 = Monster('王大锤', 750)
 ms = [m1, m2, m3]
 fight_round = 1
 while u.alive and is_any_alive(ms):
  print('========第%02d回合========' % fight_round)
  m = select_alive_one(ms) # 选中一只小怪兽
  skill = randint(1, 10) # 通过随机数选择使用哪种技能
  if skill <= 6: # 60%的概率使用普通攻击
   print('%s使用普通攻击打了%s.' % (u.name, m.name))
   u.attack(m)
   print('%s的魔法值恢复了%d点.' % (u.name, u.resume()))
  elif skill <= 9: # 30%的概率使用魔法攻击(可能因魔法值不足而失败)
   if u.magic_attack(ms):
    print('%s使用了魔法攻击.' % u.name)
   else:
    print('%s使用魔法失败.' % u.name)
  else: # 10%的概率使用究极必杀技(如果魔法值不足则使用普通攻击)
   if u.huge_attack(m):
    print('%s使用究极必杀技虐了%s.' % (u.name, m.name))
   else:
    print('%s使用普通攻击打了%s.' % (u.name, m.name))
    print('%s的魔法值恢复了%d点.' % (u.name, u.resume()))
  if m.alive > 0: # 如果选中的小怪兽没有死就回击奥特曼
   print('%s回击了%s.' % (m.name, u.name))
   m.attack(u)
  display_info(u, ms) # 每个回合结束后显示奥特曼和小怪兽的信息
  fight_round += 1
 print('\n========战斗结束!========\n')
 if u.alive > 0:
  print('%s奥特曼胜利!' % u.name)
 else:
  print('小怪兽胜利!')


if __name__ == '__main__':
 main()

案例2:扑克游戏

import random


class Card(object):
 """一张牌"""

 def __init__(self, suite, face):
  self._suite = suite
  self._face = face

 @property
 def face(self):
  return self._face

 @property
 def suite(self):
  return self._suite

 def __str__(self):
  if self._face == 1:
   face_str = 'A'
  elif self._face == 11:
   face_str = 'J'
  elif self._face == 12:
   face_str = 'Q'
  elif self._face == 13:
   face_str = 'K'
  else:
   face_str = str(self._face)
  return '%s%s' % (self._suite, face_str)
 
 def __repr__(self):
  return self.__str__()


class Poker(object):
 """一副牌"""

 def __init__(self):
  self._cards = [Card(suite, face) 
      for suite in '♠♥♣♦'
      for face in range(1, 14)]
  self._current = 0

 @property
 def cards(self):
  return self._cards

 def shuffle(self):
  """洗牌(随机乱序)"""
  self._current = 0
  random.shuffle(self._cards)

 @property
 def next(self):
  """发牌"""
  card = self._cards[self._current]
  self._current += 1
  return card

 @property
 def has_next(self):
  """还有没有牌"""
  return self._current < len(self._cards)


class Player(object):
 """玩家"""

 def __init__(self, name):
  self._name = name
  self._cards_on_hand = []

 @property
 def name(self):
  return self._name

 @property
 def cards_on_hand(self):
  return self._cards_on_hand

 def get(self, card):
  """摸牌"""
  self._cards_on_hand.append(card)

 def arrange(self, card_key):
  """玩家整理手上的牌"""
  self._cards_on_hand.sort(key=card_key)


# 排序规则-先根据花色再根据点数排序
def get_key(card):
 return (card.suite, card.face)


def main():
 p = Poker()
 p.shuffle()
 players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
 for _ in range(13):
  for player in players:
   player.get(p.next)
 for player in players:
  print(player.name + ':', end=' ')
  player.arrange(get_key)
  print(player.cards_on_hand)


if __name__ == '__main__':
 main()

说明: 大家可以自己尝试在上面代码的基础上写一个简单的扑克游戏,例如21点(Black Jack),游戏的规则可以自己在网上找一找。

案例3:工资结算系统

"""
某公司有三种类型的员工 分别是部门经理、程序员和销售员
需要设计一个工资结算系统 根据提供的员工信息来计算月薪
部门经理的月薪是每月固定15000元
程序员的月薪按本月工作时间计算 每小时150元
销售员的月薪是1200元的底薪加上销售额5%的提成
"""
from abc import ABCMeta, abstractmethod


class Employee(object, metaclass=ABCMeta):
 """员工"""

 def __init__(self, name):
  """
  初始化方法

  :param name: 姓名
  """
  self._name = name

 @property
 def name(self):
  return self._name

 @abstractmethod
 def get_salary(self):
  """
  获得月薪

  :return: 月薪
  """
  pass


class Manager(Employee):
 """部门经理"""

 def get_salary(self):
  return 15000.0


class Programmer(Employee):
 """程序员"""

 def __init__(self, name, working_hour=0):
  super().__init__(name)
  self._working_hour = working_hour

 @property
 def working_hour(self):
  return self._working_hour

 @working_hour.setter
 def working_hour(self, working_hour):
  self._working_hour = working_hour if working_hour > 0 else 0

 def get_salary(self):
  return 150.0 * self._working_hour


class Salesman(Employee):
 """销售员"""

 def __init__(self, name, sales=0):
  super().__init__(name)
  self._sales = sales

 @property
 def sales(self):
  return self._sales

 @sales.setter
 def sales(self, sales):
  self._sales = sales if sales > 0 else 0

 def get_salary(self):
  return 1200.0 + self._sales * 0.05


def main():
 emps = [
  Manager('刘备'), Programmer('诸葛亮'),
  Manager('曹操'), Salesman('荀??),
  Salesman('吕布'), Programmer('张辽'),
  Programmer('赵云')
 ]
 for emp in emps:
  if isinstance(emp, Programmer):
   emp.working_hour = int(input('请输入%s本月工作时间: ' % emp.name))
  elif isinstance(emp, Salesman):
   emp.sales = float(input('请输入%s本月销售额: ' % emp.name))
  # 同样是接收get_salary这个消息但是不同的员工表现出了不同的行为(多态)
  print('%s本月工资为: ¥%s元' %
    (emp.name, emp.get_salary()))


if __name__ == '__main__':
 main()
Python 相关文章推荐
python实现给数组按片赋值的方法
Jul 28 Python
Python的Flask框架及Nginx实现静态文件访问限制功能
Jun 27 Python
Python实现的KMeans聚类算法实例分析
Dec 29 Python
PyCharm的设置方法和第一个Python程序的建立
Jan 16 Python
python经典趣味24点游戏程序设计
Jul 26 Python
Python 日志logging模块用法简单示例
Oct 18 Python
使用Pycharm在运行过程中,查看每个变量的操作(show variables)
Jun 08 Python
Python3实现建造者模式的示例代码
Jun 28 Python
Python Tricks 使用 pywinrm 远程控制 Windows 主机的方法
Jul 21 Python
python3.9之你应该知道的新特性详解
Apr 29 Python
python使用openpyxl库读写Excel表格的方法(增删改查操作)
May 02 Python
Python 如何安装Selenium
May 06 Python
谈一谈基于python的面向对象编程基础
May 21 #Python
python字符串和常用数据结构知识总结
May 21 #Python
Opencv实现抠图背景图替换功能
May 21 #Python
python多进程读图提取特征存npy
May 21 #Python
Python中使用pypdf2合并、分割、加密pdf文件的代码详解
May 21 #Python
python+selenium实现简历自动刷新的示例代码
May 20 #Python
图文详解python安装Scrapy框架步骤
May 20 #Python
You might like
MVC模式的PHP实现
2006/10/09 PHP
Php Mssql操作简单封装支持存储过程
2009/12/11 PHP
如何在symfony中导出为CSV文件中的数据
2011/10/06 PHP
ThinkPHP中Widget扩展的两种写法及调用方法详解
2017/05/04 PHP
Thinkphp自定义生成缩略图尺寸的方法
2019/08/05 PHP
JS实现可改变列宽的table实例
2013/07/02 Javascript
JQuery记住用户名和密码的具体实现
2014/04/04 Javascript
基于javascript实现单选及多选的向右和向左移动实例
2015/07/25 Javascript
详解javascript遍历方式
2015/11/11 Javascript
javascript的正则匹配方法学习
2016/02/24 Javascript
基于javascript实现泡泡大冒险网页版小游戏
2016/03/23 Javascript
PhotoSwipe异步动态加载图片方法
2016/08/25 Javascript
jQuery使用EasyUi实现三级联动下拉框效果
2017/03/08 Javascript
详解用vue编写弹出框组件
2017/07/04 Javascript
vue写一个组件
2018/04/09 Javascript
react 组件传值的三种方法
2019/06/03 Javascript
react实现antd线上主题动态切换功能
2019/08/12 Javascript
js实现简单的日历显示效果函数示例
2019/11/25 Javascript
使用Python将数组的元素导出到变量中(unpacking)
2016/10/27 Python
Python处理XML格式数据的方法详解
2017/03/21 Python
Python利用multiprocessing实现最简单的分布式作业调度系统实例
2017/11/14 Python
python之Flask实现简单登录功能的示例代码
2018/12/24 Python
详解python 破解网站反爬虫的两种简单方法
2020/02/09 Python
在pytorch中实现只让指定变量向后传播梯度
2020/02/29 Python
TensorFlow实现批量归一化操作的示例
2020/04/22 Python
Django 构建模板form表单的两种方法
2020/06/14 Python
Python基于Faker假数据构造库
2020/11/30 Python
CSS3 Columns分列式布局方法简介
2014/05/03 HTML / CSS
详解HTML5中CSS外观属性
2020/09/10 HTML / CSS
公共事业管理本科生求职信
2013/10/07 职场文书
给儿子的表扬信
2014/01/15 职场文书
小学安全汇报材料
2014/08/14 职场文书
中学生旷课检讨书模板
2014/10/08 职场文书
堂吉诃德读书笔记
2015/06/30 职场文书
MySql开发之自动同步表结构
2021/05/28 MySQL
8个JS的reduce使用实例和reduce操作方式
2021/10/05 Javascript