Python3入门到精通——深入类和对象

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

鸭子类型

鸭子类型(duck typing)是指一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定的,而是由"当前方法和属性的集合"决定。

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型重点在于关注对象行为而非对象所属类型。借用维基百科的例子,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。

任何拥有正确的"走"和"叫"方法的对象都可被该函数接受,鸭子类型因此得名。

In [1]:
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!')
In [2]:
animal_lst = [Dog, Cat, Cow]
for animal in animal_lst:
    animal().say()  # 鸭子类型,只关注行为,不关心类型,只要有相同方法的正确实现即可
Wang! Wang!
Miao! Miao!
Mow! Mow!

抽象基类

类比于 Java 中的接口,主要针对两个场景:

  1. 判定一个对象是不是某种类型
  2. 强制要求子类实现某个方法

Python 提供了 abc 模块进行基类编程,又针对常见的对象探索需求提供了常见基类,放在了 collections.abc。典型的抽象基类编程模板如下:

import abc

class AbstractClassName(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def abstract_method_name(self, *args, **kwd):
        pass

抽象基类能帮我们在初始化对象时检查类型与方法实现,而不是在运行时才能知道有没有错

In [3]:
class A(object):
    def alpha(self):
        print('A')

class B(A):
    pass

var_b = B()
var_b.alpha()  # 运行时才检查、调用
A
In [5]:
import abc

class C(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def alpha(self):
        pass
In [6]:
class D(C):
    pass

var_d = D()  # 初始化时就会检查方法实现,没实现会报错
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-3a4e435b40bb> in <module>
      2     pass
      3 
----> 4 var_d = D()  # 初始化时就会检查方法实现,没实现会报错

TypeError: Can't instantiate abstract class D with abstract methods alpha
In [8]:
class E(C):
    def alpha(self):
        pass

var_e = E()  # 实现了抽象方法,通过

因为有鸭子类型的存在,不建议使用抽象基类,开发中频繁使用抽象基类容易导致依赖逻辑复杂化,还可以使用对象的组合

isinstance 和 type

isinstancetype方法都能判断类型,而前者会沿着继承链连续判定,后者因为有时使用==,有时使用is而容易出现偏差。判断类型优先选择isinstance

In [2]:
class A(object):
    pass

class B(A):
    pass

b = B()
In [3]:
isinstance(b,B)
Out[3]:
True
In [4]:
isinstance(b,A)  # 沿继承链检验
Out[4]:
True
In [9]:
type(b)
Out[9]:
__main__.B
In [5]:
type(b) == B  # 检验“值”是否相等
Out[5]:
True
In [6]:
type(b) == A
Out[6]:
False
In [7]:
type(b) is B  # 核查 id 值是否一致
Out[7]:
True
In [8]:
type(b) is A
Out[8]:
False

类和实例属性查找顺序

一般来说,当类和实例存在同名属性时,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 算法。具体细节公式这里不讲,写代码看一下此时问题有没有解决。

In [1]:
class D:
    pass

class B(D):
    pass

class C(D):
    pass

class A(B,C):
    pass
In [2]:
A.__mro__  # 顺序正确,且自动加上了 object
Out[2]:
(__main__.A, __main__.B, __main__.C, __main__.D, object)
In [3]:
class D:
    pass

class B(D):
    pass

class E:
    pass
    
class C(E):
    pass

class A(B,C):
    pass
In [4]:
A.__mro__  # 顺序正确,且自动加上了 object
Out[4]:
(__main__.A, __main__.B, __main__.D, __main__.C, __main__.E, object)

Python 自省机制

说到调用链就得提一下自省机制了,所谓自省就是通过某种机制访问对象内部结构,通过魔法函数__dict__实现。实例的属性都会放在里面,注意,通过调用链访问到的父类属性不会出现在其中,因为不属于该对象

In [5]:
class Person(object):
    sex = 'male'

class Coder(Person):
    def __init__(self, company):
        self.company = company
In [6]:
some_coder = Coder('Google')
some_coder.__dict__
Out[6]:
{'company': 'Google'}
In [7]:
print(some_coder.sex, some_coder.__dict__)  # 能调用到不代表是自己结构的一部分
male {'company': 'Google'}

Python 还提供了一个强大的函数 dir,能够显示一切对象的内部详细结构,包括隐藏部分

In [8]:
dir(some_coder)
Out[8]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'company',
 'sex']
In [9]:
dir(Person)
Out[9]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'sex']

Super 做了什么

子类继承父类,可能只是重写或添加了一些新方法/属性,很多父类功能可以重用,重写一边太麻烦了。这时就可以使用super()

In [1]:
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')
In [2]:
b = B('b',18)  # 重用父类初始化方法
A is a nice father
beautiful girl

表面上 super() 是调用父类方法而已,但更准确的讲,super() 实际是依照 MRO 规则,调用下一个对象的方法,还是拿上文的继承链举例

In [8]:
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__()
In [9]:
a = A()
A
B
C
D
In [7]:
A.__mro__
Out[7]:
(__main__.A, __main__.B, __main__.C, __main__.D, object)

可以看到,虽然 B 继承了 D,但是在例子中按照 MRO 规则下一个是 C,所以 super() 先调用了 C 的构造函数,最后才是 D

Mixin

虽然 Python 支持多继承,但实际开发中不推荐使用,而是应该遵循“少用继承,多用对象组合”的原则,具体到 Python 中就是 mixin (混合)模式了,类比于 Java 里的组合模式。

mixin 类提供方法给其他类使用而不建立“父子”关系,通常 Mixin 类功能单一,不和基类发生关联,参数列表里没有 Mixin 不妨碍基类初始化。当然,Mixin 里拒绝使用super(),命名时通常以 Mixin 结尾,增强代码可读性

In [10]:
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
In [11]:
obj = MyClass()
obj.say()
Hello

上下文管理器

在访问、管理外部资源的时候,经典结构是try-except-finally,尝试执行try部分内容,except捕获异常,无论怎样最终都会执行finally,Python 里对于顺利执行时还可以加上else。正因如此,通常try负责访问资源,finally负责关闭资源。

In [12]:
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')
In [13]:
try_exc()
I am trying
Get KeyError
This is end

当各部分内有return存在时,会依次压栈,然后从finally开始尝试寻找返回值

In [14]:
def try_exc_with_ret_1():
    try:
        return 1
    except:
        return -1
    else:
        return 2
    finally:
        return 3
        
try_exc_with_ret_1()
Out[14]:
3
In [15]:
def try_exc_with_ret_2():
    try:
        return 1
    except:
        return -1
    else:
        return 2
    finally:
        pass
        
try_exc_with_ret_2()
Out[15]:
1
In [16]:
def try_exc_with_ret_3():
    try:
        pass
    except:
        return -1
    else:
        return 2
    finally:
        pass
        
try_exc_with_ret_3()
Out[16]:
2
In [17]:
def try_exc_with_ret_4():
    try:
        raise AttributeError
        return 1
    except:
        return -1
    else:
        return 2
    finally:
        pass
        
try_exc_with_ret_4()
Out[17]:
-1

每次都这么写太麻烦了,Python 专门提供了 with 帮助管理上下文。要使用 with,要遵照上下文管理协议,向类中添加两个魔法函数——__enter__(),__exit__()。前者访问资源,后者负责释放资源

In [19]:
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()
Access resource
Say hello
Release resource

如果你恰巧还知道生成器(后面会写文章讲),那么上面写类实现上下文管理就不是很 Pytonic 了。Python 提供了上下文管理模块 contextlib,通过装饰器 contextmanager 实现管理器的自动转换

In [20]:
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')
Something before accessing resource
Nice work
finish work. clear resource