让简单的接口接受函数

Python 有许多内置的 API,都允许我们传入某个函数来定制它的行为。这种函数可以叫作挂钩。

names = ['Socrates''Archimedes''Plato''Aristotle']
names.sort(key=len)
print(names)
>>>
['Plato','Socrates''Aristotle','Archimedes']

用函数来当作挂钩,比定义成类更简单。同时,挂钩函数与其他函数一样,都是 Python 里的头等对象,即可以像 Python 中其他值一样传递与引用。

使用函数作为挂钩时,函数一般都是无状态的函数。那么如果想要用函数来维护状态,可以考虑定义一个带有 __call__ 方法的新类,而不要用有状态的闭包来实现。

class BetterCountMissing:
	def __init__(self):
		self.added = 0
 
	def __call__(self):
		self.added += 1
		return 0
 
counter = BetterCountMissing()
result = defaultdict(counter, {"hi": 1, "oo":2})
result["gg"] += 1
result["hh"] += 1
assert counter.added == 2

通过 @classmethod 多态来构造对象

class BaseInput:
	def __init__(self):
		pass
 
	def do_something(self):
		raise NotImplementError
 
	@classmethod
	def create_input(cls, config):
		raise NotImplementError
 
class PathInput(BaseInput):
	def __init__(self, path):
		self.path = path
 
	def do_something(self):
		with open(self.path, 'r') as fp:
			yield fp.readline()
 
	@classmethod
	def create_input(cls, config):
		return cls(config["input_path"])
 
 
class BaseOutput:
	def __init__(self):
		pass
 
	def do_something(self):
		raise NotImplementError
 
	@classmethod
	def create_output(cls, config):
		raise NotImplementError
 
 
class PathOutput(BaseInput):
	def __init__(self, path):
		self.path = path
 
	def do_something(self, text):
		with open(self.path, 'a') as fp:
			fp.write(text)
 
	@classmethod
	def create_output(cls, config):
		return cls(config["output_path"])
 
 
def pipeline(input_class, output_class, config):
	input_cls = input_class.create_input(config)
	output_cls = output_class.create_output(config)
	for anything in input_cls.do_something():
		output_cls.do_something(anything)
 
 
pipeline(PathInput, PathOutput, {"input_path": "1.txt", "output_path": "2.txt"})

使用 super 初始化超类

Python 内置了 super 函数并且规定了标准的方法解析顺序(method resolution order,MRO)。super 能够确保菱形继承体系中的共同超类只初始化一次(其他案例参见第 48 条)。MRO 可以确定超类之间的初始化顺序,它遵循 C3 线性化(C3 linearization)算法。

优雅的用多重继承来封装逻辑

如果既要通过多重继承来方便地封装逻辑,又想避开可能出现的问题,那么就应该把有待继承的类写成 mix-in 类。这种类只提供一小套方法给子类去沿用,而不定义自己实例级别的属性,也不需要 __init__ 构造函数。

例如,定义一个可以实现转换为字典形式的 to_dict 的超类。

class ToDictMinin:
	def to_dict(self):
		return self._traverse_dict(self.__dict__)
 
def _traverse_dict(self, instance_dict):
	output={}
	for key,value in instance_dict.items():
		output[key] = self._traverse(key,value)
	return output
 
def _traverse(self,key,value):
	if isinstance(value,ToDictMixin):
		return value.to_dict()
	elif isinstance(value,dict):
		return self._traverse_dict(value)
	elif isinstance(value,list):
		return [self._traverse(key,i) for i in value]
	elif hasattr(value, '__dict__'):
		return self._traverse_dict(value.__dict__)
	else:
		return value

可以创建基于该超类的二叉树,那么这个二叉树就拥有了 to_dict 功能。这种继承方式避免了多重继承时可能引发的一些问题,如 __init__ 函数内使用 super 时混乱的初始化顺序。

class BinaryTree(ToDictMixin):
	def __init__(self,value,left=None,right=None):
		self.value=value
		self.left=left
		self.right= right
 
tree = BinaryTree(10,
	left=BinaryTree(7,right=BinaryTree(9)),
	right=BinaryTree(13,left=BinaryTree(11)))
print(tree.to_dict())

如果子类要定制(或者说修改)mix-in 所提供的功能,那么可以在自己的代码里面覆盖相关的实例方法。

通过将多个 mix-in 提供的功能组合起来,就可以实现更复杂的功能。

Python 类的 public 与 private 属性

Python 类的属性只有两种访问级别,也就是 public 与 private。

class MyObject:
	def __init__(self):
		self.public_field=5
		self.__private_field=10  # 双下划线 —— private 属性
 
	def get_private_field(self):
		return self.__private_field

private 属性只能给自己的类使用,子类无法访问超类的 private 属性。

class MyParentObject:
	def __init__(self):
		self.__private_field=71
 
class MyChildObject(MyParentObject):
	def get_private_field(self):
		return self.__private_field
 
baz = MyChildObject()
baz.get_private_field()    # Error!

而这种防止访问的方式是通过变换属性名称来实现的。

当 Python 编译器看到 MyChildObject.get_private_field 这样的方法想要访问 __private_field 属性时,它会把下划线和类名加在这个属性名称的前面,所以代码实际上访问的是 _MyChildObject__private_field。在上面的例子中,__private_field 是在 MyParentObject 的 __init__ 里面定义的,所以,它变换之后的真实名称是 _MyParentObject__private_field。子类不能通过 __private_field 来访问这个属性,因为这样写实际上是在访问不存的 _MyChildObject__private_field,而不是 _MyParentObject__private_field

了解名称变换规则后,我们就可以从任何一个类里面访问 private 属性。无论是子类还是外部的类,都可以不经许可就访问到这些属性。

assert baz._MyParentObject__private_field == 71

从 collections.abc 继承来辅助补充容器功能

Python 本身提供了一些内置的容器类型,例如 list、tuple、set、dict 等,也可以用来管理数据。通过继承这些内置容器进行继承,可以拥有这些内置容器的功能。

例如下面的 FrequencyList 类继承自 list,并增加了 frequency 方法来统计每个元素的数目。

class FrequencyList(list):
	def __init__(self,members):
	super().__init__(members)
 
	def frequency(self):
		counts ={}
		for item in self:
			counts[item]=counts.get(item,0) +1
		return counts

然而,有时候并不想继承 list,但拥有 list 中的“根据下标访问元素”、“获取对象长度”的功能。

针对“根据下标访问元素”功能,Python 实际上是调用对象的 __getitem__(idx) 函数。针对“获取对象长度”功能,Python 实际上调用对象的 __len__ 函数。因此,假如设计了类 Obj,那么实现了 __getitem__(idx) 函数和 __len__ 函数,就可以拥有 list 中的“根据下标访问元素”、“获取对象长度”的功能。

class Obj:
	def __init__(self):
		self.data = [1, 3, 4]
 
	def __getitem__(self, index):  # 这里没做异常处理
		return self.data[index]
 
	def __len__(self):
		return len(self.data)
 
obj = Obj()
print(obj[1])
print(len(obj))

但是,有时候不清楚应该要实现哪些函数