数据结构
 2024-01-01
 1765字
 Python

逻辑结构

鸭子类型

鸭子类型是Python中一个重要的概念,借助这个概念可以实现非常好的一致性。

它来源于这样一句话——当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。这句话背后的含义是,我们不关心这个对象到底是什么,我们只关系这个对象有没有我想要的行为。

在编程语言中,这个概念是这样的——一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。不太像是人话,我们来举个例子说明一下。

抽象基类

假设我们需要编写一个函数,函数的功能是给动物喂食,如果是食肉动物,那就喂它肉,否则就喂它植物。需要喂食的动物可能有老虎、狮子、狼、豹子和大象、兔子等等。如果依次判断这个动物是老虎、狮子还是狼,函数会变得冗长丑陋。如果我们事先知道,所有的食肉动物实现了.eat_meat()方法(这是食肉动物所必须的),所有的素食动物都要实现.eat_greens()方法,那么我们只需要根据这个动物实现的方法来判定它是食肉还是食草动物。

也就是说,只要某个动物实现了.eat_meat()方法,那么它就是个食肉动物,不管食肉动物是不是它的父类。

这里的食肉动物类,就是一种抽象基类——没有任何一个类真正继承自它,但是在逻辑上,许多类都可以看做它的子类。Python中实现抽象基类的方法如下。

import abc  # abc模块,Abstract Base Class


class Carnivore(abc.ABC):
    @abc.abstractmethod
    def eat_meat(self):
        pass

    @classmethod
    def __subclasshook__(cls, subclass):
        if any('eat_meat' in x.__dict__ for x in subclass.__mro__):
            return True
        return NotImplemented


class Herbivore(abc.ABC):
    @abc.abstractmethod
    def eat_greens(self):
        pass

    @classmethod
    def __subclasshook__(cls, subclass):
        if any('eat_greens' in x.__dict__ for x in subclass.__mro__):
            return True
        return NotImplemented


class Lion:
    def eat_meat(self):
        print("I'm eating meat.")


class Rabbit:
    def eat_greens(self):
        print("I'm eating greens.")


def feed(animal):
    if isinstance(animal, Carnivore):
        animal.eat_meat()
    elif isinstance(animal, Herbivore):
        animal.eat_greens()
    else:
        print(f"Cloudn't feed a {type(animal)}!")


if __name__ == '__main__':
    feed(Lion())
    feed(Rabbit())
    feed([])

# 运行结果:
# I'm eating meat.
# I'm eating greens.
# Cloudn't feed a <class 'list'>!

继承关系

collections.abc模块提供了一些与数据结构有关的抽象基类,这些抽象基类之间的继承关系可以帮助梳理Python中的常见数据结构之间的区别和联系。其继承关系如下图。

这些抽象基类中比较基础的五个要求实现的方法及说明如下表。

抽象基类 要求实现的方法 方法说明
Container __contain__ 可以使用in操作判断元素是否在容器内
Sized __len__ 可以使用len()函数获取对象的长度
Iterable __iter__ 可以使用for in操作遍历
Collection - 除了继承的之外没有实现新方法
Reversible __reversed__ 可以用reversed()函数生成一个反向迭代器

序列类型

从存储的元素来看,序列可以分为指针序列内容序列。指针序列是指列表和元组这种,序列对象内部是可以指向任意对象的指针的序列。内容序列就是字符串这种,序列对象内部存储的就是数据内容的序列。

当然,序列也可以从抽象基类的角度,分为可变序列和不可变序列。

常用的序列类型如下表。

指针序列 内容序列
可变序列 list collections.deque bytearray memoryview
不可变序列 tuple bytes str range

列表

运算符

支持的算术运算符:++=**=

支持的比较运算符:<<=>>===!=

  • a + b:返回一个新的列表对象,内容是a的全部元素后面拼接上b的全部元素。两个原列表均不变。

  • a += b:在列表a的后面拼接上b的元素。名字'a'还是绑定在原来的列表对象上,只是这个列表对象的内容增加了。所以a += ba = a + b是有区别的。

  • a * nn * a:返回一个新的列表对象,内容是a的全部元素重复n次。n必须是整数。

  • a *= n:在列表a的后面重复拼接a的元素n - 1次。原理与+=类似。a = a * 3a *= 3是有区别的。

  • 列表支持所有的比较运算。效果就是正常的比较序列的字典序。

常用操作

调用格式 注释
s.append(x) x添加到s的末尾,时间复杂度$O(1)$。
s.insert(idx, x) x插入到idx位置之前,时间复杂度$O(n)$。慎用。
s.extend(iterable) iterable中的元素依次添加到s末尾,但效率远高于for+append
s.pop(idx=-1) idx位置的元素弹出。idx不为-1时慎用。
s.remove(x) 删除列表中第一个出现的x,不存在则报错。
s.reverse() 原地逆置列表。
s.sort(key=None, reverse=False) 参数含义见内建对象>函数>序列处理>sorted()

元组

字符串

运算符

支持的算术运算符:+*%

支持的比较运算符:<<=>>===!=

  • +*的原理和列表相同。因为是不可变对象,所以不支持+=*=
  • %是为了格式化输出,并不是取模。见格式化输出小节。
  • 比较运算符也是比较字典序。

常用操作

注意,字符串是不可变对象,涉及改变字符串内容的方法都是返回一个新的字符串。

调用格式 注释

字节序列

集合类型

映射类型

其它结构