# owner 所有人
# account 账户
# balance 余额
def create_account(owner, account_number, balance=0):
return {'owner': owner, 'account_number': account_number, 'balance': balance}
# deposit 存款
# withdraw 取款
def deposit(account, amount):
account['balance'] += amount
def withdraw(account, amount):
if amount <= account['balance']:
account['balance'] -= amount
else:
print('余额不足')
# display 显示
def display(account):
print(f"户主:{account['owner']},账号:{account['account_number']},余额:{account['balance']}")15 面向对象编程
15.1 认识类与对象
函数可以将完成特定功能的代码封装,实现代码的复用与模块化。 但函数封装的是行为,与行为相关的数据仍然是散落的。
例如,用函数管理银行账户:
账户数据是字典,操作数据的函数与数据本身是分离的,这会带来一些麻烦或不便,如:
- 函数里的校验逻辑可以被绕过。
withdraw()会再取钱前检查余额是否充足,避免余额为负,但没有机制阻止你写account['balance'] = -9999,把余额直接改成负数。 - 操作函数时还要手动传入
account参数,函数本身并不知道它应该操作哪个账户。
面向对象编程(Object-Oriented Programming, OOP)可以解决上述问题。它将数据与操作数据的行为封装在一起,形成类(class)。用类可创建具体的实例(instance)。 类与实例的关系,类似于一般概念与具体个体的关系:
- “行星”是一般概念,“地球”是一个这个概念下的具体个体;
- “猫”是一般概念,学校草坪上的那只小黑猫是这个概念下的具体个体。
在编程语言的语境中:
- 类是模板,定义了对象应该有哪些属性、能执行哪些方法,但类本身不存储具体数据。
- 实例是按类的模板创建出来的实体,每个实例独立存储各自的属性值。
- 实例具有数据,称为属性(attribute)。
- 实例也可以进行一些行为,通过函数实现,实例专用的函数通常称为方法(method)。
如可以创建一个“银行账户”类,用以描述账户的共同特征:有户主、账号、余额,能存款、取款、查询余额。
“张三的账户”是一个具体的实例(instance),它有具体的户主姓名、账号和余额等属性;此账户可以进行的存款,取款,查询余额操作,是其方法。
此前学过,type(obj)返回对象obj的类型。
obj的类型,就是obj所属的类。而对象obj,就是相关类的实例。
list、str、dict 就是类,[1, 2, 3]、"hello"、{'a': 1} 就是它们的实例。 调用方法时写的 s.upper()、L.append(4),就是在调用实例的方法。
# [1,2,3]是list类的实例
print(type([1, 2, 3]))
print(type("hello"))
print(type({'a': 1}))<class 'list'>
<class 'str'>
<class 'dict'>
15.2 定义类与创建实例
Python提供内置类,也允许用户定义类。
15.2.1 __init__() 与 self
定义类使用 class 关键字,语法如下:
class 类名:
类体类名习惯使用首字母大写的驼峰命名法(如 BankAccount),以区别于函数和变量。
class BankAccount:
def __init__(self, owner, account_number):
self.owner = owner
self.account_number = account_number
self.balance = 0__init__() 是一个特殊方法,用于初始化实例(initialization)。 此方法在创建实例时自动调用,不需手动调用。
self 指代当前实例本身。 在 __init__() 中,通过 self.属性名 = 值 为实例绑定属性。
创建实例的语法为 类名(实参),实参传递给 __init__() 的形参。(不包括 self):
调用类方法时,Python 自动将实例本身作为第一个参数传入 self,因此不需要手动传递self参数。
acc1 = BankAccount('张三', '62220001')
acc2 = BankAccount('李四', '62220002')上述代码创建了两个 BankAccount 实例。 acc1 和 acc2 各自独立存储属性值:
print(acc1.owner, acc1.account_number, acc1.balance)
print(acc2.owner, acc2.account_number, acc2.balance)张三 62220001 0
李四 62220002 0
修改一个实例的属性,不影响另一个实例:
acc1.balance = 1000
print(acc1.balance)
print(acc2.balance)1000
0
15.2.2 属性与方法
类体中定义的函数称为方法。 与普通函数的区别是:方法的第一个参数是 self,调用时不需手动传入。
# 为 `BankAccount` 添加方法
class BankAccount:
def __init__(self, owner, account_number):
self.owner = owner
self.account_number = account_number
self.balance = 0
def deposit(self, amount):
'''存款'''
self.balance += amount
def withdraw(self, amount):
'''取款'''
if amount <= self.balance:
self.balance -= amount
else:
print('余额不足')
def display(self):
'''显示账户信息'''
print(f'户主:{self.owner},账号:{self.account_number},余额:{self.balance}')用 实例.方法名() 的形式调用方法。
acc = BankAccount('张三', '62220001')
acc.deposit(1000)
acc.display()
acc.withdraw(300)
acc.display()户主:张三,账号:62220001,余额:1000
户主:张三,账号:62220001,余额:700
# 取款超过余额时,方法会给出提示
acc.withdraw(1000)
acc.display()余额不足
户主:张三,账号:62220001,余额:700
方法的调用本质上是将实例作为第一个参数传入函数。
# 以下两种写法等价
acc.display()
BankAccount.display(acc)户主:张三,账号:62220001,余额:700
户主:张三,账号:62220001,余额:700
通常使用前一种写法,后一种写法有助于理解 self 的含义。
可在实例创建后动态添加属性。 但动态添加的属性不在类定义中,其他实例不会有此属性,一般不推荐使用。
acc.phone = '13800138000'
print(acc.phone)13800138000
15.3 继承
15.3.1 子类
继承允许在已有类的基础上定义新类。 已有类称为父类或基类(base),新类称为子类或派生类。 子类自动获得父类的属性和方法,还可以定义自己特有的属性和方法。
定义子类的语法:
class 子类名(父类名):
子类体下面定义 SavingsAccount(储蓄账户)继承自 BankAccount:
class SavingsAccount(BankAccount):
def __init__(self, owner, account_number, interest_rate):
super().__init__(owner, account_number)
self.interest_rate = interest_rate
def add_interest(self):
'''按利率计算并增加利息'''
interest = self.balance * self.interest_rate
self.balance += interest
print(f'利息:{interest:.2f}')SavingsAccount 自动拥有 BankAccount 的 deposit()、withdraw()、display() 方法, 同时新增了 interest_rate 属性和 add_interest() 方法:
sacc = SavingsAccount('王五', '62220003', 0.02)
sacc.deposit(10000)
sacc.add_interest()
sacc.display()利息:200.00
户主:王五,账号:62220003,余额:10200.0
15.3.2 super() 与方法重写
super() 的作用是在子类中引用父类。 在 __init__() 中,super().__init__() 调用父类的初始化方法, 避免重复编写父类的初始化代码。
子类中可以重写父类的方法,即定义与父类同名的方法,覆盖父类的实现。 重写时,可以用 super() 调用父类的方法,在此基础上增加子类特有的逻辑。
下面定义 CreditAccount(信用卡账户),它允许在透支额度内取款:
class CreditAccount(BankAccount):
def __init__(self, owner, account_number, credit_limit):
super().__init__(owner, account_number)
self.credit_limit = credit_limit
def withdraw(self, amount):
'''重写取款方法:允许在透支额度内取款'''
if amount <= self.balance + self.credit_limit:
self.balance -= amount
else:
print('超出透支额度')
def display(self):
'''重写显示方法:增加透支额度信息'''
super().display()
print(f'透支额度:{self.credit_limit}')CreditAccount 重写了 withdraw() 和 display()。 withdraw() 在父类逻辑基础上增加了透支判断; display() 用 super().display() 调用父类的显示逻辑,再追加透支额度信息。
cacc = CreditAccount('赵六', '62220004', 5000)
cacc.deposit(1000)
cacc.display()
cacc.withdraw(3000)
cacc.display()
cacc.withdraw(5000)户主:赵六,账号:62220004,余额:1000
透支额度:5000
户主:赵六,账号:62220004,余额:-2000
透支额度:5000
超出透支额度
15.4 练习
定义一个
Question类表示选择题。属性包括:题干text、选项options(列表)、正确答案answer(如'A')。方法check(self, choice)接受学生的选择,判断是否正确,返回True或False。创建几道选择题并测试。定义一个
Character类表示RPG游戏角色。属性包括:名称name、血量hp、攻击力attack_power。方法包括:attack(self, target)对目标角色造成伤害(目标血量减少)、is_alive()判断角色是否存活、show_status()显示角色状态。在第2题的基础上,创建
Character的子类Tank(坦克)和Mage(法师)。坦克有额外的shield(护盾)属性,受到攻击时先消耗护盾,再消耗血量;法师有额外的mp(魔法值)属性和fire_ball(self, target)方法(消耗魔法值,造成高额伤害)。测试各子类的特殊能力。在2,3题的基础上创建两个角色,模拟对战。