类是面向对象程序设计中抽象的主要方法。下面以创建 CreditCard 类作为例子讲解面向对象如何定义类。
class CreditCard:
"""有关一个用户的信用卡"""
def __init__(self, customer, bank, acnt, limit):
"""初始化一个信用卡实例
Args:
customer (str): 用户名
bank (str): 银行名
acnt (str): 用户账户ID
limit (float): 信用卡限额
"""
self._customer = customer
self._bank = bank
self._acnt = acnt
self._limit = limit
self._balance = 0. # 初始账户额度为 0
def get_customer(self):
"""返回用户名"""
return self._customer
def get_bank(self):
"""返回银行名"""
return self._bank
def get_acnt(self):
"""返回账户ID"""
return self._acnt
def get_limit(self):
"""返回额度"""
return self._limit
def charge(self, price):
"""
返回是否能继续提款,即检查是否超出额度
:param price: 希望提取的额度
:return: True 如果加上目前所用额度没有超出限度,否则 False
"""
if self._balance + price > self._limit:
return False
else:
self._balance += price
return True
def make_payment(self, amount):
"""用现金抵消了部分信用卡贷款"""
self._balance -= amountself 代表了一个实例,可以理解为对象自己。同时,self 也确定了调用方法时作用的对象。例如 obj.get_customer() 表示对实例化后的对象 obj 调用方法 get_customer() 。
在面向对象编程中,我们称位于类定义 class 之中的各种函数 def 为方法。而 __init__() 称为初始化方法,它是当实例化对象时首先被执行的。在这个例子中,参数除了对象自己 self ,也包含了 customer, bank, acnt, limit 这些都是初始化时需要传入的。
类似于初始化方法,其他的函数也是接受参数,返回结果。例如:charge() 方法,使用 obj.charge(100) 表示对于对象 obj ,传入参数 price = 100 ,然后返回一个结果。
在数据成员名称中的前加下划线,比如 _balance ,表明它被设计为非公有的(nonpublic)。类的用户不应该直接访问这样的成员。可以提供类似于 get_balance 的访问函数,以提供拥有只读访问特性的类的用户。
cc = CreditCard(customer="John Doe", bank="Nation Bank", acnt="123 456 789", limit=1000.)使用 cc = CreditCard() 并传入初始化参数的方式实例化对象。此时就可以对对象 cc 进行操作,调用方法。
CreditCard 类的实现方法不够稳健:
- 例如:没有明确地检查参数的类型,如果用户创建了一个类似于
visa.charge('candy)的调用,代码可能会崩溃。所以应该设计一些抛出异常。 - 例如:逻辑错误的。如果允许用户收取一个类似于
visa.charge(-300)的负价格,这将导致用户的余额变少,这是不和逻辑的
所以,这个例子只是介绍面向对象编程时,定义类、实例化和调用方法的基础知识。之后需要不断完善。
一般对类的定义会单独放在一个 .py 文件中,那么我们需要对类进行检查,测试其是否合理,是否报错。但是,如果通过 from ... import ... 的方法导入主程序测试显然不是我们希望的。所以,我们可以在定义类的 .py 文件下使用 if __name__ = '__main__': 的方法。
if __name__ = '__main__'::这个判断表示,只有当当前文件以主程序的方式运行时,if语句后面的内容才运行。这样可以避免在from ... import ...代入类时,连带运行if语句后的测试语句。
例如:在当前目录下创建文件夹 utils 内部放置文件 cc.py ,在 cc.py 书写类的定义,于是可以代入类
from utils.cc import CreditCard
cc_new = CreditCard("John", "Bank1", "123 456 789", 3010.)而在文件 ./utils/cc.py 中就可以书写 if __name__ = '__main__': 的测试代码
class CreditCard: ...
if __name__ == '__main__':
wallet = []
wallet.append(CreditCard("John", "Bank1", "123 456 789", 3010.))
wallet.append(CreditCard("Mike", "Bank2", "456 123 789", 6210.))
wallet.append(CreditCard("Ann", "Bank3", "789 456 123", 4010.))
print(wallet[0].charge(1000.))
print(wallet[1].charge(2000.))
print(wallet[2].charge(5000.))
for account in wallet:
print("Customer: {}".format(account.get_customer()))
print("Balance: {}".format(account.get_balance()))
if account.get_balance() > 100.:
account.make_payment(100.)
print("New Balance: {}".format(account.get_balance()))Python 的内置类为许多操作提供了自然的语义。比如,a + b 可以是数值相加,也可以是字符串相连。
默认情况下,对于新的类来说,+ 操作符是未定义的,可通过操作符重载来定义它。例如:在类的定义里定义方法 __add__() 即可定义 + 的含义。
以 a + b 为例,其中 a 和 b 均为字符串,则 a + b 能成立是因为 Python 内置类 字符串 定义了方法 __add__() 将其表达为字符串相连,于是 a + b 才会正常使用。类似地,自定义类时也可以去重载这些运算符,下面是常见的重载:
下面通过定义向量,来解释如何自定义重载。
class Vector:
"""多维向量"""
def __init__(self, d):
"""d 维度向量"""
self._coords = [0] * d # 初始化 d 维向量
def __len__(self):
"""获取维度: 重载 len(a)"""
return len(self._coords)
def __getitem__(self, k):
"""返回第 k 个维度的值: 重载 a[k]"""
return self._coords[k]
def __setitem__(self, k, v):
"""设置第 k 个维度的值为 v: 重载 a[k] = v"""
self._coords[k] = v
def __add__(self, other):
"""定义向量加法: 重载 a + b"""
if len(self) != len(other): # 此处可以直接使用重载后的定义
raise ValueError("Dimensions must be the same!")
result = Vector(len(self))
for i in range(len(self)):
result[i] = self[i] + other[i]
return result
def __sub__(self, other):
"""定义向量减法: 重载 a - b"""
if len(self) != len(other):
raise ValueError("Dimensions must be the same!")
result = Vector(len(self))
for i in range(len(self)):
result[i] = self[i] - other[i]
return result
def __eq__(self, other):
"""判断向量坐标是否相等: 重载 a == b"""
return self._coords == other._coords
def __ne__(self, other):
"""判断向量坐标是否不相等: 重载 a != b"""
return not self == other # 等价于 self.__eq__(other) 直接使用重载后的定义
def __str__(self):
"""以字符串的形式展现这个向量类: 重载 str(a) 或 a 可以以字符串形式展示"""
return '<' + str(self._coords)[1:-1] + '>'以上的方法各个方法都重载了一些运算符,具体解释见代码注释。
在上面引入类时,我们使用了 from utils.cc import CreditCard ,这并不规范。我们可以将类定义写入一个 python 文件,然后统一放在文件夹 utils 中,并且再写一个 __init__.py 文件将 utils 文件夹整个变成一个标准的 python 包,目录结构见下:
./utils
├── __init__.py
├── cc.py
└── vector.py然后在 __init__.py 文件中写入:
# coding=utf-8
from .cc import CreditCard
from .vector import Vector如此便可在其他文件中直接引用
from utils import CreditCard, Vector集合迭代器(iterator)提供了一个关键功能:它支持一个名为 __next__ 的特殊方法,如果集合有下一个元素,该方法返回该元素,否则产生一个 StopIteration 异常来表明没有下一个元素。
Python 为实现了 __len__ 和 __getitem__ 方法的的类提供了一个自动的迭代器,每次调用 __next__ 方法时便会索引递增。例如,下面的 SquenceIterator 类。
【注意】迭代器的实现必须要求【已经实现了
__len__和__getitem__方法】
class SequenceIterator:
"""为已经定义了__len__和__getitem__的对象实现迭代器方法"""
def __init__(self, seq):
"""迭代器初始化"""
self._seq = seq
self._k = -1 # 只有迭代器调用时才从 0 开始
def __next__(self):
"""迭代器"""
self._k += 1
if self._k < len(self._seq):
return self._seq[self._k] # 返回向前一步后的值
else:
raise StopIteration()
def __iter__(self):
"""一般__iter__方法都要返回自己,一种书写规范"""
return selfclass Range:
"""模拟Python的Range类: range(start, stop, step)"""
def __init__(self, start, stop=None, step=1):
"""
初始化
:param self: 对象自身
:param start: 起始数字
:param stop: 终止数字
:param step: 每步跨度
:return: Range类
"""
if step == 0:
raise ValueError("step can not be zero")
if stop is None:
# 应对输入 Range(n), 则当作 Range(0, n) 处理
start = 0
stop = start
# 计算真实长度,应对有余数的情形
self._length = max(0, (stop - start + step - 1) // step)
# 考虑到已经计算长度,故无需记录 stop
self._start = start
self._step = step
def __len__(self):
"""长度"""
return self._length
def __getitem__(self, index):
"""取数值"""
if index < 0:
index += self._length # 从后向前取
if index >= self._length or index < 0:
raise IndexError("index out of range")
return self._start + index * self._step
def __str__(self):
"""只是以列表的形式展示,不重要。因为Range是模仿Python的range功能"""
result = ""
for i in Range(self._start, self._length):
result += str(self._start + i * self._step) + ", "
result = "[" + result[:-2] + "]"
return result继承技术允许基于一个现有的类作为起点定义新的类。在面向对象的术语中,通常描述现有的类为基类 (base class)、父类 (parent class) 或者超类 (superclass),而称新定义的类为子类 (subclass 或者 child class)
子类可以覆盖父类方法,也可以在父类的基础上扩展方法。
下面以第一节中定义的 CreditCard 类为父类,定义新的子类 PredatoryCreditCard 。
我们希望实现的新功能:
- 当尝试收费由于超过信用卡额度被拒绝时,将会收取 5 美元的费用
- 将有一个对未清余额按月收取利息的机制,即基于参数年利率
apr计算利息
class PredatoryCreditCard(CreditCard):
"""继承父类,扩展方法"""
def __init__(self, customer, bank, acnt, limit, apr):
"""
继承父类,常见新的一个账户
:param customer: 用户名
:param bank: 银行名
:param acnt: 账户ID
:param limit: 额度限制
:param apr: 年利率,用以计算利息
"""
super().__init__(customer, bank, acnt, limit) # 调用父类的初始化方法 __init__
self._apr = apr
# self._balance 在调用父类初始化时也已经赋值 0
def charge(self, price):
"""覆盖父类的charge方法,添加扣除手续费功能"""
success = super().charge(price) # 调用父类方法,检查是否仍在限额内
if not success:
self._balance += 5 # 如果失败收取 5 元手续费,贷款 balance 提高
return success # 返回结果 True or False
def process_month(self):
"""收取每月利息"""
if self._balance > 0:
# 月贴现因子,apr 为年利率,故除以 12 年化
monthly_factor = pow(1 + self._apr, 1 / 12)
self._balance *= monthly_factor定义子类 B 继承父类 A 时,采用下面的格式
class B(A):
pass在子类中首先需要调用父类的初始化语句和传参
def __init__(self, param1, param2, sub_param):
super().__init__(param1, param2) # 调用父类的初始化方法 __init__
self.sub_param = sub_param # 子类的参数在子类中调用父类方法,使用 super() 代替父类对象。例如:在使用父类的方法 charge() 时,直接采用
super().charge(param1) # 传入相应参数【注意】在子类中,我们直接调用了父类的受保护数据
_balance(以_开头的数据视为受保护数据),这不是最好的方法。只是在这里,我们需要在子类中修改 balance ,但如果调用get_balance方法是无法修改 balance 的,所以只好直接修改._balance。【改进】不过可以在父类中定义非公有/受保护的方法
_set_balance()让子类能使用它改变._balance而外界则不使用这个方法。如此可以做到子类使用父类方法改变父类受保护数据,而不是直接在父类数据上改变。
Progression 类的分层如图。我们希望之后定义的各种数列均以 Progression 类为父类。
- Progression 类产生
0, 1, 2, ...的无穷数列。
- 定义
__next__, __iter__方法,实现next(obj)的方法迭代,同时也支持for i in obj的方法 - 定义
_advance方法,为更新提供了非公有方法,为未来子类不同的更新形式提供方法 - 定义
print_progression方法,方便以字符串的方式展示当前值的后 n 位数值,以序列的形式展示
class Progression:
"""
定义普适的数列父类
默认产生 0, 1, 2, ... 的无穷数列
"""
def __init__(self, start=0):
"""初始化记录起始默认值"""
self._current = start
def _advance(self):
"""
非公有方法,用于更新 self._current
为后续子类覆盖提供方法,子类不同的数列需要覆写不同的更新方法
父类默认的更新方式为 += 1
"""
self._current += 1
def __next__(self):
"""迭代下一个值,或抛出异常 StopIteration"""
if self._current is None:
raise StopIteration
else:
result = self._current
self._advance()
return result
def __iter__(self):
"""
习惯于 iter 与 next 合并使用
By convention, an iterator must return itself as an iterator.
"""
return self
def print_progression(self, n):
"""打印当前值之后的 n 个值"""
print(" ".join(str(next(self)) for _ in range(n)))注意此个例子,使用
for i in obj会进入无穷序列。
我们希望等差数列 ArithmeticProgression 类产生等差数列,只需要继承父类后,覆写 _advance 方法。
class ArithmeticProgression(Progression):
"""继承基础数列类,定义等差数列类"""
def __init__(self, increment=1, start=0):
"""
初始化等差数列
:param increment: 公差
:param start: 首项
"""
super().__init__(start)
self._increment = increment
def _advance(self):
"""覆写新更新规则"""
self._current += self._incrementaprog1 = ArithmeticProgression(4)
aprog2 = ArithmeticProgression(4, 1)
aprog1.print_progression(7)
aprog2.print_progression(7)
"""
0 4 8 12 16 20 24
1 5 9 13 17 21 25
"""类似地定义等比数列 GeometricProgression 类,需要注意首选不能为 0
class GeometricProgression(Progression):
"""等比数列"""
def __init__(self, base=2, start=1):
"""
初始化等比数列
:param base: 公比,默认为 2
:param start: 首项,不可为 0
"""
super().__init__(start)
self._base = base
def _advance(self):
"""覆写更新规则"""
self._current *= self._base定义斐波那契数列 FibonacciProgression 类。除了提供父类需要的参数首项和自己的参数第二项,还需要增加一个非公有参数记录相邻两项的差 _prev 。
class FibonacciProgression(Progression):
"""斐波那契数列"""
def __init__(self, first=0, second=1):
"""
初始化,提供第一第二项
:param first: 第一项,作为参数传给父类的 start
:param second: 第二项
"""
super().__init__(first)
self._prev = second - first
def _advance(self):
"""覆写更新规则"""
self._prev, self._current = self._current, self._prev + self._currentfcprog1 = FibonacciProgression()
fcprog2 = FibonacciProgression(1, 1)
fcprog1.print_progression(7)
fcprog2.print_progression(7)
"""
0 1 1 2 3 5 8
1 1 2 3 5 8 13
"""抽象基类:一个类的唯一目的是作为继承的基类。其中被继承的我们称之为抽象基类,而其他的类则为具体的类。
- 一般而言,抽象类不能直接实例化,而具体的类可以被实例化。
- 理论上之前的例子中, Progression 类虽然严格来说是具体的类,但我们希望把它设计为一个抽象基类。
Python 的 abc 模块提供了正式的抽象基类的定义,例如我们定义一个抽象基类 Sequence
- 其一,我们不需要这个抽象基类 Sequence 提供
__len__和__getitem__的具体实现,这两个方法的实现由继承它的子类完成。 - 其二,我们希望这个基类能完成功能:通过整数查看序列元素,所以要具体实现
__contains__index和count方法。
from abc import ABC, abstractmethod
class Sequence(ABC):
"""抽象基类 Sequence"""
@abstractmethod
def __len__(self):
"""返回序列长度,由子类实现"""
pass
@abstractmethod
def __getitem__(self, j):
"""返回第 j 位置的值,由子类实现"""
pass
def __contains__(self, val):
"""查看 val 是否在序列中,返回 True or False"""
for j in range(len(self)):
if val == self[j]:
return True
return False
def index(self, val):
"""返回 val 在序列中的下标"""
for j in range(len(self)):
if val == self[j]:
return j
raise ValueError("Value not found")
def count(self, val):
"""计数多少值等于 val"""
k = 0
for j in range(len(self)):
if val == self[j]:
k += 1
return k这一类与 Python 的 collections 模块的
collections.abc.Sequence类似。因为采用抽象定义,故类
Sequence不能被实例化。
-
继承
abc.ABC类,表明这是一个抽象基类,只能被继承不可被实例化。 -
上述的修饰器
@abstractmethod表名这个方法必须由子类实现,抽象基类无需实现。
使用 abc 模块定义过于复杂,可以直接使用 Python 的 collections 模块,它已经封装了常见的抽象基类。
- 例如在第 3.3 节实现的 Range 类,已经实现了
__len__和__getitem__方法,但没有实现抽象基类中的__contains__index和count方法。 - 而这与
collections.abc.Sequence类相匹配,可以让我们定义的Range类继承collections.abc.Sequence类,从覆写__len__和__getitem__,同时也依靠父类实现 contains、index 和 count 方法。
from collections.abc import Sequence
# 改写 Range 的定义
class Range(Sequence):
...【注意】在
Python3.x版本后collections相关抽象基类被移动到了collections.abc中。故使用时应导入from collections.abc import Sequence
【结果测试】
if __name__ == "__main__":
r = NewRange(0, 10)
print(r)
print(4 in r) # 判断 4 是否在序列中 __contains__ 方法
print(r.index(0)) # 返回下标 index 方法
print(r.count(1)) # 查看多少值等于 1
"""
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
True
0
1
"""其实采用之前我们自定义的
Sequence抽象基类也可以达到相同目的。
from abc import ABC, abstractmethod
class Sequence(ABC):
...
class Range(Sequence):
...以 CreditCard 类和 PredatoryCreditCard 类为例,展示出其类命名空间和实例命名空间如下:
类级别的数据成员:一些值(如常量)被一个类的所有实例共享
- 在这种情况下,在每个实例的命名空间中存储这个值就会造成不必要的浪费。所以使用如下格式定义这样的类数据成员。
class PredatoryCreditCard(CreditCard):
OVER_LIMIT_FEE = 5
...
def charge(self, price):
"""覆盖父类的charge方法,添加扣除手续费功能"""
success = super().charge(price) # 调用父类方法,检查是否仍在限额内
if not success:
# 使用 PredatoryCreditCard.OVER_LIMIT_FEE 方式调用类数据成员
self._balance += PredatoryCreditCard.OVER_LIMIT_FEE
return success # 返回结果 True or False嵌套类:在一个类的定义里定义另一个类。需要注意的是,二者只是嵌套关系,没有继承关系。
class A:
class B:
...Python 提供了一种直接的机制来表示实例命名空间:类定义必须提供一个名为 __slots__ 的类级别的成员分配给一个固定的字符串序列。例如,在 CreditCard 类中,声明如下:
class CreditCard:
__slots__ = '_customer', '_bank', '_acnt', '_limit', '_balance'当子类继承时,只需要声明子类新增的实例即可
class PredatoryCreditCard(CreditCard):
__slots__ = '_apr'当使用 a = b 时,Python 在做的只是拷贝了一个别名,或者说是一个地址。
当遇到不可变类型时还不会出现问题,一旦二者指向的是可变类型,例如列表。则会出现一方变,整体变。
""" 1. 简单赋值,复制了地址 """
print("1. 简单赋值,复制了地址")
a = [1, 2]
b = a
b.append(3)
print(a) # [1, 2, 3]例如:列表中写入颜色对象。 warmtones 表示现有的颜色列表。希望创建一个新的 palette 列表,复制一份 warmtones 列表。但是对 palette 不希望影响到 warmtones 。
""" 2. 浅拷贝,简历了新地址,但仍然指向同一不可变数据 """
print("2. 浅拷贝,简历了新地址,但仍然指向同一不可变数据")
warmtones = ['red', 'green', 'blue']
palette = list(warmtones) # 浅拷贝
palette[0] = 'yellow'
print(warmtones) # ['red', 'green', 'blue']
print(palette) # ['yellow', 'green', 'blue']这比直接赋值更好,即复制了一份可变数据类型 (list) ,但指向的不可变数据仍然相同。例如:warmtones[0] 和 palette[0] 为实例 _red 的别名,实际指向相同。
我们希望让 warmtones 和 palette 之间完全没有关联,就可以使用 copy.deepcopy() 方法。
import copy
""" 3. 深拷贝,完全拷贝一份 """
print("3. 深拷贝,完全拷贝一份")
warmtones = ['red', 'green', 'blue']
palette = copy.deepcopy(warmtones) # 深拷贝
palette[0] = 'yellow'
print(warmtones) # ['red', 'green', 'blue']
print(palette) # ['yellow', 'green', 'blue']








