学习数据结构一个重要的点是数据抽象, 也是计算机人毕生所追求的理念: 将现实世界的问题抽象成形式语言, 变为计算机可读的数据. 这种数据的组织结构, 在面向对象编程(Object Oriented Programming)范式中, 被抽象成一个类, 这也是 OOP 的核心.
Python 的 OOP 和其它语言有些细微的差距, 但是 Python 本身作为一门 OOP 语言, 自然在机制的实现上不输他者. 正好这学期的 DSA 拿 Python 教学, 正好也想更系统地学习 OOP, 遂有了这篇文章的产生.
Before: 作用域与命名空间
namespace
各位肯定不陌生. 命名空间表示名称到对象的映射.
- 是个标识符, 指向不同的内存区域.
- 有它, 两个不同模块就都可以定义例如
max
, 且不会混淆 A.max
, B.max
- 命名空间是隐式创建的. Python 里莫得
namespace
关键字.
- 点后面的名称称为属性(attribute).
math.sqrt
math
是模块对象, sqrt
是模块的属性.
属性是可读可写的. 例如, 可以如下操作
1 2 3 4 5 6 7 8
| >>> import my_module >>> my_module.answer 100 >>> del my_module.answer >>> my_module.answer Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'my_module' has no attribute 'answer'
|
- 命名空间在不同时刻创建, 拥有不同的生命周期.
builtins
的 namespace 就是 Python 解释器一打开就创建的.
- 模块的全局命名空间在读取模块定义时创建(合理).
- 函数的局部命名空间: 函数调用时创建
return
/ raise ...Exception()
之后就没了(或者说, 被”遗忘”了)
大慈树王对小草神说: 把关于我的知识放到函数栈上.
- 命名空间的作用域(scope): 这个区域里可以直接访问该命名空间
- 问题: 如果 count 在局部定义了一个, 全局定义了一个, 找的是谁
- 原则: 从内向外
- 内层(局部) -> 外层闭包函数的作用域 -> 全局 -> builtins namespace
nonlocal
关键字
- 很直接, 在外层作用域里重新绑定(总之不是局部变量, 往上层找)
- 也很好理解这个错误 SyntaxError: nonlocal declaration not allowed at module level
global
关键字
实验 1: 作用域一二事
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def scope_test(): def do_local(): spam = "local spam"
def do_nonlocal(): nonlocal spam spam = "nonlocal spam"
def do_global(): global spam spam = "global spam"
spam = "test spam" do_local() print("After local assignment:", spam) do_nonlocal() print("After nonlocal assignment:", spam) do_global() print("After global assignment:", spam)
scope_test() print("In global scope:", spam)
|
1 2 3 4
| After local assignment: test spam After nonlocal assignment: nonlocal spam After global assignment: nonlocal spam In global scope: global spam
|
类
- 进入类会创建一个新的命名空间, 并将其作用于局部作用域
- 离开类定义, 将创建一个类对象.
Python 类的定义很简单, 没有额外的关键字限定.
- 属性引用:
obj.name
1 2 3 4
| class A: i = 12345 def f(self): print('a')
|
1 2 3 4
| >>> A.i 12345 >>> A.f <function A.f at 0x7fb863b1f9a0>
|
- 实例化:
instance = ClassName()
1 2 3 4 5 6 7
| class Complex: def __init__(self, realpart, imagpart): self.r = realpart self.i = imagpart
x = Complex(3.0, -4.5) x.r, x.i
|
类/实例变量
- 类变量: 所有实例共享
- 实例变量: 单独实例唯一数据
1 2 3 4
| class A: x = 10 def __init__(self, name): self.name = name
|
继承
1 2
| class DerivedClass(BaseClass): pass
|
- 派生类如果找不到某一属性, 就递归式地查找基类
- 派生类里的方法自带 C++ 里面的
virtual
关键字, 也就都是虚函数
- 继承机制有用的两个内置函数:
isinstance
: 检查一个类的实例类型
issubclass
: 检查类的继承关系
issubclass(bool, int)
True
多重继承
1 2
| class DerivedClassName(Base1, Base2, Base3): pass
|
- 搜索父类的顺序?
- DFS, 从左往右?
- 万一搜索过程中遇到相同的父类怎么办…
1 2 3 4 5 6 7 8 9 10 11 12
| class A: def method(self): print("CommonA") class B(A): pass class C(A): def method(self): print("CommonC") class D(B, C): pass
D().method()
|
- 搜索顺序: D->B->A->C->A
- 需要一套新的方法解析顺序(Method Resolution Order, MRO)算法
- 有重复, 只保留最后一个
A
?
- 然而单调性不能保证…
1 2 3 4 5 6 7 8 9 10
| class X(object): pass class Y(object): pass class A(X, Y): pass class B(Y, X): pass class C(A, B): pass
|
按照上面的遍历方式
A->X->Y->object
B->Y->X->object
C->A->B->X->Y->object
Python 的 MRO 算法采用了一种 C3 方法, 它是一种很精巧的线性化算法. 在这里就不过多讨论了.
封装与私有变量
- 没有
private
关键字
- 只有一套编码约定: 带一个下划线
_x
API 的非共有部分
- 名称改写机制(name mangling)
- 想让
__init__
调用的父类的 update, 但不被子类版本的 override
- 双下划线
__x
- 最后会改名成
_ClassName__x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Mapping: def __init__(self, iterable): self.items = [] self.__update(iterable) def update(self, iterable): for item in iterable: self.items.append(item) __update = update class MappingSubclass(Mapping): def update(self, keys, values): for item in zip(keys, values): self.items.append(item)
|
1 2
| >>> MappingSubclass._Mapping__update <function Mapping.update at 0x7ffa754cba30>
|
属性
1 2 3 4 5 6
| class A: def __init__(self, x) -> None: self._x = x def get_x(self): return self._x
|
- 有点像 C# 里的 property
- Python 里也有 property 的这样一个装饰器
1 2 3 4 5 6 7
| class A: def __init__(self, x) -> None: self._x = x @property def x(self): return self._x
|
你就可以像访问变量一样访问 x
C 风格结构体
1 2 3 4 5 6 7
| from dataclasses import dataclass
@dataclass class Employee: name: str dept: str salary: int
|
1 2 3
| >>> john = Employee('john', 'computer lab', 1000) >>> john.dept 'computer lab'
|
迭代器和生成器
参考资料
- Python 文档 - 类
- 多重继承 C3 方法