Python OOP Cookbook

学习数据结构一个重要的点是数据抽象, 也是计算机人毕生所追求的理念: 将现实世界的问题抽象成形式语言, 变为计算机可读的数据. 这种数据的组织结构, 在面向对象编程(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

1
2
class ClassName:
pass
  • 进入类会创建一个新的命名空间, 并将其作用于局部作用域
  • 离开类定义, 将创建一个类对象.

Python 类的定义很简单, 没有额外的关键字限定.

  • 类对象支持两种操作
  1. 属性引用: 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>
  1. 实例化: instance = ClassName()
  • 构造函数 __init__()
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 # class variable
def __init__(self, name): # instance variable
self.name = name

继承

1
2
class DerivedClass(BaseClass):
pass
  • 派生类如果找不到某一属性, 就递归式地查找基类
  • 派生类里的方法自带 C++ 里面的 virtual 关键字, 也就都是虚函数
  • 继承机制有用的两个内置函数:
    • isinstance: 检查一个类的实例类型
      • isinstance(2, int) True
    • 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
    • A 在前, 所以输出的是 A 的 method.
  • 需要一套新的方法解析顺序(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
    • 等等, B 怎么先去遍历 X 了…

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)
# _Mapping__update
__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

1
2
3
>>> a = A(2)
>>> a.x
2

C 风格结构体

  • dataclass 的一个装饰器
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'

迭代器和生成器

参考资料

  1. Python 文档 - 类
  2. 多重继承 C3 方法