本文最后更新于:星期四, 一月 14日 2021, 4:14 凌晨

方便的数据结构之

namedtuple 与 dataclass 以及 类结构进阶的基本使用

目录

其中,nametupledataclasses 个人觉得比较相似,这两个都是用来保存数据的,我们一起来看看区别吧。

在python内置模块 collections 中,有一个类为 nametuple 看名字我们可以大概猜出意思,有名字的元组。那么,这个 namedtuple 到底能做什么呢?
我们通过代码来看一下,

namedtuple

from collections import namedtuple  # 导入模块
food = namedtuple("Foods", ["fruit", "price"])  # 初始化一个对象,但是并不能直接使用。我们需要向其中加入数据。
food
__main__.Foods
data_1 = food(fruit="apple", price=123)
data_1
Foods(fruit='apple', price=123)
# 访问数据
print(data_1.fruit)
print(data_1.price)
apple
123
# 尝试更改数据
data_1.fruit = "banana"
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-5-acfec753951a> in <module>
      1 # 尝试更改数据
----> 2 data_1.fruit = "banana"


AttributeError: can't set attribute

基本操作就是如上,可以看到我们通过 namedtuple 可以获得一个对象,并且可以通过属性访问.

并且与元组 tuple 相同,不能更改属性,也就是对象一旦创立,遍不能更改,即不可变性依然保持着。

那么问题来了,这玩意创建步骤也比较麻烦。到底有什么用呢?

提高代码可读性!!!

你没看错,就是提高代码可读性。
举个栗子:
你需要用一个数据结构保存不会变的东西,我们假设这个东西是水果。这个东西需要保存两个值,假设值分别为: apple 13。
我们看看元组怎么做:

data = ("apple", 13)
data
('apple', 13)

emmm….对,很简单!一步就初始化出来了,但是我们要访问怎么办呢?这时候就只能用索引或者遍历了,如下:

print(data[0])
print(data[1])
for d in data:
    print(d)
apple
13
apple
13

这时候就有问题了,如果这是你自己写的代码还好,知道数据在哪个位置(但是数据多了,有时也会忘记),但是如果是别人来看的话,可能就比较懵逼了,需要不断的查看代码段。
别怕,我们有 namedtuple ,来,我们看看 namedtuple 有什么特效。
数据,两个值,那么 这两个值肯定有含义的对吧, 对吧?(不要抬杠哦!), 我们假设含义是 水果种类 和 价格
那么我们就可以取两个名字,为了方便认识,我们就叫 fruitname吧。(不要告诉我你要用 a,b 命名。。如果打算这样命名,还是直接用元组吧。)

# 首先呢,我们创建一个不可变的容器,这容器就叫 Fruit 吧,这个 Fruit 就保存两个值,取名如上
super_fruit = namedtuple("Fruit", ["name", "price"])  # 创建容器
# 保存数据 gogogo
data = super_fruit(name="apple", price=13)
# 一个不够,再来一个
data2 = super_fruit(name="big apple", price=26)
# 继续访问数据
print(data.name, data.price)
print(data2.name, data2.price)
apple 13
big apple 26

划重点 (namedtuple)

通过 namedtuple 我们也能得到不可变的数据结构,并且可以通过属性来进行访问,大大提高了代码的可读性,并且更加 pythonic 。
如有元组的情况,如果结构比较复杂的话,强烈推荐使用哦!
namedtuple 进阶

这个特殊的东西我们就先打住,我们换一个新东西 3.6 中的新模块 dataclasses 中的 dataclass(没错,这个也是用来保存数据的,并且可以数据可变)。

dataclass 用来干什么, 老规矩,我们先设想一个场景-用类来保存属性,通过属性来访问值,我们用正常的代码来看看。

dataclass

# 假设需要一个对象类,代表测试环境数据库的连接,我们需要给一堆属性用于配置, 并添加默认参数
class TestEnv:
    def __init__(self, port=3306, host="localhost", db="test_database", tb_name="table_name"):
        self.port = port
        self.host = host
        self.db = db
        self.tb_name = tb_name
"""当然,你也可以这样写:
class TestEnv:
    def __init__(self):
        self.port = 3306
        self.host = "localhost"
        self.db = "test_database"
        self.tb_name = "table_name"
"""
# 我们来点 pythonic 的写法,加上类型注释
class PythonicTestEnv:
    def __init__(self, port: int=3306, host: str="localhost", db: str="test_database", tb_name: str="table_name"):
        self.port = port
        self.host = host
        self.db = db
        self.tb_name = tb_name

我们实例化对象后查看一下属性

env = TestEnv()
print(env.port)
print(env.tb_name)

env = PythonicTestEnv()
print(env.port)
print(env.tb_name)
3306
table_name
3306
table_name

当然,属性与 namedtuple 不同,是可以改变的。

env.port = 3308
print(env.port)
env
3308
>>> <__main__.PythonicTestEnv at 0x104092be0>

所有要保证数据的不可变性的话,还是推荐使用 namedtuple, 但是对于这种纯数据的对象这样写比较繁琐,所以,何不试试新方法~ go!

from dataclasses import dataclass
@dataclass
class DCTestEnv:
    port: int=3306
    host: str="localhost"
    db: str="test_database"
    tb_name: str="table_name"
env = DCTestEnv()
print(env.port)
print(env.tb_name)
env
3306
table_name
>>>  DCTestEnv(port=3306, host='localhost', db='test_database', tb_name='table_name')

看,是不是一气呵成,简单方便。
细心的同学可能会发现,当对象在交互模式出现时,输出的结果不一样!
没错 dataclass 还帮我们把 __repr__ 也重写好了(划重点)!是不是很方便!没错 dataclass 最重要的就是 省代码!省代码!省代码! 重要的事说三遍!方便快捷,选他没错!

但其实不止这些方法,dataclass 还帮我们重写了 __eq__ 什么的,我们也可以重写这些方法。

好了,小技巧引入完了,我们来进入正题,面向对象知识的进阶!

Python对象模型

这里我们通过两个对象来引入:

  • 卡牌
  • 向量

卡牌对象(FrenchDeck)

这是一副扑克,记录了扑克的所有卡牌。

  • 有一个 _cards 属性保存了所有的卡牌,每一张卡牌只有花色和卡牌大小

要是你会怎么设计这个对象呢?
我们来看看常规思路。

  • 因为卡牌比较多,所以这个 _cards 肯定是循环生成的。
  • 因为每张卡牌固定有两个属性,所以我们用不可变对象来保存能更节省空间。
  • 不可变对象,要保存花色和卡牌,我们可以用字符串或者元组来实现。
  • 但是字符串肯定不太合适,花色和大小相关度不是很高,也不便于维护。
  • 所以我们用元组来实现。

代码如下:

class FrenchDeck:
    ranks = [str(i) for i in range(2, 11)] + list('JDKA')
    suits = ["黑桃", "方块", "梅花", "红桃"]
    def __init__(self):
        self._cards = [(suit, rank) for suit in self.suits
                     for rank in self.ranks]

我们实例化对象看看效果

puke_cards = FrenchDeck()
puke_cards._cards
[('黑桃', '2'),
 ('黑桃', '3'),
 ('黑桃', '4'),
 ('黑桃', '5'),
 ('黑桃', '6'),
 ('黑桃', '7'),
 ('黑桃', '8'),
 ('黑桃', '9'),
 ('黑桃', '10'),
 ('黑桃', 'J'),
 ('黑桃', 'D'),
 ('黑桃', 'K'),
 ('黑桃', 'A'),
 ('方块', '2'),
 ('方块', '3'),
 ('方块', '4'),
 ('方块', '5'),
 ('方块', '6'),
 ('方块', '7'),
 ('方块', '8'),
 ('方块', '9'),
 ('方块', '10'),
 ('方块', 'J'),
 ('方块', 'D'),
 ('方块', 'K'),
 ('方块', 'A'),
 ('梅花', '2'),
 ('梅花', '3'),
 ('梅花', '4'),
 ('梅花', '5'),
 ('梅花', '6'),
 ('梅花', '7'),
 ('梅花', '8'),
 ('梅花', '9'),
 ('梅花', '10'),
 ('梅花', 'J'),
 ('梅花', 'D'),
 ('梅花', 'K'),
 ('梅花', 'A'),
 ('红桃', '2'),
 ('红桃', '3'),
 ('红桃', '4'),
 ('红桃', '5'),
 ('红桃', '6'),
 ('红桃', '7'),
 ('红桃', '8'),
 ('红桃', '9'),
 ('红桃', '10'),
 ('红桃', 'J'),
 ('红桃', 'D'),
 ('红桃', 'K'),
 ('红桃', 'A')]

emmmm..有点感觉,我们试着随机访问几个元素看看

from random import randint

for i in range(3):
    card = puke_cards._cards[randint(0, 53)]
    print(card, card[0], card[1])
('梅花', '7') 梅花 7
('梅花', 'J') 梅花 J
('方块', 'J') 方块 J

元素比较少,还能勉强猜出意思。我们用刚学的 nametuple 来看看。

nametuple 参与创建卡牌

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = '黑桃 方块 梅花 红桃'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                       for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]
print(FrenchDeck.ranks)  # 生成需要的卡牌列表
FrenchDeck.suits  # 卡牌花色
 ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
 >>> ['黑桃', '方块', '梅花', '红桃']
fcards = FrenchDeck()  # 实例化对象
fcards._cards  # 查看以下 nametuple 的出来的卡牌,是不是更加直观好看
[Card(rank='2', suit='黑桃'),
 Card(rank='3', suit='黑桃'),
 Card(rank='4', suit='黑桃'),
 Card(rank='5', suit='黑桃'),
 Card(rank='6', suit='黑桃'),
 Card(rank='7', suit='黑桃'),
 Card(rank='8', suit='黑桃'),
 Card(rank='9', suit='黑桃'),
 Card(rank='10', suit='黑桃'),
 Card(rank='J', suit='黑桃'),
 Card(rank='Q', suit='黑桃'),
 Card(rank='K', suit='黑桃'),
 Card(rank='A', suit='黑桃'),
 Card(rank='2', suit='方块'),
 Card(rank='3', suit='方块'),
 Card(rank='4', suit='方块'),
 Card(rank='5', suit='方块'),
 Card(rank='6', suit='方块'),
 Card(rank='7', suit='方块'),
 Card(rank='8', suit='方块'),
 Card(rank='9', suit='方块'),
 Card(rank='10', suit='方块'),
 Card(rank='J', suit='方块'),
 Card(rank='Q', suit='方块'),
 Card(rank='K', suit='方块'),
 Card(rank='A', suit='方块'),
 Card(rank='2', suit='梅花'),
 Card(rank='3', suit='梅花'),
 Card(rank='4', suit='梅花'),
 Card(rank='5', suit='梅花'),
 Card(rank='6', suit='梅花'),
 Card(rank='7', suit='梅花'),
 Card(rank='8', suit='梅花'),
 Card(rank='9', suit='梅花'),
 Card(rank='10', suit='梅花'),
 Card(rank='J', suit='梅花'),
 Card(rank='Q', suit='梅花'),
 Card(rank='K', suit='梅花'),
 Card(rank='A', suit='梅花'),
 Card(rank='2', suit='红桃'),
 Card(rank='3', suit='红桃'),
 Card(rank='4', suit='红桃'),
 Card(rank='5', suit='红桃'),
 Card(rank='6', suit='红桃'),
 Card(rank='7', suit='红桃'),
 Card(rank='8', suit='红桃'),
 Card(rank='9', suit='红桃'),
 Card(rank='10', suit='红桃'),
 Card(rank='J', suit='红桃'),
 Card(rank='Q', suit='红桃'),
 Card(rank='K', suit='红桃'),
 Card(rank='A', suit='红桃')]

同样我们访问元素看看

for i in range(3):
    card = fcards._cards[randint(0, 53)]
    print(card, card.rank, card.suit)
Card(rank='5', suit='方块') 5 方块
Card(rank='5', suit='黑桃') 5 黑桃
Card(rank='J', suit='红桃') J 红桃

通过属性访问,是不是可读性提高很多了呢?

奇妙的对象模型

神奇的魔术方法(magic mthod) 或者 双下方法(dunder method)

今天我们介绍两个简单的魔术方法,因为魔术方法很多以后会慢慢添加。

  • __len__
  • __getitem__

可以看名字直接猜猜意思哦!

__len__

# len:
class A:
    def __len__(self):
        print("Attention __len__ is called!!!")
        return 12

其实看名字我们就能猜出个八九不离十,肯定和长度有关嘛。首先随便定义一个类,看看有什么神奇的效果!

test_len = A()
len(test_len)
Attention __len__ is called!!!
>>>  12

没错,其实 len(object) 时,就是重载了 object.__len__方法,不过用 len(obj) 看起来更加优雅哦。
next one!

__getitem__

class B:
    def __getitem__(self, item):
        print(item)
        return "Attention item is calling"
B()[0]
0
>>> 'Attention item is calling'

没错 __getitem__ 就是当索引对象时重载的方法。
当然,我们也可以传一些奇怪的索引给对象!

B()["pythonic!"]
pythonic!
>>>  'Attention item is calling'

!没错!这就变成字典的索引!是不是很神奇呢?
但是有些时候重写这两个方法也不是一件容易的事,但是我们可以偷偷懒。
如同定义 FrenchDeck 的操作。
利用原对象的特性!我们让 FrenchDeck 也具有了长度和索引的技能!

len(fcards)
52
fcards[randint(0, 53)]
Card(rank='J', suit='红桃')

如果自定义对象也需要这两个方法的时候,可以重点研究尝试一下!

总结

学到哪,总结到哪!我们简单回顾一下我们学的:

一些额外的补充

  • 一些常见的运算魔术方法。

  • namedtuple 的高阶使用。

本文地址: https://dustyposa.github.com/posts/6f4644c4/