Python 开发指南:设计模式、能力式、操作符重载、单例模式
能力式设计
能力式设计是所有动态语言的特性,比如,同为脚本语言的 Groovy。见:通过 Groovy 了解动态语言 - 掘金 (juejin.cn)。下面的函数 f
是这样声明的:
def f(anyone) -> None:
anyone.g()
在没有 上下文 ( Context ) 的环境下,我们不知道 anyone
是什么样的类型,也不确保它具备 g()
方法;这对于 Python 解释器也一样。anyone
参数的类型是被动态确定的,脚本语言通过牺牲了一部分性能换取了动态派发的能力。
但从积极的角度思考,我们也可以认为:f()
函数不对 anyone
做任何的约束。正因如此,我们不需要事先从顶层定义任何的接口定义规范,仅仅是 "认为" anyone
应当能够提供 g()
方法。这种 "契约式开发" 的思想非常适用于轻量级项目的敏捷开发,同时保留了可拓展性。
当然,一定要在 Python 里采用接口式编程来对类型作强约束也无可厚非。只不过代码可能会写成这个样子:
class Foo:
def g(self): raise NotImplementedError("declare a sub type and implement it.")
class Foo1(Foo):
def g(self): print("method from Foo1")
class Foo2(Foo):
def g(self): print("method from Foo2")
def f(anyone: Foo) -> None:
assert isinstance(anyone, Foo), "anyone should implement class: Foo"
anyone.g()
f(Foo1())
f(Foo2())
不带任何类型标识的变量也被人戏称为 "鸭子类型"。它的典故来自于 Python 的这一设计理念:"如果它走路像鸭子,叫声也像鸭子,那它就是一只鸭子"。
这里通过 isinstance
判断对象是否满足类型。我们还有其它手段去动态判断 ( 甚至是 ) 修改一个对象的属性和方法,见元编程部分。
面向对象编程
准确的说,Python 的类 ( class ) 更贴近其它语言的特质 ( Trait ),或者是富接口 ( Interface ) 的概念,因为 Python 的类支持多重继承,我们能通过 组合 的方式让一个类获得强大的各种功能。比如:
class Swim: # trait
def swim(self):print(f"{self.__class__.__name__} swim")
class Quack: # trait
def quack(self):print(f"{self.__class__.__name__} quack")
# 括号表示继承,允许多重继承
class Duck(Swim,Quack): pass
duck = Duck()
duck.swim() # Duck swim
duck.quack() # Duck quack
前文已经涉及了一些 Python 类定义与实例创建的内容,这里主要对细节做进一步补充。
方法接收者 self
Python 的实例方法 ( method ) 的首个参数必须为 self
,它指代被调用的对象本身,可以把它理解成是类似 Go 语言的 "方法接收者"。但在调用时,应当忽略掉 self
参数。如果方法的参数列表不带 self
参数,则需要通过一个 @staticmethod
装饰器 ( 这个符号在 Java 中称之注解,见下文 ) 将其标注为静态方法。
class Foo:
def methood(self): print("a method")
@staticmethod
def functioon(): print("a function")
foo = Foo()
foo.methood() # 调用实例方法 'methood'
# 不需要创建 'Foo' 的实例
Foo.functioon() # 调用静态方法: 'functioon'
静态方法的设计主要是考虑到了模块命名空间的规范化管理。另外,通过类的实例调用静态方法也不会报错。比如:
foo.functioon()
另一个类似的装饰器是 @classmethod
,用于标注类方法。它和 @staticmethod
的区别在于:它会携带一个 cls
参数表示类型。它可以被拿来做很多事情,比如说元编程。
*下划线前缀标识符
首先,单下划线 _xx
命名的标识符表示 模块空间或类的静态域 下的私有声明,这些声明不对外公开。比如以下声明:
def _private_func(): print("only accessible in this module")
_private_var = 100
双下划线 __xx
命名的标识符表示实例的私有属性,这些属性不对外公开。比如以下声明:
class Foo:
def __init__(self,v_):
self.__private_v = v_
def __private_method(self): print("private method.")
以 __xx__
命名的方法称之为 Python 的魔法函数,Python 利用它们来实现各种语法糖,或者是 trick 机制。到目前为止,我们使用最多的魔法函数是 __init__()
,它可以被认为是类的初始化器。其内部通过 self.xxx
声明了类实例的属性。但事实上,我们是通过元信息注入的方式实现的属性声明,见后文的元编程。
操作符重载
在 Python 提供的魔法函数中,有一部分是用于操作符重载的。这些函数名和操作符一一对应,比如:__add__()
对应 +
操作符,__sub__()
对应 -
操作符,__getitem__
对应 []
访问操作符等等,这里不一一列举。操作符重载机制极大丰富了 Python 程序的表达能力。比如:
class Pipeline:
def __init__(self, seq_):
self.seq = seq_
def __getitem__(self, lamb):
stream = [lamb(x) for x in self.seq if x is not None]
return Pipeline(stream)
def __iter__(self): return iter(self.seq)
# 这种设计可以优化,见后文的 "免费定理"。
pipe = Pipeline([1, 2, 3, 4, 5])[lambda x: x + 1][lambda x: x * 3] \
[lambda x: x * 2 if x % 2 == 0 else x] # 实现一个若为偶数则翻倍的偏函数
# [2, 4, 6, 8, 10]
print(*pipe)
这里利用了指令链接和操作符重载创建了一个符号化的数据流管道,用户可以通过紧凑的 []
操作符连续传递 lambda 表达式。管道内的数组将依次执行映射变换。
除了 __getitem__()
可以重载 x[]
操作符之外,Python 还提供了可以重载 x()
操作符的 __call__()
方法。或者说:重载了此方法的对象将变成 可调用对象 。
class Foo:
def __call__(self, *args, **kwargs):
param = kwargs.get("param", "none")
print(f"callable test:{param}")
foo = Foo()
print(callable(foo)) # True
foo(param="test") # 可传入参数
由此可见,我们熟悉的操作符,在 Python 的不同语境下可能有完全不同的语义。
单例模式
Python 的每一个 *.py
模块天然就是单例模式。比如,我们可以在第一个模块 A 内作如下定义:
class Foo: pass
foo = Foo()
然后在另一个模块 B 下仅引入 foo
这一个引用。
from moduleA import foo
如果我们在网上搜索 "Python 单例模式",通常会得到五花八门的答案。但无论如何实现,Python 的单例模式仅仅是建立在约定上的,就像能力式设计那样。本质的原因是:我们无法从根本上禁止其它用户调用构造器。
作者:花花子
来源:稀土掘金