鸭子类型(duck typing)是指一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定的,而是由"当前方法和属性的集合"决定。
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
鸭子类型重点在于关注对象行为而非对象所属类型。借用维基百科的例子,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。
任何拥有正确的"走"和"叫"方法的对象都可被该函数接受,鸭子类型因此得名。
class Dog(object):
def say(self):
print('Wang! Wang!')
class Cat(object):
def say(self):
print('Miao! Miao!')
class Cow(object):
def say(self):
print('Mow! Mow!')
animal_lst = [Dog, Cat, Cow]
for animal in animal_lst:
animal().say() # 鸭子类型,只关注行为,不关心类型,只要有相同方法的正确实现即可
类比于 Java 中的接口,主要针对两个场景:
Python 提供了 abc
模块进行基类编程,又针对常见的对象探索需求提供了常见基类,放在了 collections.abc
。典型的抽象基类编程模板如下:
import abc
class AbstractClassName(metaclass=abc.ABCMeta):
@abc.abstractmethod
def abstract_method_name(self, *args, **kwd):
pass
抽象基类能帮我们在初始化对象时检查类型与方法实现,而不是在运行时才能知道有没有错
class A(object):
def alpha(self):
print('A')
class B(A):
pass
var_b = B()
var_b.alpha() # 运行时才检查、调用
import abc
class C(metaclass=abc.ABCMeta):
@abc.abstractmethod
def alpha(self):
pass
class D(C):
pass
var_d = D() # 初始化时就会检查方法实现,没实现会报错
class E(C):
def alpha(self):
pass
var_e = E() # 实现了抽象方法,通过
因为有鸭子类型的存在,不建议使用抽象基类,开发中频繁使用抽象基类容易导致依赖逻辑复杂化,还可以使用对象的组合
isinstance
和type
方法都能判断类型,而前者会沿着继承链连续判定,后者因为有时使用==
,有时使用is
而容易出现偏差。判断类型优先选择isinstance
class A(object):
pass
class B(A):
pass
b = B()
isinstance(b,B)
isinstance(b,A) # 沿继承链检验
type(b)
type(b) == B # 检验“值”是否相等
type(b) == A
type(b) is B # 核查 id 值是否一致
type(b) is A
一般来说,当类和实例存在同名属性时,object.attribute
会按照先实例后类的顺序查找。但实际调用顺序 Method Resolution Order (MRO) 算法没那么简单,Python 从 2.0 到 3.0 经历了 DFS,BFS 到 C3 算法的演进。下面,以下面两幅图说明 DFS,BFS 存在的问题
图一使用 BFS 寻找属性不会遇到问题,但如果继续在第二幅图中使用 BFS,比如检索完 B 理应检索其父类 D,但如果恰巧 C 和 D 有同名属性,则 D 内属性就被遮蔽了
同样的,图二使用 DFS 不会有问题,但如果在图一中使用,则调用链为 A-B-D-C,检索完 B 后应检索同级父类 C,但如果 C 和 D 有同名属性,则 C 内属性会被遮蔽
因为这种两难场景,在 Python 3 中提出新式类(不用显式继承object
,解析器会默认执行)后提出了 C3 算法。具体细节公式这里不讲,写代码看一下此时问题有没有解决。
class D:
pass
class B(D):
pass
class C(D):
pass
class A(B,C):
pass
A.__mro__ # 顺序正确,且自动加上了 object
class D:
pass
class B(D):
pass
class E:
pass
class C(E):
pass
class A(B,C):
pass
A.__mro__ # 顺序正确,且自动加上了 object
说到调用链就得提一下自省机制了,所谓自省就是通过某种机制访问对象内部结构,通过魔法函数__dict__
实现。实例的属性都会放在里面,注意,通过调用链访问到的父类属性不会出现在其中,因为不属于该对象
class Person(object):
sex = 'male'
class Coder(Person):
def __init__(self, company):
self.company = company
some_coder = Coder('Google')
some_coder.__dict__
print(some_coder.sex, some_coder.__dict__) # 能调用到不代表是自己结构的一部分
Python 还提供了一个强大的函数 dir
,能够显示一切对象的内部详细结构,包括隐藏部分
dir(some_coder)
dir(Person)
子类继承父类,可能只是重写或添加了一些新方法/属性,很多父类功能可以重用,重写一边太麻烦了。这时就可以使用super()
了
class A(object):
def __init__(self, name):
self.name = name
print('A is a nice father')
class B(A):
def __init__(self, name, age):
self.age = age
super().__init__(name)
print('beautiful girl')
b = B('b',18) # 重用父类初始化方法
表面上 super()
是调用父类方法而已,但更准确的讲,super()
实际是依照 MRO 规则,调用下一个对象的方法,还是拿上文的继承链举例
class D(object):
def __init__(self):
print('D')
class B(D):
def __init__(self):
print('B')
super().__init__()
class C(D):
def __init__(self):
print('C')
super().__init__()
class A(B,C):
def __init__(self):
print('A')
super().__init__()
a = A()
A.__mro__
可以看到,虽然 B 继承了 D,但是在例子中按照 MRO 规则下一个是 C,所以 super()
先调用了 C 的构造函数,最后才是 D
虽然 Python 支持多继承,但实际开发中不推荐使用,而是应该遵循“少用继承,多用对象组合”的原则,具体到 Python 中就是 mixin (混合)模式了,类比于 Java 里的组合模式。
mixin 类提供方法给其他类使用而不建立“父子”关系,通常 Mixin 类功能单一,不和基类发生关联,参数列表里没有 Mixin 不妨碍基类初始化。当然,Mixin 里拒绝使用super()
,命名时通常以 Mixin
结尾,增强代码可读性
class SayHiMixin(object):
def say(self):
print('Hi')
class SayHelloMixin(object):
def say(self):
print('Hello')
class BaseClass(object):
pass
class MyClass(SayHelloMixin, SayHiMixin, BaseClass):
pass
obj = MyClass()
obj.say()
在访问、管理外部资源的时候,经典结构是try-except-finally
,尝试执行try
部分内容,except
捕获异常,无论怎样最终都会执行finally
,Python 里对于顺利执行时还可以加上else
。正因如此,通常try
负责访问资源,finally
负责关闭资源。
def try_exc():
try:
print('I am trying')
raise KeyError
except KeyError:
print('Get KeyError')
else:
print('If everything goes well. You will be here')
finally:
print('This is end')
try_exc()
当各部分内有return
存在时,会依次压栈,然后从finally
开始尝试寻找返回值
def try_exc_with_ret_1():
try:
return 1
except:
return -1
else:
return 2
finally:
return 3
try_exc_with_ret_1()
def try_exc_with_ret_2():
try:
return 1
except:
return -1
else:
return 2
finally:
pass
try_exc_with_ret_2()
def try_exc_with_ret_3():
try:
pass
except:
return -1
else:
return 2
finally:
pass
try_exc_with_ret_3()
def try_exc_with_ret_4():
try:
raise AttributeError
return 1
except:
return -1
else:
return 2
finally:
pass
try_exc_with_ret_4()
每次都这么写太麻烦了,Python 专门提供了 with
帮助管理上下文。要使用 with
,要遵照上下文管理协议,向类中添加两个魔法函数——__enter__()
,__exit__()
。前者访问资源,后者负责释放资源
class DemoContext:
def __enter__(self):
print('Access resource')
return self # 返回资源对象
def __exit__(self, exc_type, exc_val, exc_tb):
print('Release resource')
def do_something(self):
print('Say hello')
with DemoContext() as DC:
DC.do_something()
如果你恰巧还知道生成器(后面会写文章讲),那么上面写类实现上下文管理就不是很 Pytonic 了。Python 提供了上下文管理模块 contextlib
,通过装饰器 contextmanager
实现管理器的自动转换
import contextlib
@contextlib.contextmanager
def pretend_open_file(file_name):
print('Something before accessing resource')
yield {}
print('finish work. clear resource')
with pretend_open_file('gobble') as pf:
print('Nice work')