RealPython 中文系列教程(六十四)

原文:RealPython

协议:CC BY-NC-SA 4.0

Python 3.7+中的数据类(指南)

原文:https://realpython.com/python-data-classes/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 在 Python 中使用数据类

Python 3.7 中一个令人兴奋的新特性是数据类。数据类通常主要包含数据,尽管实际上没有任何限制。它是使用新的@dataclass装饰器创建的,如下所示:

from dataclasses import dataclass

@dataclass
class DataClassCard:
    rank: str
    suit: str

**注意:**这段代码,以及本教程中的所有其他例子,将只在 Python 3.7 和更高版本中工作。

数据类带有已经实现的基本功能。例如,您可以立即实例化、打印和比较数据类实例:

>>> queen_of_hearts = DataClassCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
DataClassCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == DataClassCard('Q', 'Hearts')
True

与普通班级相比。最小的常规类应该是这样的:

class RegularCard:
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

虽然没有更多的代码要写,但是您已经可以看到样板文件之痛的迹象:ranksuit都重复了三次,只是为了初始化一个对象。此外,如果您尝试使用这个普通的类,您会注意到对象的表示不是非常具有描述性,并且由于某种原因,红心皇后与红心皇后不同:

>>> queen_of_hearts = RegularCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
<__main__.RegularCard object at 0x7fb6eee35d30>
>>> queen_of_hearts == RegularCard('Q', 'Hearts')
False

似乎数据类正在幕后帮助我们。默认情况下,数据类实现了一个 .__repr__()方法来提供良好的字符串表示,还实现了一个.__eq__()方法来进行基本的对象比较。为了让RegularCard类模仿上面的数据类,您还需要添加这些方法:

class RegularCard
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __repr__(self):
        return (f'{self.__class__.__name__}'
                f'(rank={self.rank!r}, suit={self.suit!r})')

    def __eq__(self, other):
        if other.__class__ is not self.__class__:
            return NotImplemented
        return (self.rank, self.suit) == (other.rank, other.suit)

在本教程中,您将确切了解数据类提供了哪些便利。除了漂亮的表示和比较,您还会看到:

  • 如何向数据类字段添加默认值
  • 数据类如何允许对象排序
  • 如何表示不可变数据
  • 数据类如何处理继承

我们将很快深入研究数据类的这些特性。然而,你可能会想你以前已经见过类似的东西了。

免费下载: 从 Python 技巧中获取一个示例章节:这本书用简单的例子向您展示了 Python 的最佳实践,您可以立即应用它来编写更漂亮的+Python 代码。

数据类的替代方案

对于简单的数据结构,你可能已经使用过tupledict。您可以用以下任何一种方式代表红心皇后牌:

>>> queen_of_hearts_tuple = ('Q', 'Hearts')
>>> queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'}

它工作了。然而,作为一名程序员,这给你带来了很多责任:

  • 你需要记住的是queen_of_hearts_... 变量代表一张牌。
  • 对于tuple版本,你需要记住属性的顺序。编写('Spades', 'A')会搞乱你的程序,但可能不会给你一个容易理解的错误信息。
  • 如果您使用dict版本,您必须确保属性的名称是一致的。例如{'value': 'A', 'suit': 'Spades'}将无法按预期工作。

此外,使用这些结构并不理想:

>>> queen_of_hearts_tuple[0]  # No named access
'Q'
>>> queen_of_hearts_dict['suit']  # Would be nicer with .suit
'Hearts'

更好的选择是 namedtuple 。它长期以来被用来创建可读的小型数据结构。事实上,我们可以像这样使用namedtuple重新创建上面的数据类示例:

from collections import namedtuple

NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])

NamedTupleCard的这个定义将给出与我们的DataClassCard示例完全相同的输出:

>>> queen_of_hearts = NamedTupleCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
NamedTupleCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == NamedTupleCard('Q', 'Hearts')
True

那么,为什么还要麻烦数据类呢?首先,数据类提供了比你目前所见更多的特性。同时,namedtuple还有一些其他不一定可取的特性。按照设计,一个namedtuple是一个常规元组。这可以从比较中看出,例如:

>>> queen_of_hearts == ('Q', 'Hearts')
True

虽然这看起来是件好事,但是缺乏对自身类型的了解可能会导致微妙且难以发现的错误,尤其是因为它还会乐于比较两个不同的namedtuple类:

>>> Person = namedtuple('Person', ['first_initial', 'last_name']
>>> ace_of_spades = NamedTupleCard('A', 'Spades')
>>> ace_of_spades == Person('A', 'Spades')
True

namedtuple也有一些限制。例如,很难向namedtuple中的一些字段添加默认值。一个namedtuple本质上也是不可改变的。也就是说,namedtuple的值永远不会改变。在某些应用程序中,这是一个很棒的功能,但在其他设置中,如果有更多的灵活性就更好了:

>>> card = NamedTupleCard('7', 'Diamonds')
>>> card.rank = '9'
AttributeError: can't set attribute

数据类不会取代namedtuple的所有用途。例如,如果您需要您的数据结构表现得像一个元组,那么命名元组是一个很好的选择!

另一个选择,也是数据类的灵感之一,是 attrs项目。安装了attrs(pip install attrs)后,可以编写如下的卡类:

import attr

@attr.s
class AttrsCard:
    rank = attr.ib()
    suit = attr.ib()

这可以以与前面的DataClassCardNamedTupleCard示例完全相同的方式使用。attrs项目很棒,支持一些数据类不支持的特性,包括转换器和验证器。此外,attrs已经存在了一段时间,在 Python 2.7 以及 Python 3.4 和更高版本中都得到了支持。然而,由于attrs不是标准库的一部分,它给你的项目增加了一个外部依赖项。通过数据类,类似的功能将随处可见。

除了tupledictnamedtupleattrs之外,还有很多其他类似的项目,包括 typing.NamedTuplenamedlistattrdictplumberfields 。虽然数据类是一个很好的新选择,但是仍然有旧的变体更适合的用例。例如,如果您需要与期望元组的特定 API 兼容,或者需要数据类中不支持的功能。

Remove ads

基本数据类别

让我们回到数据类。例如,我们将创建一个Position类,它将使用名称以及纬度和经度来表示地理位置:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float
    lat: float

使它成为数据类的是类定义上面的 @dataclass装饰符。在class Position:行下面,您只需简单地列出您想要包含在数据类中的字段。用于字段的:符号使用了 Python 3.6 中的一个新特性,叫做变量注释。我们将很快谈论更多关于这个符号以及为什么我们指定像strfloat这样的数据类型。

您只需要这几行代码。新类已经可以使用了:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> print(pos)
Position(name='Oslo', lon=10.8, lat=59.9)
>>> pos.lat
59.9
>>> print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')
Oslo is at 59.9°N, 10.8°E

您也可以像创建命名元组一样创建数据类。以下(几乎)等同于上面Position的定义:

from dataclasses import make_dataclass

Position = make_dataclass('Position', ['name', 'lat', 'lon'])

数据类是一个常规的 Python 类。它与众不同的唯一一点是,它为您实现了基本的数据模型方法,如.__init__().__repr__().__eq__()

默认值

向数据类的字段添加默认值很容易:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

这与您在常规类的.__init__()方法的定义中指定默认值完全一样:

>>> Position('Null Island')
Position(name='Null Island', lon=0.0, lat=0.0)
>>> Position('Greenwich', lat=51.8)
Position(name='Greenwich', lon=0.0, lat=51.8)
>>> Position('Vancouver', -123.1, 49.3)
Position(name='Vancouver', lon=-123.1, lat=49.3)

稍后你会了解到default_factory,它给出了一种提供更复杂默认值的方法。

类型提示

到目前为止,我们还没有对数据类支持开箱即用的输入这一事实大惊小怪。您可能已经注意到我们用类型提示定义了字段:name: str表示name应该是一个文本字符串 ( str类型)。

事实上,在定义数据类中的字段时,添加某种类型的提示是强制性的。如果没有类型提示,字段将不会是数据类的一部分。但是,如果您不想向您的数据类添加显式类型,请使用typing.Any:

from dataclasses import dataclass
from typing import Any

@dataclass
class WithoutExplicitTypes:
    name: Any
    value: Any = 42

虽然在使用数据类时需要以某种形式添加类型提示,但这些类型在运行时并不是强制的。以下代码运行时没有任何问题:

>>> Position(3.14, 'pi day', 2018)
Position(name=3.14, lon='pi day', lat=2018)

Python 中的类型通常是这样工作的: Python 现在是并且将永远是一种动态类型语言。为了实际捕捉类型错误,可以在源代码上运行像 Mypy 这样的类型检查器。

Remove ads

添加方法

你已经知道数据类只是一个普通的类。这意味着您可以自由地将自己的方法添加到数据类中。作为一个例子,让我们沿着地球表面计算一个位置和另一个位置之间的距离。一种方法是使用哈弗辛公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以向数据类添加一个.distance_to()方法,就像处理普通类一样:

from dataclasses import dataclass
from math import asin, cos, radians, sin, sqrt

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

    def distance_to(self, other):
        r = 6371  # Earth radius in kilometers
        lam_1, lam_2 = radians(self.lon), radians(other.lon)
        phi_1, phi_2 = radians(self.lat), radians(other.lat)
        h = (sin((phi_2 - phi_1) / 2)**2
             + cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2)**2)
        return 2 * r * asin(sqrt(h))

它的工作方式如您所料:

>>> oslo = Position('Oslo', 10.8, 59.9)
>>> vancouver = Position('Vancouver', -123.1, 49.3)
>>> oslo.distance_to(vancouver)
7181.7841229421165

更灵活的数据类别

到目前为止,您已经看到了 data 类的一些基本特性:它为您提供了一些方便的方法,您仍然可以添加默认值和其他方法。现在您将了解一些更高级的特性,比如@dataclass装饰器和field()函数的参数。当创建数据类时,它们一起给你更多的控制。

让我们回到您在本教程开始时看到的扑克牌示例,并添加一个包含一副扑克牌的类:

from dataclasses import dataclass
from typing import List

@dataclass
class PlayingCard:
    rank: str
    suit: str

@dataclass
class Deck:
    cards: List[PlayingCard]

可以像这样创建一个只包含两张卡片的简单卡片组:

>>> queen_of_hearts = PlayingCard('Q', 'Hearts')
>>> ace_of_spades = PlayingCard('A', 'Spades')
>>> two_cards = Deck([queen_of_hearts, ace_of_spades])
Deck(cards=[PlayingCard(rank='Q', suit='Hearts'),
 PlayingCard(rank='A', suit='Spades')])

高级默认值

假设您想给Deck一个默认值。例如,如果Deck()创建一副由 52 张扑克牌组成的普通(法国)牌,那将会很方便。首先,指定不同的军衔和服装。然后,添加一个函数make_french_deck(),它创建一个PlayingCard实例的列表:

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()

def make_french_deck():
    return [PlayingCard(r, s) for s in SUITS for r in RANKS]

有趣的是,这四种不同的套装是用它们的 Unicode 符号指定的。

**注意:**上面,我们在源代码中直接使用了类似的 Unicode 字形。我们可以这样做,因为默认情况下 Python 支持在 UTF-8 中编写源代码。关于如何在您的系统上输入这些内容,请参考本页的 Unicode 输入。您也可以使用\N命名字符转义符(如\N{BLACK SPADE SUIT})或\u Unicode 转义符(如\u2660)为套装输入 Unicode 符号。

为了简化以后卡片的比较,等级和套装也按照通常的顺序排列。

>>> make_french_deck()
[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]

理论上,您现在可以使用这个函数为Deck.cards指定一个默认值:

from dataclasses import dataclass
from typing import List

@dataclass
class Deck:  # Will NOT work
    cards: List[PlayingCard] = make_french_deck()

不要这样!这引入了 Python 中最常见的反模式之一:使用可变默认参数。问题是Deck的所有实例将使用相同的列表对象作为.cards属性的默认值。这意味着,比方说,如果从一个Deck中移除一张卡片,那么它也会从所有其他Deck的实例中消失。实际上,数据类试图阻止你这样做,上面的代码会引发一个ValueError

相反,数据类使用一种叫做default_factory的东西来处理可变的默认值。要使用default_factory(以及数据类的许多其他很酷的特性),您需要使用field()说明符:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
    cards: List[PlayingCard] = field(default_factory=make_french_deck)

default_factory的参数可以是任何可调用的零参数。现在很容易创建一副完整的扑克牌:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

field()说明符用于单独定制数据类的每个字段。稍后您将看到其他一些示例。作为参考,以下是field()支持的参数:

  • default:该字段的默认值
  • default_factory:返回字段初始值的函数
  • init:在.__init__()方法中使用字段?(默认为True。)
  • repr:使用对象的repr中的字段?(默认为True。)
  • compare:在比较中包含该字段?(默认为True。)
  • hash:计算hash()时包含该字段?(默认使用与compare相同的。)
  • metadata:关于字段信息的映射

Position示例中,您看到了如何通过编写lat: float = 0.0来添加简单的默认值。然而,如果您还想定制这个字段,例如在repr中隐藏它,您需要使用default参数:lat: float = field(default=0.0, repr=False)。您不能同时指定defaultdefault_factory

数据类本身不使用metadata参数,但是您(或第三方包)可以使用它将信息附加到字段中。在Position示例中,您可以指定纬度和经度应该以度为单位:

from dataclasses import dataclass, field

@dataclass
class Position:
    name: str
    lon: float = field(default=0.0, metadata={'unit': 'degrees'})
    lat: float = field(default=0.0, metadata={'unit': 'degrees'})

可以使用fields()函数检索元数据(以及关于字段的其他信息)(注意复数 s ):

>>> from dataclasses import fields
>>> fields(Position)
(Field(name='name',type=<class 'str'>,...,metadata={}),
 Field(name='lon',type=<class 'float'>,...,metadata={'unit': 'degrees'}),
 Field(name='lat',type=<class 'float'>,...,metadata={'unit': 'degrees'}))
>>> lat_unit = fields(Position)[2].metadata['unit']
>>> lat_unit
'degrees'

Remove ads

你需要代理吗?

回想一下,我们可以凭空创造卡片组:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

虽然这种对Deck的表示是显式的和可读的,但它也非常冗长。在上面的输出中,我已经删除了 52 张卡片中的 48 张。在一个 80 列的显示器上,仅仅打印完整的Deck就要占用 22 行!让我们添加一个更简洁的表示。一般来说,一个 Python 对象有两种不同的字符串表示:

  • repr(obj)obj.__repr__()定义,应该返回一个对开发者友好的obj的表示。如果可能的话,这应该是可以重新创建obj的代码。数据类就是这样做的。

  • str(obj)obj.__str__()定义,应该返回一个用户友好的obj表示。数据类没有实现.__str__()方法,所以 Python 将退回到.__repr__()方法。

让我们实现一个PlayingCard的用户友好表示:

from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

    def __str__(self):
        return f'{self.suit}{self.rank}'

卡片现在看起来漂亮多了,但是这副牌还是和以前一样冗长:

>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades
PlayingCard(rank='A', suit='♠')
>>> print(ace_of_spades)
♠A
>>> print(Deck())
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

为了说明添加自己的.__repr__()方法也是可能的,我们将违反它应该返回可以重新创建对象的代码的原则。实用性终究胜过纯粹性。下面的代码添加了一个更简洁的Deck表示:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
    cards: List[PlayingCard] = field(default_factory=make_french_deck)

    def __repr__(self):
        cards = ', '.join(f'{c!s}' for c in self.cards)
        return f'{self.__class__.__name__}({cards})'

注意{c!s}格式字符串中的!s说明符。这意味着我们明确地想要使用每个PlayingCardstr()表示。有了新的.__repr__(),对Deck的描绘更加赏心悦目:

>>> Deck()
Deck(2,3,4,5,6,7,8,9,10, ♣J, ♣Q, ♣K, ♣A,2,3,4,5,6,7,8,9,10, ♢J, ♢Q, ♢K, ♢A,2,3,4,5,6,7,8,9,10, ♡J, ♡Q, ♡K, ♡A,2,3,4,5,6,7,8,9,10, ♠J, ♠Q, ♠K, ♠A)

这是一个更好的甲板代表。然而,这是有代价的。您不再能够通过执行其表示来重新创建牌组。通常,用.__str__()实现相同的表示会更好。

比较卡片

在许多纸牌游戏中,纸牌是互相比较的。例如,在典型的取牌游戏中,最高的牌取牌。正如当前实现的那样,PlayingCard类不支持这种比较:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
TypeError: '>' not supported between instances of 'Card' and 'Card'

然而,这(看起来)很容易纠正:

from dataclasses import dataclass

@dataclass(order=True)
class PlayingCard:
    rank: str
    suit: str

    def __str__(self):
        return f'{self.suit}{self.rank}'

装饰器有两种形式。到目前为止,您已经看到了没有任何括号和参数的简单形式。然而,您也可以给圆括号中的@dataclass()装饰器提供参数。支持以下参数:

  • init:添加.__init__()方法?(默认为True。)
  • repr:添加.__repr__()方法?(默认为True。)
  • eq:添加.__eq__()方法?(默认为True。)
  • order:添加订购方式?(默认为False。)
  • unsafe_hash:强制添加一个.__hash__()方法?(默认为False。)
  • frozen:如果True,赋值给字段引发异常。(默认为False。)

参见原 PEP 了解更多关于各参数的信息。设置order=True后,可以比较PlayingCard的实例:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
False

但是这两张卡有什么不同呢?您没有指定应该如何排序,而且出于某种原因,Python 似乎认为皇后比 a 高…

事实证明,数据类比较对象就好像它们是其字段的元组一样。换句话说,皇后比 a 高,因为在字母表中'Q'排在'A'之后:

>>> ('A', '♠') > ('Q', '♡')
False

这对我们来说并不奏效。相反,我们需要定义某种使用RANKSSUITS顺序的排序索引。大概是这样的:

>>> RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
>>> SUITS = '♣ ♢ ♡ ♠'.split()
>>> card = PlayingCard('Q', '♡')
>>> RANKS.index(card.rank) * len(SUITS) + SUITS.index(card.suit)
42

为了让PlayingCard使用这个排序索引进行比较,我们需要向类中添加一个字段.sort_index。然而,该字段应根据其他字段.rank.suit自动计算。这正是特殊方法.__post_init__()的目的。它允许在调用常规的.__init__()方法后进行特殊处理:

from dataclasses import dataclass, field

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()

@dataclass(order=True)
class PlayingCard:
    sort_index: int = field(init=False, repr=False)
    rank: str
    suit: str

    def __post_init__(self):
        self.sort_index = (RANKS.index(self.rank) * len(SUITS)
                           + SUITS.index(self.suit))

    def __str__(self):
        return f'{self.suit}{self.rank}'

请注意,.sort_index是作为该类的第一个字段添加的。这样,首先使用.sort_index进行比较,只有当出现平局时才使用其他字段。使用field(),您还必须指定.sort_index不应该作为参数包含在.__init__()方法中(因为它是从.rank.suit字段中计算出来的)。为了避免用户对这个实现细节感到困惑,从类的repr中移除.sort_index可能也是一个好主意。

最后,ace 很高:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
True

现在,您可以轻松创建一个已排序的卡片组:

>>> Deck(sorted(make_french_deck()))
Deck(2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,10,10,10,10, ♣J, ♢J, ♡J,
 ♠J, ♣Q, ♢Q, ♡Q, ♠Q, ♣K, ♢K, ♡K, ♠K, ♣A, ♢A, ♡A, ♠A)

或者,如果你不在乎排序,这是你随机抽取 10 张牌的方法:

>>> from random import sample
>>> Deck(sample(make_french_deck(), k=10))
Deck(2, ♡A,10,2,3,3, ♢A,8,9,2)

当然,你不需要order=True来做这个…

Remove ads

不可变数据类

您之前看到的namedtuple的一个定义特性是是不可变的。也就是说,其字段的值可能永远不会改变。对于许多类型的数据类来说,这是一个好主意!为了使数据类不可变,在创建时设置frozen=True。例如,下面是您之前看到的的Position的不可变版本:

from dataclasses import dataclass

@dataclass(frozen=True)
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

在冻结的数据类中,不能在创建后为字段赋值:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> pos.name
'Oslo'
>>> pos.name = 'Stockholm'
dataclasses.FrozenInstanceError: cannot assign to field 'name'

请注意,如果您的数据类包含可变字段,这些字段仍然可能会发生变化。这适用于 Python 中的所有嵌套数据结构(更多信息请参见本视频):

from dataclasses import dataclass
from typing import List

@dataclass(frozen=True)
class ImmutableCard:
    rank: str
    suit: str

@dataclass(frozen=True)
class ImmutableDeck:
    cards: List[ImmutableCard]

尽管ImmutableCardImmutableDeck都是不可变的,但是包含cards的列表却不是。因此,您仍然可以更改这副牌中的牌:

>>> queen_of_hearts = ImmutableCard('Q', '♡')
>>> ace_of_spades = ImmutableCard('A', '♠')
>>> deck = ImmutableDeck([queen_of_hearts, ace_of_spades])
>>> deck
ImmutableDeck(cards=[ImmutableCard(rank='Q', suit='♡'), ImmutableCard(rank='A', suit='♠')])
>>> deck.cards[0] = ImmutableCard('7', '♢')
>>> deck
ImmutableDeck(cards=[ImmutableCard(rank='7', suit='♢'), ImmutableCard(rank='A', suit='♠')])

为了避免这种情况,请确保不可变数据类的所有字段都使用不可变类型(但请记住,类型在运行时是不强制的)。应该使用元组而不是列表来实现ImmutableDeck

继承

你可以很自由地子类化数据类。例如,我们将使用一个country字段来扩展我们的Position示例,并使用它来记录大写字母:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float
    lat: float

@dataclass
class Capital(Position):
    country: str

在这个简单的例子中,一切顺利:

>>> Capital('Oslo', 10.8, 59.9, 'Norway')
Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')

Capitalcountry字段添加在Position的三个原始字段之后。如果基类中的任何字段都有默认值,事情会变得稍微复杂一些:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

@dataclass
class Capital(Position):
    country: str  # Does NOT work

这段代码会立即崩溃,并出现一个TypeError抱怨“非默认参数‘country’跟在默认参数后面。”问题是我们新的country字段没有默认值,而lonlat字段有默认值。数据类将尝试用下面的签名编写一个.__init__()方法:

def __init__(name: str, lon: float = 0.0, lat: float = 0.0, country: str):
    ...

但是,这不是有效的 Python。如果一个参数有默认值,所有后续参数也必须有默认值。换句话说,如果基类中的字段有默认值,那么子类中添加的所有新字段也必须有默认值。

另一件需要注意的事情是字段在子类中是如何排序的。从基类开始,字段按照第一次定义的顺序排序。如果一个字段在子类中被重新定义,它的顺序不会改变。例如,如果将PositionCapital定义如下:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

@dataclass
class Capital(Position):
    country: str = 'Unknown'
    lat: float = 40.0

那么Capital中字段的顺序仍然是namelonlatcountry。但是lat的默认值会是40.0

>>> Capital('Madrid', country='Spain')
Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain')

Remove ads

优化数据类

我要用几句关于的话来结束这个教程。插槽可以用来使类更快,使用更少的内存。数据类没有处理插槽的显式语法,但是创建插槽的正常方式也适用于数据类。(他们真的只是普通班!)

from dataclasses import dataclass

@dataclass
class SimplePosition:
    name: str
    lon: float
    lat: float

@dataclass
class SlotPosition:
    __slots__ = ['name', 'lon', 'lat']
    name: str
    lon: float
    lat: float

本质上,槽是使用.__slots__来定义的,列出一个类中的变量。不存在于.__slots__中的变量或属性可能无法定义。此外,插槽类可能没有默认值。

添加这些限制的好处是可以进行某些优化。例如,插槽类占用更少的内存,可以使用 Pympler 来测量:

>>> from pympler import asizeof
>>> simple = SimplePosition('London', -0.1, 51.5)
>>> slot = SlotPosition('Madrid', -3.7, 40.4)
>>> asizeof.asizesof(simple, slot)
(440, 248)

类似地,插槽类通常使用起来更快。以下示例使用标准库中的 timeit 来测量 slots 数据类和常规数据类的属性访问速度。

>>> from timeit import timeit
>>> timeit('slot.name', setup="slot=SlotPosition('Oslo', 10.8, 59.9)", globals=globals())
0.05882283499886398
>>> timeit('simple.name', setup="simple=SimplePosition('Oslo', 10.8, 59.9)", globals=globals())
0.09207444800267695

在这个特殊的例子中,slot 类大约快了 35%。

结论和进一步阅读

数据类是 Python 3.7 的新特性之一。使用数据类,您不必编写样板代码来获得对象的正确初始化、表示和比较。

您已经看到了如何定义自己的数据类,以及:

  • 如何向数据类中的字段添加默认值
  • 如何自定义数据类对象的排序
  • 如何使用不可变数据类
  • 继承如何为数据类工作

如果你想深入了解数据类的所有细节,看看 PEP 557 以及最初的 GitHub repo 中的讨论。

此外,Raymond Hettinger 的 PyCon 2018 talk Dataclasses:结束所有代码生成器的代码生成器非常值得一看。

如果您还没有 Python 3.7,那么 Python 3.6 还有一个数据类反向移植。现在,向前迈进,编写更少的代码!

立即观看**本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 在 Python 中使用数据类******

用 Pandas 和 NumPy 清理 Pythonic 数据

原文:https://realpython.com/python-data-cleaning-numpy-pandas/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: 用熊猫和 NumPy 进行数据清洗

数据科学家花费大量时间清理数据集,并将它们转换成他们可以使用的形式。事实上,许多数据科学家认为,获取和清理数据的初始步骤构成了 80%的工作。

因此,如果您刚刚进入这个领域或者计划进入这个领域,能够处理杂乱的数据是很重要的,无论这意味着丢失的值、不一致的格式、畸形的记录还是无意义的异常值。

在本教程中,我们将利用 Python 的 Pandas 和 NumPy 库来清理数据。

我们将讨论以下内容:

  • 删除DataFrame中不必要的列
  • 改变一个DataFrame的索引
  • 使用.str()方法清洁色谱柱
  • 使用DataFrame.applymap()函数逐个元素地清理整个数据集
  • 将列重命名为更容易识别的标签集
  • 跳过 CSV 文件中不必要的行

免费奖励: 点击此处获取免费的 NumPy 资源指南,它会为您指出提高 NumPy 技能的最佳教程、视频和书籍。

以下是我们将使用的数据集:

  • BL-Flickr-Images-book . csv–包含大英图书馆书籍信息的 CSV 文件
  • university _ towns . txt–包含美国各州大学城名称的文本文件
  • Olympics . csv–总结所有国家参加夏季和冬季奥运会的 CSV 文件

您可以从 Real Python 的 GitHub 仓库下载数据集,以便了解这里的示例。

注意:我推荐使用 Jupyter 笔记本来跟进。

本教程假设对 Pandas 和 NumPy 库有基本的了解,包括 Panda 的主力 SeriesDataFrame对象,可以应用于这些对象的常用方法,以及熟悉 NumPy 的 NaN 值。

让我们导入所需的模块并开始吧!

>>> import pandas as pd
>>> import numpy as np

DataFrame中拖放列

通常,您会发现并非数据集中的所有数据类别都对您有用。例如,您可能有一个包含学生信息(姓名、年级、标准、父母姓名和地址)的数据集,但您希望专注于分析学生的成绩。

在这种情况下,地址或父母的名字对你来说并不重要。保留这些不需要的类别会占用不必要的空间,还可能会影响运行时间。

Pandas 提供了一种简便的方法,通过 drop() 功能从DataFrame中删除不需要的列或行。让我们看一个简单的例子,我们从一个DataFrame中删除了一些列。

首先,让我们从 CSV 文件“BL-Flickr-Images-Book.csv”中创建一个 DataFrame 。在下面的例子中,我们传递了一个到pd.read_csv的相对路径,这意味着所有的数据集都在我们当前工作目录中一个名为Datasets的文件夹中:

>>> df = pd.read_csv('Datasets/BL-Flickr-Images-Book.csv')
>>> df.head()

 Identifier             Edition Statement      Place of Publication  \
0         206                           NaN                    London
1         216                           NaN  London; Virtue & Yorston
2         218                           NaN                    London
3         472                           NaN                    London
4         480  A new edition, revised, etc.                    London

 Date of Publication              Publisher  \
0         1879 [1878]       S. Tinsley & Co.
1                1868           Virtue & Co.
2                1869  Bradbury, Evans & Co.
3                1851          James Darling
4                1857   Wertheim & Macintosh

 Title     Author  \
0                  Walter Forbes. [A novel.] By A. A      A. A.
1  All for Greed. [A novel. The dedication signed...  A., A. A.
2  Love the Avenger. By the author of “All for Gr...  A., A. A.
3  Welsh Sketches, chiefly ecclesiastical, to the...  A., E. S.
4  [The World in which I live, and my place in it...  A., E. S.

 Contributors  Corporate Author  \
0                               FORBES, Walter.               NaN
1  BLAZE DE BURY, Marie Pauline Rose - Baroness               NaN
2  BLAZE DE BURY, Marie Pauline Rose - Baroness               NaN
3                   Appleyard, Ernest Silvanus.               NaN
4                           BROOME, John Henry.               NaN

 Corporate Contributors Former owner  Engraver Issuance type  \
0                     NaN          NaN       NaN   monographic
1                     NaN          NaN       NaN   monographic
2                     NaN          NaN       NaN   monographic
3                     NaN          NaN       NaN   monographic
4                     NaN          NaN       NaN   monographic

 Flickr URL  \
0  http://www.flickr.com/photos/britishlibrary/ta...
1  http://www.flickr.com/photos/britishlibrary/ta...
2  http://www.flickr.com/photos/britishlibrary/ta...
3  http://www.flickr.com/photos/britishlibrary/ta...
4  http://www.flickr.com/photos/britishlibrary/ta...

 Shelfmarks
0    British Library HMNTS 12641.b.30.
1    British Library HMNTS 12626.cc.2.
2    British Library HMNTS 12625.dd.1.
3    British Library HMNTS 10369.bbb.15.
4    British Library HMNTS 9007.d.28.

当我们使用head()方法查看前五个条目时,我们可以看到一些列提供了对图书馆有帮助的辅助信息,但并没有很好地描述书籍本身:Edition StatementCorporate AuthorCorporate ContributorsFormer ownerEngraverIssuance typeShelfmarks

我们可以通过以下方式删除这些列:

>>> to_drop = ['Edition Statement',
...            'Corporate Author',
...            'Corporate Contributors',
...            'Former owner',
...            'Engraver',
...            'Contributors',
...            'Issuance type',
...            'Shelfmarks']

>>> df.drop(to_drop, inplace=True, axis=1)

上面,我们定义了一个列表,其中包含了我们想要删除的所有列的名称。接下来,我们调用对象上的drop()函数,将inplace参数作为True传入,将axis参数作为1传入。这告诉 Pandas 我们希望直接在我们的对象中进行更改,并且它应该在对象的列中寻找要删除的值。

当我们再次检查DataFrame时,我们会看到不需要的列已经被删除:

>>> df.head()
 Identifier      Place of Publication Date of Publication  \
0         206                    London         1879 [1878]
1         216  London; Virtue & Yorston                1868
2         218                    London                1869
3         472                    London                1851
4         480                    London                1857

 Publisher                                              Title  \
0       S. Tinsley & Co.                  Walter Forbes. [A novel.] By A. A
1           Virtue & Co.  All for Greed. [A novel. The dedication signed...
2  Bradbury, Evans & Co.  Love the Avenger. By the author of “All for Gr...
3          James Darling  Welsh Sketches, chiefly ecclesiastical, to the...
4   Wertheim & Macintosh  [The World in which I live, and my place in it...

 Author                                         Flickr URL
0      A. A.  http://www.flickr.com/photos/britishlibrary/ta...
1  A., A. A.  http://www.flickr.com/photos/britishlibrary/ta...
2  A., A. A.  http://www.flickr.com/photos/britishlibrary/ta...
3  A., E. S.  http://www.flickr.com/photos/britishlibrary/ta...
4  A., E. S.  http://www.flickr.com/photos/britishlibrary/ta...

或者,我们也可以通过将列直接传递给columns参数来删除列,而不是单独指定要删除的标签和熊猫应该在哪个轴上寻找标签:

>>> df.drop(columns=to_drop, inplace=True)

这种语法更直观,可读性更强。我们要做的事情很明显。

如果您事先知道想要保留哪些列,另一个选项是将它们传递给pd.read_csvusecols参数。

Remove ads

改变一个DataFrame的索引

Pandas Index扩展了 NumPy 数组的功能,允许更多的切片和标记。在许多情况下,使用数据的唯一值标识字段作为索引是很有帮助的。

例如,在上一节使用的数据集中,可以预计当图书管理员搜索记录时,他们可能会输入一本书的唯一标识符(Identifier列中的值):

>>> df['Identifier'].is_unique
True

让我们使用set_index用这个列替换现有的索引:

>>> df = df.set_index('Identifier')
>>> df.head()
 Place of Publication Date of Publication  \
206                           London         1879 [1878]
216         London; Virtue & Yorston                1868
218                           London                1869
472                           London                1851
480                           London                1857

 Publisher  \
206              S. Tinsley & Co.
216                  Virtue & Co.
218         Bradbury, Evans & Co.
472                 James Darling
480          Wertheim & Macintosh

 Title     Author  \
206                         Walter Forbes. [A novel.] By A. A      A. A.
216         All for Greed. [A novel. The dedication signed...  A., A. A.
218         Love the Avenger. By the author of “All for Gr...  A., A. A.
472         Welsh Sketches, chiefly ecclesiastical, to the...  A., E. S.
480         [The World in which I live, and my place in it...  A., E. S.

 Flickr URL
206         http://www.flickr.com/photos/britishlibrary/ta...
216         http://www.flickr.com/photos/britishlibrary/ta...
218         http://www.flickr.com/photos/britishlibrary/ta...
472         http://www.flickr.com/photos/britishlibrary/ta...
480         http://www.flickr.com/photos/britishlibrary/ta...

技术细节:与 SQL 中的主键不同,Pandas Index不保证唯一性,尽管许多索引和合并操作会注意到运行时的加速。

我们可以用loc[]直接访问每条记录。虽然loc[]可能没有名字那么直观,但它允许我们做基于标签的索引,这是对行或记录的标签,而不考虑其位置:

>>> df.loc[206]
Place of Publication                                               London
Date of Publication                                           1879 [1878]
Publisher                                                S. Tinsley & Co.
Title                                   Walter Forbes. [A novel.] By A. A
Author                                                              A. A.
Flickr URL              http://www.flickr.com/photos/britishlibrary/ta...
Name: 206, dtype: object

换句话说,206 是索引的第一个标签。要通过位置访问它,我们可以使用df.iloc[0],它执行基于位置的索引。

技术细节 : .loc[]在技术上是一个类实例,并且有一些特殊的语法,这些语法并不完全符合大多数普通的 Python 实例方法。

以前,我们的索引是一个 RangeIndex:从0开始的整数,类似于 Python 的内置range。通过将一个列名传递给set_index,我们将索引更改为Identifier中的值。

您可能已经注意到,我们将变量重新分配给了由带有df = df.set_index(...)的方法返回的对象。这是因为,默认情况下,该方法返回我们的对象的修改副本,并不直接对对象进行更改。我们可以通过设置inplace参数来避免这种情况:

df.set_index('Identifier', inplace=True)

整理数据中的字段

到目前为止,我们已经删除了不必要的列,并将DataFrame的索引改为更合理的。在这一节中,我们将清理特定的列,并将它们转换为统一的格式,以便更好地理解数据集并增强一致性。特别是,我们将清洁Date of PublicationPlace of Publication

经检查,目前所有的数据类型都是object dtype ,这大致类似于原生 Python 中的str

它封装了任何不能作为数字或分类数据的字段。这是有意义的,因为我们处理的数据最初是一堆杂乱的字符串:

>>> df.get_dtype_counts()
object    6

强制使用数字值有意义的一个字段是出版日期,这样我们可以在以后进行计算:

>>> df.loc[1905:, 'Date of Publication'].head(10)
Identifier
1905           1888
1929    1839, 38-54
2836        [1897?]
2854           1865
2956        1860-63
2957           1873
3017           1866
3131           1899
4598           1814
4884           1820
Name: Date of Publication, dtype: object

一本书只能有一个出版日期。因此,我们需要做到以下几点:

  • 删除方括号中的多余日期:1879 [1878]
  • 将日期范围转换为它们的“开始日期”,如果有的话:1860-63;1839, 38-54
  • 完全去掉我们不确定的日期,用 NumPy 的NaN:【1897?]
  • 将字符串nan转换为 NumPy 的NaN

综合这些模式,我们实际上可以利用一个正则表达式来提取出版年份:

regex = r'^(\d{4})'

上面的正则表达式旨在查找字符串开头的任意四位数字,这就满足了我们的情况。上面是一个原始字符串(意思是反斜杠不再是转义字符),这是正则表达式的标准做法。

\d代表任意数字,{4}重复这个规则四次。^字符匹配一个字符串的开头,圆括号表示一个捕获组,这向 Pandas 发出信号,表明我们想要提取正则表达式的这一部分。(我们希望^避免[开始串的情况。)

让我们看看在数据集上运行这个正则表达式会发生什么:

>>> extr = df['Date of Publication'].str.extract(r'^(\d{4})', expand=False)
>>> extr.head()
Identifier
206    1879
216    1868
218    1869
472    1851
480    1857
Name: Date of Publication, dtype: object

**延伸阅读:**不熟悉 regex?你可以在 regex101.com查看上面的表达式,用正则表达式:Python 中的正则表达式学习所有关于正则表达式的知识。

从技术上讲,这个列仍然有object dtype,但是我们可以很容易地用pd.to_numeric得到它的数字版本:

>>> df['Date of Publication'] = pd.to_numeric(extr)
>>> df['Date of Publication'].dtype
dtype('float64')

这导致大约十分之一的值丢失,对于现在能够对剩余的有效值进行计算来说,这是一个很小的代价:

>>> df['Date of Publication'].isnull().sum() / len(df)
0.11717147339205986

太好了!就这么定了!

Remove ads

str方法与 NumPy 结合起来清洗色谱柱

以上,你可能注意到了df['Date of Publication'].str的用法。这个属性是在 Pandas 中访问快速的字符串操作的一种方式,这些操作很大程度上模仿了原生 Python 字符串或编译后的正则表达式的操作,如.split().capitalize()

为了清理Place of Publication字段,我们可以将 Pandas str方法与 NumPy 的np.where函数结合起来,该函数基本上是 Excel 的IF()宏的矢量化形式。它具有以下语法:

>>> np.where(condition, then, else)

这里,condition或者是一个类数组对象,或者是一个布尔掩码。then是在condition评估为True时使用的值,而else是在其他情况下使用的值。

本质上,.where()获取用于condition的对象中的每个元素,检查该特定元素在条件的上下文中是否评估为True,并返回包含thenelsendarray,这取决于哪一个适用。

它可以嵌套在一个复合 if-then 语句中,允许我们基于多个条件计算值:

>>> np.where(condition1, x1, 
 np.where(condition2, x2, 
 np.where(condition3, x3, ...)))

我们将利用这两个函数来清理Place of Publication,因为这个列有 string 对象。以下是该专栏的内容:

>>> df['Place of Publication'].head(10)
Identifier
206                                  London
216                London; Virtue & Yorston
218                                  London
472                                  London
480                                  London
481                                  London
519                                  London
667     pp. 40\. G. Bryan & Co: Oxford, 1898
874                                 London]
1143                                 London
Name: Place of Publication, dtype: object

我们看到,对于某些行,发布位置被其他不必要的信息所包围。如果我们要查看更多的值,我们会发现只有一些发布地点为“London”或“Oxford”的行是这种情况。

让我们来看看两个具体条目:

>>> df.loc[4157862]
Place of Publication                                  Newcastle-upon-Tyne
Date of Publication                                                  1867
Publisher                                                      T. Fordyce
Title                   Local Records; or, Historical Register of rema...
Author                                                        T.  Fordyce
Flickr URL              http://www.flickr.com/photos/britishlibrary/ta...
Name: 4157862, dtype: object

>>> df.loc[4159587]
Place of Publication                                  Newcastle upon Tyne
Date of Publication                                                  1834
Publisher                                                Mackenzie & Dent
Title                   An historical, topographical and descriptive v...
Author                                               E. (Eneas) Mackenzie
Flickr URL              http://www.flickr.com/photos/britishlibrary/ta...
Name: 4159587, dtype: object

这两本书是在同一个地方出版的,但是一本在地名上有连字符,而另一本没有。

为了在一次扫描中清理这个列,我们可以使用str.contains()来获得一个布尔掩码。

我们按照以下步骤清洗色谱柱:

>>> pub = df['Place of Publication']
>>> london = pub.str.contains('London')
>>> london[:5]
Identifier
206    True
216    True
218    True
472    True
480    True
Name: Place of Publication, dtype: bool

>>> oxford = pub.str.contains('Oxford')

我们将它们与np.where结合起来:

df['Place of Publication'] = np.where(london, 'London',
 np.where(oxford, 'Oxford',
 pub.str.replace('-', ' ')))

>>> df['Place of Publication'].head()
Identifier
206    London
216    London
218    London
472    London
480    London
Name: Place of Publication, dtype: object

这里,np.where函数在一个嵌套结构中被调用,其中condition是用str.contains()获得的布尔值的Seriescontains()方法的工作方式类似于内置的 in关键字,用于查找 iterable 中实体(或字符串中的子字符串)的出现。

要使用的替换是一个表示我们想要的发布地点的字符串。我们还将连字符替换为带有str.replace()的空格,并重新分配给我们的DataFrame中的列。

尽管这个数据集中有更多的脏数据,我们现在只讨论这两列。

让我们来看看前五个条目,它们看起来比我们开始时清晰得多:

>>> df.head()
 Place of Publication Date of Publication              Publisher  \
206                      London                1879        S. Tinsley & Co.
216                      London                1868           Virtue & Co.
218                      London                1869  Bradbury, Evans & Co.
472                      London                1851          James Darling
480                      London                1857   Wertheim & Macintosh

 Title    Author  \
206                         Walter Forbes. [A novel.] By A. A        AA
216         All for Greed. [A novel. The dedication signed...   A. A A.
218         Love the Avenger. By the author of “All for Gr...   A. A A.
472         Welsh Sketches, chiefly ecclesiastical, to the...   E. S A.
480         [The World in which I live, and my place in it...   E. S A.

 Flickr URL
206         http://www.flickr.com/photos/britishlibrary/ta...
216         http://www.flickr.com/photos/britishlibrary/ta...
218         http://www.flickr.com/photos/britishlibrary/ta...
472         http://www.flickr.com/photos/britishlibrary/ta...
480         http://www.flickr.com/photos/britishlibrary/ta...

注意:在这一点上,Place of Publication将是转换为 Categorical dtype 的一个很好的候选,因为我们可以用整数对相当小的唯一的一组城市进行编码。(一个分类的内存使用量与分类的数量加上数据的长度成正比;对象数据类型是一个常数乘以数据的长度。)

Remove ads

使用applymap函数清理整个数据集

在某些情况下,您会看到“污垢”并不局限于某一列,而是更加分散。

在某些情况下,将定制函数应用于数据帧的每个单元格或元素会很有帮助。Pandas .applymap()方法类似于内置的 map()函数,只是将一个函数应用于DataFrame中的所有元素。

让我们看一个例子。我们将从“university_towns.txt”文件中创建一个DataFrame:

$ head Datasets/univerisity_towns.txt
Alabama[edit]
Auburn (Auburn University)[1]
Florence (University of North Alabama)
Jacksonville (Jacksonville State University)[2]
Livingston (University of West Alabama)[2]
Montevallo (University of Montevallo)[2]
Troy (Troy University)[2]
Tuscaloosa (University of Alabama, Stillman College, Shelton State)[3][4]
Tuskegee (Tuskegee University)[5]
Alaska[edit]

我们看到,我们有周期性的州名,后面跟着该州的大学城:StateA TownA1 TownA2 StateB TownB1 TownB2...。如果我们观察状态名在文件中的书写方式,我们会发现所有的状态名中都有“[edit]”子字符串。

我们可以通过创建一个由(state, city)元组组成的列表并将该列表包装在一个DataFrame中来利用这种模式:

>>> university_towns = []
>>> with open('Datasets/university_towns.txt') as file:
...     for line in file:
...         if '[edit]' in line:
...             # Remember this `state` until the next is found
...             state = line
...         else:
...             # Otherwise, we have a city; keep `state` as last-seen
...             university_towns.append((state, line))

>>> university_towns[:5]
[('Alabama[edit]\n', 'Auburn (Auburn University)[1]\n'),
 ('Alabama[edit]\n', 'Florence (University of North Alabama)\n'),
 ('Alabama[edit]\n', 'Jacksonville (Jacksonville State University)[2]\n'),
 ('Alabama[edit]\n', 'Livingston (University of West Alabama)[2]\n'),
 ('Alabama[edit]\n', 'Montevallo (University of Montevallo)[2]\n')]

我们可以将这个列表包装在一个 DataFrame 中,并将列设置为“State”和“RegionName”。Pandas 将获取列表中的每个元素,并将State设置为左边的值,将RegionName设置为右边的值。

生成的数据帧如下所示:

>>> towns_df = pd.DataFrame(university_towns,
...                         columns=['State', 'RegionName'])

>>> towns_df.head()
 State                                         RegionName
0  Alabama[edit]\n                    Auburn (Auburn University)[1]\n
1  Alabama[edit]\n           Florence (University of North Alabama)\n
2  Alabama[edit]\n  Jacksonville (Jacksonville State University)[2]\n
3  Alabama[edit]\n       Livingston (University of West Alabama)[2]\n
4  Alabama[edit]\n         Montevallo (University of Montevallo)[2]\n

虽然我们可以在上面的 for 循环中清理这些字符串,但 Pandas 让它变得很容易。我们只需要州名和镇名,其他的都可以去掉。虽然我们可以在这里再次使用 Pandas 的.str()方法,但是我们也可以使用applymap()将一个 Python callable 映射到 DataFrame 的每个元素。

我们一直在使用术语元素,但是它到底是什么意思呢?考虑以下“玩具”数据帧:

 0           1
0    Mock     Dataset
1  Python     Pandas
2    Real     Python
3   NumPy     Clean

在这个例子中,每个单元格(’ Mock ‘,’ Dataset ‘,’ Python ‘,’ Pandas '等)。)是一个元素。因此,applymap()将独立地对其中的每一个应用一个函数。让我们来定义这个函数:

>>> def get_citystate(item):
...     if ' (' in item:
...         return item[:item.find(' (')]
...     elif '[' in item:
...         return item[:item.find('[')]
...     else:
...         return item

Pandas 的.applymap()只有一个参数,它是应该应用于每个元素的函数(可调用的):

>>> towns_df =  towns_df.applymap(get_citystate)

首先,我们定义一个 Python 函数,它将来自DataFrame的一个元素作为它的参数。在函数内部,执行检查以确定元素中是否有([

根据检查结果,函数会相应地返回值。最后,在我们的对象上调用applymap()函数。现在数据框架更加整洁了:

>>> towns_df.head()
 State    RegionName
0  Alabama        Auburn
1  Alabama      Florence
2  Alabama  Jacksonville
3  Alabama    Livingston
4  Alabama    Montevallo

applymap()方法从 DataFrame 中取出每个元素,将其传递给函数,原始值被返回值替换。就这么简单!

技术细节:虽然它是一个方便且通用的方法,但是.applymap对于较大的数据集来说有很长的运行时间,因为它将一个可调用的 Python 映射到每个单独的元素。在某些情况下,利用 Cython 或 NumPY(反过来,用 C 进行调用)进行矢量化操作会更有效。

Remove ads

重命名列和跳过行

通常,您将使用的数据集要么具有不容易理解的列名,要么在前几行和/或最后几行中具有不重要的信息,如数据集中术语的定义或脚注。

在这种情况下,我们希望重命名列并跳过某些行,这样我们就可以使用正确和合理的标签深入到必要的信息。

为了演示我们如何去做,让我们先看一下“olympics.csv”数据集的前五行:

$ head -n 5 Datasets/olympics.csv
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
,? Summer,01 !,02 !,03 !,Total,? Winter,01 !,02 !,03 !,Total,? Games,01 !,02 !,03 !,Combined total
Afghanistan (AFG),13,0,0,2,2,0,0,0,0,0,13,0,0,2,2
Algeria (ALG),12,5,2,8,15,3,0,0,0,0,15,5,2,8,15
Argentina (ARG),23,18,24,28,70,18,0,0,0,0,41,18,24,28,70

现在,我们将把它读入熊猫数据帧:

>>> olympics_df = pd.read_csv('Datasets/olympics.csv')
>>> olympics_df.head()
 0         1     2     3     4      5         6     7     8  \
0                NaN  ? Summer  01 !  02 !  03 !  Total  ? Winter  01 !  02 !
1  Afghanistan (AFG)        13     0     0     2      2         0     0     0
2      Algeria (ALG)        12     5     2     8     15         3     0     0
3    Argentina (ARG)        23    18    24    28     70        18     0     0
4      Armenia (ARM)         5     1     2     9     12         6     0     0

 9     10       11    12    13    14              15
0  03 !  Total  ? Games  01 !  02 !  03 !  Combined total
1     0      0       13     0     0     2               2
2     0      0       15     5     2     8              15
3     0      0       41    18    24    28              70
4     0      0       11     1     2     9              12

这真的很乱!这些列是索引为 0 的整数的字符串形式。本应是我们标题的行(即用于设置列名的行)位于olympics_df.iloc[0]。这是因为我们的 CSV 文件以 0,1,2,…,15 开头。

此外,如果我们转到数据集的源,我们会看到上面的NaN应该是类似于“国家”的东西,? Summer应该代表“夏季运动会”,01 !应该是“黄金”,等等。

因此,我们需要做两件事:

  • 跳过一行,将标题设置为第一行(索引为 0)
  • 重命名列

我们可以通过向read_csv()函数传递一些参数,在读取 CSV 文件时跳过行并设置标题。

这个函数需要 很多 的可选参数,但是在这种情况下我们只需要一个(header)来删除第 0 行:

>>> olympics_df = pd.read_csv('Datasets/olympics.csv', header=1)
>>> olympics_df.head()
 Unnamed: 0  ? Summer  01 !  02 !  03 !  Total  ? Winter  \
0        Afghanistan (AFG)        13     0     0     2      2         0
1            Algeria (ALG)        12     5     2     8     15         3
2          Argentina (ARG)        23    18    24    28     70        18
3            Armenia (ARM)         5     1     2     9     12         6
4  Australasia (ANZ) [ANZ]         2     3     4     5     12         0

 01 !.1  02 !.1  03 !.1  Total.1  ? Games  01 !.2  02 !.2  03 !.2  \
0       0       0       0        0       13       0       0       2
1       0       0       0        0       15       5       2       8
2       0       0       0        0       41      18      24      28
3       0       0       0        0       11       1       2       9
4       0       0       0        0        2       3       4       5

 Combined total
0               2
1              15
2              70
3              12
4              12

现在,我们已经将正确的行设置为标题,并删除了所有不必要的行。请注意熊猫如何将包含国家名称的列的名称从NaN更改为Unnamed: 0

为了重命名列,我们将利用 DataFrame 的rename()方法,该方法允许您基于映射(在本例中为dict)重新标记轴。

让我们首先定义一个字典,将当前的列名(作为键)映射到更有用的列名(字典的值):

>>> new_names =  {'Unnamed: 0': 'Country',
...               '? Summer': 'Summer Olympics',
...               '01 !': 'Gold',
...               '02 !': 'Silver',
...               '03 !': 'Bronze',
...               '? Winter': 'Winter Olympics',
...               '01 !.1': 'Gold.1',
...               '02 !.1': 'Silver.1',
...               '03 !.1': 'Bronze.1',
...               '? Games': '# Games',
...               '01 !.2': 'Gold.2',
...               '02 !.2': 'Silver.2',
...               '03 !.2': 'Bronze.2'}

我们在对象上调用rename()函数:

>>> olympics_df.rename(columns=new_names, inplace=True)

就地设置为True指定我们的更改直接作用于对象。让我们看看这是否属实:

>>> olympics_df.head()
 Country  Summer Olympics  Gold  Silver  Bronze  Total  \
0        Afghanistan (AFG)               13     0       0       2      2
1            Algeria (ALG)               12     5       2       8     15
2          Argentina (ARG)               23    18      24      28     70
3            Armenia (ARM)                5     1       2       9     12
4  Australasia (ANZ) [ANZ]                2     3       4       5     12

 Winter Olympics  Gold.1  Silver.1  Bronze.1  Total.1  # Games  Gold.2  \
0                0       0         0         0        0       13       0
1                3       0         0         0        0       15       5
2               18       0         0         0        0       41      18
3                6       0         0         0        0       11       1
4                0       0         0         0        0        2       3

 Silver.2  Bronze.2  Combined total
0         0         2               2
1         2         8              15
2        24        28              70
3         2         9              12
4         4         5              12

Remove ads

Python 数据清理:概述和资源

在本教程中,您学习了如何使用drop()函数从数据集中删除不必要的信息,以及如何为数据集设置索引,以便可以轻松引用其中的项目。

此外,您还学习了如何使用.str()访问器清理object字段,以及如何使用applymap()方法清理整个数据集。最后,我们探索了如何跳过 CSV 文件中的行并使用rename()方法重命名列。

了解数据清理非常重要,因为它是数据科学的一大部分。现在,您已经对如何利用 Pandas 和 NumPy 清理数据集有了基本的了解!

请查看下面的链接,找到对您的 Python 数据科学之旅有所帮助的其他资源:

  • 熊猫文档
  • NumPy 文档
  • 熊猫的创造者韦斯·麦金尼的数据分析 Python
  • 数据科学培训师兼顾问泰德·彼得鲁的熊猫食谱

免费奖励: 点击此处获取免费的 NumPy 资源指南,它会为您指出提高 NumPy 技能的最佳教程、视频和书籍。

立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: 用熊猫和 NumPy 进行数据清洗******

什么是数据工程,它适合你吗?

原文:https://realpython.com/python-data-engineer/

大数据。云数据。AI 训练数据和个人识别数据。数据无处不在,并且每天都在增长。软件工程已经发展到包括数据工程(一个直接关注数据传输、转换和存储的分支学科)是有意义的。

也许你已经看过大数据的招聘信息,并对处理 Pb 级数据的前景感兴趣。也许你很好奇生成性对抗网络是如何从底层数据中创造出逼真的图像的。也许你从未听说过数据工程,但对开发人员如何处理当今大多数应用程序所需的大量数据感兴趣。

无论你属于哪一类,这篇介绍性文章都适合你。你将对这个领域有一个大致的了解,包括什么是数据工程以及它需要什么样的工作。

在这篇文章中,你将了解到:

  • 数据工程领域的当前状态是什么
  • 数据工程在行业中是如何使用的
  • 数据工程师的各种客户是谁
  • 什么是数据工程领域的一部分,什么不是
  • 如何决定你是否想将数据工程作为一门学科来学习

首先,你要回答这个领域最紧迫的问题之一:数据工程师到底是做什么的?

免费奖励: 并学习 Python 3 的基础知识,如使用数据类型、字典、列表和 Python 函数。

数据工程师是做什么的?

数据工程是一个非常广泛的学科,有多个头衔。在许多组织中,它甚至可能没有特定的标题。因此,最好首先确定数据工程的目标,然后讨论什么样的工作会带来期望的结果。

数据工程的最终目标是提供有组织的、一致的数据流,以实现数据驱动的工作,例如:

  • 训练机器学习模型
  • 进行探索性数据分析
  • 用外部数据填充应用程序中的字段

这种数据流可以通过多种方式实现,并且所需的特定工具集、技术和技能在团队、组织和期望的结果之间会有很大的不同。然而,一种常见的模式是数据流水线。这是一个由独立程序组成的系统,这些程序对输入或收集的数据进行各种操作。

数据管道通常分布在多个服务器上:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此图是一个简化的数据管道示例,让您对可能遇到的架构有一个非常基本的了解。你会看到更复杂的表现形式。

数据可以来自任何来源:

  • 物联网设备
  • 车辆遥测技术
  • 房地产数据馈送
  • web 应用程序上的正常用户活动
  • 你能想到的任何其他收集或测量工具

根据这些来源的性质,传入的数据将在实时中进行处理,或者在批处理中以某种规则的节奏进行处理。

数据通过的管道是数据工程师的责任。数据工程团队负责设计、构建、维护、扩展,通常还负责支持数据管道的基础设施。他们还可能负责传入的数据,或者更常见的是负责数据模型以及数据最终是如何存储的。

如果您将数据管道视为一种应用程序,那么数据工程开始看起来像任何其他软件工程学科。

许多团队也在朝着建立数据平台的方向前进。在许多组织中,仅有一个管道将传入数据保存到某个地方的 SQL 数据库是不够的。大型组织有多个团队,他们需要对不同类型的数据进行不同级别的访问。

例如,人工智能(AI) 团队可能需要标注和拆分清洗过的数据的方法。商业智能(BI) 团队可能需要轻松访问来聚合数据和构建数据可视化。数据科学团队可能需要数据库级别的访问权限来正确地探索数据。

如果你熟悉 web 开发,那么你可能会发现这种结构类似于模型-视图-控制器(MVC)设计模式。使用 MVC,数据工程师负责模型,AI 或 BI 团队处理视图,所有团队在控制器上协作。对于拥有依赖数据访问的多样化团队的组织来说,构建满足所有这些需求的数据平台正成为首要任务。

现在,您已经了解了一些数据工程师的工作,以及他们与所服务的客户之间的关系,进一步了解这些客户以及数据工程师对他们的责任将会很有帮助。

Remove ads

数据工程师的职责是什么?

依赖数据工程师的客户就像数据工程团队本身的技能和产出一样多种多样。无论你从事什么领域,你的客户永远决定你解决什么问题,你如何解决问题。

在本节中,您将从数据需求的角度了解数据工程团队的一些常见客户:

  • 数据科学和人工智能团队
  • 商业智能或分析团队
  • 产品团队

在这些团队有效工作之前,必须满足某些需求。特别是,数据必须:

  • 可靠地路由到更广泛的系统中
  • 规范化为合理的数据模型
  • 清理以填补重要缺口
  • 向所有相关成员公开

Monica Rogarty 的优秀文章人工智能需求层次对这些需求进行了更全面的描述。作为一名数据工程师,您有责任满足客户的数据需求。但是,您将使用各种方法来适应他们各自的工作流。

数据流

要对系统中的数据做任何事情,您必须首先确保数据能够可靠地流入和通过系统。输入几乎可以是您能想到的任何类型的数据,包括:

  • JSON 或 XML 数据的实时流
  • 每小时更新一批视频
  • 每月抽血数据
  • 每周批量标记的图像
  • 来自部署传感器的遥测

数据工程师通常负责消费这些数据,设计一个系统,该系统可以将来自一个或多个来源的数据作为输入,转换这些数据,然后为客户存储这些数据。这些系统通常被称为 ETL 管道,分别代表提取转换加载

数据流责任主要属于提取步骤。但是数据工程师的职责并不仅限于将数据导入管道。他们必须确保管道足够健壮,以应对意外或畸形的数据、离线的源和致命的错误。正常运行时间非常重要,尤其是在使用实时数据或时间敏感型数据时。

无论你的客户是谁,你维护数据流的责任都是一致的。但是,有些客户可能比其他客户要求更高,尤其是当客户是一个依赖于实时更新数据的应用程序时。

数据标准化和建模

流入系统的数据是巨大的。然而,在某些时候,数据需要符合某种架构标准。规范化数据包括使用户更容易访问数据的任务。这包括但不限于以下步骤:

  • 删除重复项(重复数据删除)
  • 修复冲突数据
  • 使数据符合指定的数据模型

这些过程可能发生在不同的阶段。例如,假设您在一个大型组织中工作,有数据科学家和 BI 团队,他们都依赖于您的数据。您可以将非结构化数据存储在数据湖中,供您的数据科学客户用于探索性数据分析。您还可以将规范化的数据存储在一个关系数据库或一个更专门构建的数据仓库中,供 BI 团队在其报告中使用。

您可能有更多或更少的客户团队,或者可能有一个使用您的数据的应用程序。下图显示了先前管道示例的修改版本,突出显示了某些团队可能访问数据的不同阶段:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在此图中,您可以看到一个假设的数据管道,以及您经常会发现不同客户团队工作的各个阶段。

如果您的客户是一个产品团队,那么一个架构良好的数据模型是至关重要的。一个深思熟虑的数据模型可能是一个缓慢的、几乎没有响应的应用程序和一个运行起来好像已经知道用户想要访问什么数据的应用程序之间的区别。这类决策通常是产品和数据工程团队合作的结果。

数据规范化和建模通常是 ETL 的转换步骤的一部分,但它们不是这一类别中的唯一部分。另一个常见的转型步骤是数据清理。

Remove ads

数据清理

数据清理与数据标准化齐头并进。有些人甚至认为数据规范化是数据清理的一个子集。但是,虽然数据规范化主要关注于使不同的数据符合某种数据模型,但数据清理包括许多使数据更加统一和完整的操作,包括:

  • 将相同的数据转换为单一类型(例如,强制整数字段中的字符串成为整数
  • 确保日期格式一致
  • 如果可能,填写缺失的字段
  • 将字段的值约束到指定的范围
  • 删除损坏或不可用的数据

数据清理可以纳入上图中的重复数据消除和统一数据模型步骤。但实际上,这些步骤中的每一步都非常庞大,可以包含任意数量的阶段和单独的过程。

您采取的清理数据的具体操作将高度依赖于输入、数据模型和期望的结果。然而,干净数据的重要性是不变的:

  • 数据科学家需要它来执行精确的分析。
  • 机器学习工程师需要它来建立精确的、可推广的模型。
  • 商业智能团队需要 it 为企业提供准确的报告和预测。
  • 产品团队需要它来确保他们的产品不会崩溃或者给用户错误的信息。

数据清理的责任落在许多不同的肩上,取决于整个组织及其优先级。作为一名数据工程师,您应该努力尽可能地实现自动化清理,并对传入和存储的数据进行定期抽查。您的客户团队和领导层可以提供关于什么构成符合其目的的干净数据的见解。

数据可访问性

数据可访问性没有得到像数据规范化和清理那样多的关注,但它可以说是以客户为中心的数据工程团队更重要的职责之一。

数据可访问性是指数据对于客户来说访问和理解的难易程度。这一点根据客户的不同而有不同的定义:

  • 数据科学团队可能只需要可以用某种查询语言访问的数据。
  • 分析团队可能更喜欢按某种指标分组的数据,可通过基本查询或报告界面访问。
  • 考虑到产品性能和可靠性,产品团队通常希望数据可以通过快速、简单的查询获得,并且不会经常改变。

因为较大的组织为这些团队和其他团队提供相同的数据,所以许多组织已经开始为不同的团队开发自己的内部平台。这方面一个非常成熟的例子是打车服务优步,它分享了其令人印象深刻的大数据平台的许多细节。

事实上,许多数据工程师发现自己正在成为平台工程师,这表明了数据工程技能对数据驱动型企业的持续重要性。因为数据可访问性与数据的存储方式密切相关,所以它是 ETL 的 load 步骤的主要组成部分,它指的是如何存储数据以备后用。

现在,您已经遇到了一些常见的数据工程客户,并了解了他们的需求,是时候更仔细地看看您可以开发哪些技能来帮助满足这些需求了。

有哪些常见的数据工程技能?

数据工程技能在很大程度上与你从事软件工程所需的技能相同。然而,有几个领域是数据工程师更关注的。在本节中,您将了解几项重要的技能:

  • 一般编程概念
  • 数据库
  • 分布式系统和云工程

在让你成为一名全面发展的数据工程师的过程中,上述每一项都将发挥至关重要的作用。

通用编程技巧

数据工程是软件工程的一个专门化,所以软件工程的基础在这个列表的顶部是有意义的。与其他软件工程专业一样,数据工程师应该理解设计概念,如 DRY(不要重复)面向对象编程数据结构和算法。

和其他专业一样,也有少数偏爱的语言。在撰写本文时,你在数据工程工作描述中最常看到的是 Python、Scala 和 Java 。是什么让这些语言如此受欢迎?

Python 受欢迎有几个原因。其中最大的一个是它的普遍性。从很多方面来看,Python 是世界上最流行的三种编程语言之一。例如,它在 2020 年 11 月的 TIOBE 社区指数中排名第二,在 Stack Overflow 的 2020 开发者调查中排名第三。

它也被机器学习和人工智能团队广泛使用。紧密合作的团队经常需要能够用同一种语言交流,而 Python 仍然是这个领域的通用语言。

Python 流行的另一个更有针对性的原因是它在编排工具中的使用,如 Apache Airflow 和流行工具的可用库,如 Apache Spark 。如果一个组织使用这样的工具,那么了解他们使用的语言是很重要的。

Scala 也很受欢迎,和 Python 一样,这部分是由于使用它的工具的流行,尤其是 Apache Spark。Scala 是一种运行在 Java 虚拟机(JVM)上的函数式语言,这使得它能够与 Java 无缝地结合使用。

Java 在数据工程中并不那么受欢迎,但是你仍然会在一些工作描述中看到它。这部分是因为它在企业软件栈中的普遍性,部分是因为它与 Scala 的互操作性。随着 Scala 被用于 Apache Spark,一些团队也使用 Java 是有道理的。

除了一般的编程技能之外,熟悉数据库技术也是必不可少的。

Remove ads

数据库技术

如果您要移动数据,那么您将会大量使用数据库。非常宽泛地说,您可以将数据库技术分为两类:SQL 和 NoSQL。

SQL 数据库是关系数据库管理系统 (RDBMS),它对关系进行建模,并通过使用结构化查询语言或 SQL 进行交互。这些通常用于对由关系定义的数据进行建模,例如客户订单数据。

**注意:**如果你想学习更多关于 SQL 的知识,以及如何用 Python 与 SQL 数据库进行交互,那么请查看Python SQL 库简介

NoSQL 通常意味着“其他一切”这些数据库通常存储非关系数据,如下所示:

虽然不要求您了解所有数据库技术的来龙去脉,但是您应该了解这些不同系统的优缺点,并能够快速学习其中的一两种。

数据工程师工作的系统越来越多地位于云上,数据管道通常分布在多个服务器或集群上,无论是否在私有云上。因此,未来的数据工程师应该了解分布式系统和云工程。

分布式系统和云工程

诸如 ETL 管道之类的数据工程技术的主要优势之一是,它们有助于实现分布式系统。一种常见的模式是让管道的独立部分运行在单独的服务器上,由像 RabbitMQApache Kafka 这样的消息队列来编排。

了解如何设计这些系统,它们的好处和风险是什么,以及何时应该使用它们是非常重要的。

这些系统需要许多服务器,地理上分散的团队经常需要访问它们包含的数据。Amazon Web Services、Google Cloud 和 Microsoft Azure 等私有云提供商是构建和部署分布式系统的非常流行的工具。

对云提供商的主要产品以及一些更流行的分布式消息传递工具的基本了解将有助于您找到第一份数据工程工作。你可以在工作中更深入地学习这些工具。

到目前为止,您已经了解了很多关于什么是数据工程的知识。但是因为这个学科没有标准的定义,而且因为有很多相关的学科,你也应该知道什么是数据工程而不是

什么不是数据工程?

许多领域与数据工程密切相关,您的客户通常是这些领域的成员。了解你的客户很重要,所以你应该了解这些领域以及它们与数据工程的区别。

以下是一些与数据工程密切相关的领域:

  • 数据科学
  • 商业智能
  • 机器学习工程

在本节中,您将从数据科学开始,更仔细地了解这些领域。

数据科学

如果说数据工程是由如何移动和组织海量数据决定的,那么数据科学则是由如何处理这些数据决定的。

数据科学家通常会查询、探索并尝试从数据集中获得见解。他们可能编写用于特定数据集的一次性脚本,而数据工程师倾向于使用软件工程最佳实践创建可重用的程序。

数据科学家使用统计工具,如 k 均值聚类回归以及机器学习技术。他们经常使用 R 或 Python,并试图从数据中获得洞察力和预测,以指导企业各个层面的决策。

**注:**你想探索数据科学吗?看看以下任何一种学习途径:

数据科学家通常来自科学或统计背景,他们的工作风格反映了这一点。他们从事一个项目,回答一个特定的研究问题,而数据工程团队专注于构建可扩展的、可重用的、快速的内部产品。

数据科学家回答研究问题的一个很好的例子可以在生物技术和健康技术公司找到,在那里,数据科学家探索药物相互作用、副作用、疾病结果等数据。

Remove ads

商业智能

商业智能类似于数据科学,但有一些重要的区别。数据科学侧重于预测和做出未来预测,而商业智能侧重于提供业务当前状态的视图。

这两个组都由数据工程团队提供服务,甚至可能来自同一个数据池。然而,商业智能关注的是分析业务绩效并从数据中生成报告。然后,这些报告帮助管理层在业务层面做出决策。

像数据科学家一样,商业智能团队依靠数据工程师来构建工具,使他们能够分析和报告与其关注领域相关的数据。

机器学习工程

机器学习工程师是你会经常接触到的另一个群体。你可能和他们做类似的工作,或者你甚至可能被嵌入到一个机器学习工程师的团队中。

像数据工程师一样,机器学习工程师更专注于构建可重用的软件,许多人都有计算机科学背景。然而,他们不太专注于构建应用程序,而是更专注于构建机器学习模型或设计用于模型的新算法。

**注:**如果你对机器学习领域感兴趣,那就去看看用 Python 进行机器学习的学习路径吧。

机器学习工程师建立的模型经常被产品团队用于面向客户的产品中。你作为数据工程师提供的数据将用于训练他们的模型,使你的工作成为任何与你合作的机器学习团队的能力的基础。

例如,机器学习工程师可能为您公司的产品开发新的推荐算法,而数据工程师将提供用于训练和测试该算法的数据。

需要理解的一件重要事情是,您在这里看到的字段通常并不清晰。具有数据科学、BI 或机器学习背景的人可能会在某个组织中从事数据工程工作,作为一名数据工程师,您可能会被要求协助这些团队的工作。

你可能会发现自己某一天重新构建了一个数据模型,另一天构建了一个数据标签工具,然后优化了一个内部深度学习框架。好的数据工程师灵活、好奇,并且愿意尝试新事物。

结论

这就完成了你对数据工程领域的介绍,这是对有计算机科学和技术背景或兴趣的人最需要的学科之一!

在本教程中,您学习了:

  • 数据工程师做什么
  • 谁是数据工程师的客户
  • 哪些技能是数据工程中常见的
  • 什么数据工程不是

现在你可以决定是否要深入了解这个令人兴奋的领域。你对数据工程感兴趣吗?你有兴趣更深入地探索它吗?请在评论中告诉我们!****

通用 Python 数据结构(指南)

原文:https://realpython.com/python-data-structures/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 栈和队列:选择理想的数据结构

数据结构是构建程序的基础结构。每种数据结构都提供了一种特定的组织数据的方式,因此可以根据您的使用情况有效地访问数据。Python 在其标准库中附带了一组广泛的数据结构。

然而,Python 的命名约定并不像其他语言那样清晰。在 Java 中,列表不仅仅是一个list——它或者是一个LinkedList或者是一个ArrayList。在 Python 中并非如此。即使是有经验的 Python 开发人员有时也会怀疑内置的list类型是作为链表还是动态数组实现的。

在本教程中,您将学习:

  • Python 标准库中内置了哪些常见的抽象数据类型
  • 最常见的抽象数据类型如何映射到 Python 的命名方案
  • 如何在各种算法中把抽象数据类型实际运用

**注:**本教程改编自 Python 招数:书 中“Python 中常见的数据结构”一章。如果你喜欢下面的内容,那么一定要看看这本书的其余部分。

免费下载: 从 Python 技巧中获取一个示例章节:这本书用简单的例子向您展示了 Python 的最佳实践,您可以立即应用它来编写更漂亮的+Python 代码。

字典、地图和哈希表

在 Python 中,字典(或简称为字典)是一个中心数据结构。字典存储任意数量的对象,每个对象都由唯一的字典标识。

字典也经常被称为映射哈希表查找表,或者关联数组。它们允许高效地查找、插入和删除与给定键相关联的任何对象。

电话簿是字典对象的真实模拟。它们允许您快速检索与给定键(人名)相关的信息(电话号码)。你可以或多或少地直接跳到一个名字,并查找相关信息,而不必从头到尾阅读电话簿来查找某人的号码。

当涉及到如何组织信息以允许快速查找时,这个类比就有些站不住脚了。但是基本的性能特征保持不变。字典允许您快速找到与给定关键字相关的信息。

字典是计算机科学中最重要和最常用的数据结构之一。那么,Python 是如何处理字典的呢?让我们浏览一下核心 Python 和 Python 标准库中可用的字典实现。

Remove ads

dict:您的首选词典

因为字典非常重要,Python 提供了一个健壮的字典实现,它直接内置在核心语言中: dict 数据类型。

Python 还提供了一些有用的语法糖,用于在程序中使用字典。例如,花括号({ })字典表达式语法和字典理解允许您方便地定义新的字典对象:

>>> phonebook = {
...     "bob": 7387,
...     "alice": 3719,
...     "jack": 7052,
... }

>>> squares = {x: x * x for x in range(6)}

>>> phonebook["alice"]
3719

>>> squares
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

对于哪些对象可以用作有效的键有一些限制。

Python 的字典是通过关键字索引的,关键字可以是任何哈希类型。一个 hashable 对象有一个哈希值,这个哈希值在它的生命周期内不会改变(见__hash__),它可以和其他对象进行比较(见__eq__)。被比较为相等的可散列对象必须具有相同的散列值。

不可变类型字符串数字是可散列的,并且作为字典键工作得很好。你也可以使用 tuple对象作为字典键,只要它们本身只包含可散列类型。

对于大多数用例,Python 的内置字典实现将完成您需要的一切。字典是高度优化的,是语言的许多部分的基础。例如,类属性堆栈框架中的变量都存储在字典内部。

Python 字典基于一个经过良好测试和微调的哈希表实现,它提供了您所期望的性能特征: O (1)一般情况下查找、插入、更新和删除操作的时间复杂度。

没有理由不使用 Python 中包含的标准dict实现。然而,存在专门的第三方字典实现,例如跳过列表基于 B 树的字典。

除了普通的dict对象,Python 的标准库还包括许多专门的字典实现。这些专门的字典都基于内置的字典类(并共享其性能特征),但也包括一些额外的便利特性。

让我们来看看它们。

collections.OrderedDict:记住按键的插入顺序

Python 包含一个专门的dict子类,它会记住添加到其中的键的插入顺序: collections.OrderedDict

注意: OrderedDict不是核心语言的内置部分,必须从标准库中的collections模块导入。

虽然在 CPython 3.6 和更高版本中,标准的dict实例保留了键的插入顺序,但这只是 CPython 实现的一个副作用,直到 Python 3.7 才在语言规范中定义。因此,如果键的顺序对算法的工作很重要,那么最好通过显式地使用OrderedDict类来清楚地表达这一点:

>>> import collections
>>> d = collections.OrderedDict(one=1, two=2, three=3)

>>> d
OrderedDict([('one', 1), ('two', 2), ('three', 3)])

>>> d["four"] = 4
>>> d
OrderedDict([('one', 1), ('two', 2),
 ('three', 3), ('four', 4)])

>>> d.keys()
odict_keys(['one', 'two', 'three', 'four'])

Python 3.8 之前,不能使用reversed()逆序迭代字典条目。只有OrderedDict实例提供该功能。即使在 Python 3.8 中,dictOrderedDict对象也不完全相同。OrderedDict实例有一个普通dict实例没有的 .move_to_end()方法,以及一个比普通dict实例更加可定制的 .popitem()方法

collections.defaultdict:返回缺失键的默认值

defaultdict 类是另一个字典子类,在其构造函数中接受一个 callable,如果找不到请求的键,将使用其返回值。

与在常规字典中使用get()或捕捉 KeyError异常相比,这可以节省您的一些输入,并使您的意图更加清晰:

>>> from collections import defaultdict
>>> dd = defaultdict(list)

>>> # Accessing a missing key creates it and
>>> # initializes it using the default factory,
>>> # i.e. list() in this example:
>>> dd["dogs"].append("Rufus")
>>> dd["dogs"].append("Kathrin")
>>> dd["dogs"].append("Mr Sniffles")

>>> dd["dogs"]
['Rufus', 'Kathrin', 'Mr Sniffles']

Remove ads

collections.ChainMap:将多个字典作为单个映射进行搜索

collections.ChainMap 数据结构将多个字典组合成一个映射。查找逐个搜索底层映射,直到找到一个键。插入、更新和删除仅影响添加到链中的第一个映射:

>>> from collections import ChainMap
>>> dict1 = {"one": 1, "two": 2}
>>> dict2 = {"three": 3, "four": 4}
>>> chain = ChainMap(dict1, dict2)

>>> chain
ChainMap({'one': 1, 'two': 2}, {'three': 3, 'four': 4})

>>> # ChainMap searches each collection in the chain
>>> # from left to right until it finds the key (or fails):
>>> chain["three"]
3
>>> chain["one"]
1
>>> chain["missing"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'missing'

types.MappingProxyType:用于制作只读字典的包装器

MappingProxyType 是一个标准字典的包装器,它提供了对包装字典数据的只读视图。这个类是在 Python 3.3 中添加的,可以用来创建不可变的字典代理版本。

例如,如果你想从一个类或模块返回一个携带内部状态的字典,同时阻止对这个对象的写访问,那么MappingProxyType会很有帮助。使用MappingProxyType允许您设置这些限制,而不必首先创建字典的完整副本:

>>> from types import MappingProxyType
>>> writable = {"one": 1, "two": 2}
>>> read_only = MappingProxyType(writable)

>>> # The proxy is read-only:
>>> read_only["one"]
1
>>> read_only["one"] = 23
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment

>>> # Updates to the original are reflected in the proxy:
>>> writable["one"] = 42
>>> read_only
mappingproxy({'one': 42, 'two': 2})

Python 中的字典:摘要

本教程中列出的所有 Python 字典实现都是内置于 Python 标准库中的有效实现。

如果您正在寻找在程序中使用哪种映射类型的一般建议,我会向您推荐内置的dict数据类型。它是一个通用的、优化的哈希表实现,直接内置于核心语言中。

我建议您使用这里列出的其他数据类型,除非您有超出dict所提供的特殊需求。

所有的实现都是有效的选择,但是如果你的代码大部分时间都依赖于标准的 Python 字典,那么你的代码将会更加清晰和易于维护。

数组数据结构

一个数组是大多数编程语言中可用的基本数据结构,它在不同的算法中有广泛的用途。

在本节中,您将了解 Python 中的数组实现,这些实现只使用 Python 标准库中包含的核心语言特性或功能。您将看到每种方法的优点和缺点,因此您可以决定哪种实现适合您的用例。

但是在我们开始之前,让我们先了解一些基础知识。数组是如何工作的,它们有什么用途?数组由固定大小的数据记录组成,允许根据索引有效地定位每个元素:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为数组将信息存储在相邻的内存块中,所以它们被认为是连续的数据结构(例如,相对于像链表这样的链接的数据结构)。

数组数据结构的真实类比是停车场。你可以把停车场看作一个整体,把它当作一个单独的对象,但是在停车场内部,有一些停车位,它们由一个唯一的数字索引。停车点是车辆的容器——每个停车点可以是空的,也可以停放汽车、摩托车或其他车辆。

但并不是所有的停车场都一样。一些停车场可能仅限于一种类型的车辆。例如,一个房车停车场不允许自行车停在上面。受限停车场对应于一个类型的数组数据结构,它只允许存储相同数据类型的元素。

就性能而言,根据元素的索引查找数组中包含的元素非常快。对于这种情况,适当的阵列实现保证了恒定的 O (1)访问时间。

Python 在其标准库中包含了几个类似数组的数据结构,每个结构都有略微不同的特征。让我们来看看。

Remove ads

list:可变动态数组

列表是核心 Python 语言的一部分。尽管名字如此,Python 的列表在幕后被实现为动态数组

这意味着列表允许添加或删除元素,并且列表将通过分配或释放内存来自动调整保存这些元素的后备存储。

Python 列表可以保存任意元素——Python 中的一切都是对象,包括函数。因此,您可以混合和匹配不同种类的数据类型,并将它们全部存储在一个列表中。

这可能是一个强大的功能,但缺点是同时支持多种数据类型意味着数据通常不太紧凑。因此,整个结构占据了更多的空间:

>>> arr = ["one", "two", "three"]
>>> arr[0]
'one'

>>> # Lists have a nice repr:
>>> arr
['one', 'two', 'three']

>>> # Lists are mutable:
>>> arr[1] = "hello"
>>> arr
['one', 'hello', 'three']

>>> del arr[1]
>>> arr
['one', 'three']

>>> # Lists can hold arbitrary data types:
>>> arr.append(23)
>>> arr
['one', 'three', 23]

tuple:不可变容器

就像列表一样,元组是 Python 核心语言的一部分。然而,与列表不同,Python 的tuple对象是不可变的。这意味着不能动态地添加或删除元素——元组中的所有元素都必须在创建时定义。

元组是另一种可以保存任意数据类型元素的数据结构。拥有这种灵活性是非常强大的,但是同样,这也意味着数据没有在类型化数组中那么紧密:

>>> arr = ("one", "two", "three")
>>> arr[0]
'one'

>>> # Tuples have a nice repr:
>>> arr
('one', 'two', 'three')

>>> # Tuples are immutable:
>>> arr[1] = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

>>> del arr[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object doesn't support item deletion

>>> # Tuples can hold arbitrary data types:
>>> # (Adding elements creates a copy of the tuple)
>>> arr + (23,)
('one', 'two', 'three', 23)

array.array:基本类型数组

Python 的array模块为基本的 C 风格数据类型(如字节、32 位整数、浮点数等)提供了节省空间的存储。

array.array 类创建的数组是可变的,其行为类似于列表,除了一个重要的区别:它们是被限制为单一数据类型的类型化数组

由于这个限制,array.array具有许多元素的对象比列表和元组更有空间效率。存储在其中的元素被紧密打包,如果您需要存储许多相同类型的元素,这可能会很有用。

此外,数组支持许多与常规列表相同的方法,并且您可以将它们作为一种替代方法来使用,而无需对应用程序代码进行其他更改。

>>> import array
>>> arr = array.array("f", (1.0, 1.5, 2.0, 2.5))
>>> arr[1]
1.5

>>> # Arrays have a nice repr:
>>> arr
array('f', [1.0, 1.5, 2.0, 2.5])

>>> # Arrays are mutable:
>>> arr[1] = 23.0
>>> arr
array('f', [1.0, 23.0, 2.0, 2.5])

>>> del arr[1]
>>> arr
array('f', [1.0, 2.0, 2.5])

>>> arr.append(42.0)
>>> arr
array('f', [1.0, 2.0, 2.5, 42.0])

>>> # Arrays are "typed":
>>> arr[1] = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be real number, not str

str:不可变的 Unicode 字符数组

Python 3.x 使用 str 对象将文本数据存储为 Unicode 字符的不可变序列。实际上,这意味着str是一个不可变的字符数组。奇怪的是,它也是一个递归数据结构——字符串中的每个字符本身都是一个长度为 1 的str对象。

字符串对象是空间高效的,因为它们被紧密地打包,并且它们专门用于一种数据类型。如果你存储的是 Unicode 文本,那么你应该使用一个字符串。

因为字符串在 Python 中是不可变的,所以修改字符串需要创建一个修改后的副本。与可变字符串最接近的等效方式是在列表中存储单个字符:

>>> arr = "abcd"
>>> arr[1]
'b'

>>> arr
'abcd'

>>> # Strings are immutable:
>>> arr[1] = "e"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

>>> del arr[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object doesn't support item deletion

>>> # Strings can be unpacked into a list to
>>> # get a mutable representation:
>>> list("abcd")
['a', 'b', 'c', 'd']
>>> "".join(list("abcd"))
'abcd'

>>> # Strings are recursive data structures:
>>> type("abc")
"<class 'str'>"
>>> type("abc"[0])
"<class 'str'>"

Remove ads

bytes:不可变的单字节数组

bytes 对象是不可变的单字节序列,或者 0 ≤ x ≤ 255 范围内的整数。从概念上讲,bytes对象类似于str对象,你也可以把它们看作不可变的字节数组。

像字符串一样,bytes有自己的文字语法来创建对象,并且空间效率高。bytes对象是不可变的,但与字符串不同,有一个专用的可变字节数组数据类型,称为bytearray,它们可以被解压到:

>>> arr = bytes((0, 1, 2, 3))
>>> arr[1]
1

>>> # Bytes literals have their own syntax:
>>> arr
b'\x00\x01\x02\x03'
>>> arr = b"\x00\x01\x02\x03"

>>> # Only valid `bytes` are allowed:
>>> bytes((0, 300))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: bytes must be in range(0, 256)

>>> # Bytes are immutable:
>>> arr[1] = 23
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

>>> del arr[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object doesn't support item deletion

bytearray:单字节可变数组

bytearray 类型是一个可变的整数序列,范围为 0 ≤ x ≤ 255。bytearray对象与bytes对象密切相关,主要区别在于bytearray可以自由修改——您可以覆盖元素、删除现有元素或添加新元素。bytearray物体会相应地增大和缩小。

一个bytearray可以被转换回不可变的bytes对象,但是这涉及到完全复制存储的数据——一个花费 O ( n )时间的缓慢操作:

>>> arr = bytearray((0, 1, 2, 3))
>>> arr[1]
1

>>> # The bytearray repr:
>>> arr
bytearray(b'\x00\x01\x02\x03')

>>> # Bytearrays are mutable:
>>> arr[1] = 23
>>> arr
bytearray(b'\x00\x17\x02\x03')

>>> arr[1]
23

>>> # Bytearrays can grow and shrink in size:
>>> del arr[1]
>>> arr
bytearray(b'\x00\x02\x03')

>>> arr.append(42)
>>> arr
bytearray(b'\x00\x02\x03*')

>>> # Bytearrays can only hold `bytes`
>>> # (integers in the range 0 <= x <= 255)
>>> arr[1] = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object cannot be interpreted as an integer

>>> arr[1] = 300
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: byte must be in range(0, 256)

>>> # Bytearrays can be converted back into bytes objects:
>>> # (This will copy the data)
>>> bytes(arr)
b'\x00\x02\x03*'

Python 中的数组:摘要

在 Python 中实现数组时,有许多内置的数据结构可供选择。在本节中,您已经关注了标准库中包含的核心语言特性和数据结构。

如果你愿意超越 Python 标准库,那么像 NumPypandas 这样的第三方包为科学计算和数据科学提供了广泛的快速数组实现。

如果您想将自己局限于 Python 中包含的数组数据结构,那么这里有一些指导原则:

  • 如果您需要存储任意对象,可能是混合数据类型,那么使用listtuple,这取决于您是否想要一个不可变的数据结构。

  • 如果您有数字(整数或浮点)数据,并且紧密封装和性能很重要,那么尝试一下array.array

  • 如果您有表示为 Unicode 字符的文本数据,那么使用 Python 的内置str。如果你需要一个可变的类似字符串的数据结构,那么使用字符的list

  • 如果您想要存储一个连续的字节块,那么使用不可变的bytes类型,或者如果您需要可变的数据结构,使用bytearray

在大多数情况下,我喜欢从简单的list开始。如果性能或存储空间成为一个问题,我将稍后专门讨论。很多时候,使用像list这样的通用数组数据结构,可以给你最快的开发速度和最大的编程便利。

我发现这通常在开始时比从一开始就试图挤出最后一滴表现要重要得多。

记录、结构和数据传输对象

与数组相比,记录数据结构提供了固定数量的字段。每个字段可以有一个名称,也可以有不同的类型。

在本节中,您将看到如何仅使用标准库中的内置数据类型和类在 Python 中实现记录、结构和普通的旧数据对象。

**注意:**我在这里不严格地使用记录的定义。例如,我还将讨论像 Python 的内置tuple这样的类型,它们在严格意义上可能被认为是记录,也可能不被认为是记录,因为它们不提供命名字段。

Python 提供了几种数据类型,可用于实现记录、结构和数据传输对象。在本节中,您将快速了解每个实现及其独特的特征。最后,你会发现一个总结和一个决策指南,可以帮助你做出自己的选择。

**注:**本教程改编自 Python 招数:书 中“Python 中常见的数据结构”一章。如果你喜欢你正在阅读的东西,那么一定要看看这本书的其余部分。

好吧,让我们开始吧!

Remove ads

dict:简单数据对象

正如前面提到的,Python 字典存储任意数量的对象,每个对象由一个惟一的键标识。字典通常也被称为映射关联数组,允许高效地查找、插入和删除与给定键相关的任何对象。

在 Python 中使用字典作为记录数据类型或数据对象是可能的。Python 中的字典很容易创建,因为它们以字典文字的形式在语言中内置了自己的语法糖。该词典语法简洁,打字十分方便。

使用字典创建的数据对象是可变的,而且几乎没有防止字段名拼写错误的保护措施,因为字段可以随时自由添加和删除。这两个属性都会引入令人惊讶的错误,并且总是要在便利性和错误恢复能力之间进行权衡:

>>> car1 = {
...     "color": "red",
...     "mileage": 3812.4,
...     "automatic": True,
... }
>>> car2 = {
...     "color": "blue",
...     "mileage": 40231,
...     "automatic": False,
... }

>>> # Dicts have a nice repr:
>>> car2
{'color': 'blue', 'automatic': False, 'mileage': 40231}

>>> # Get mileage:
>>> car2["mileage"]
40231

>>> # Dicts are mutable:
>>> car2["mileage"] = 12
>>> car2["windshield"] = "broken"
>>> car2
{'windshield': 'broken', 'color': 'blue',
 'automatic': False, 'mileage': 12}

>>> # No protection against wrong field names,
>>> # or missing/extra fields:
>>> car3 = {
...     "colr": "green",
...     "automatic": False,
...     "windshield": "broken",
... }

tuple:不可变的对象组

Python 的元组是对任意对象进行分组的直接数据结构。元组是不可变的——它们一旦被创建就不能被修改。

就性能而言,在 CPython 中,元组占用的内存列表略少,而且它们的构建速度也更快。

正如您在下面的字节码反汇编中看到的,构造一个元组常量只需要一个LOAD_CONST操作码,而构造一个具有相同内容的列表对象需要更多的操作:

>>> import dis
>>> dis.dis(compile("(23, 'a', 'b', 'c')", "", "eval"))
 0 LOAD_CONST           4 ((23, "a", "b", "c"))
 3 RETURN_VALUE

>>> dis.dis(compile("[23, 'a', 'b', 'c']", "", "eval"))
 0 LOAD_CONST           0 (23)
 3 LOAD_CONST           1 ('a')
 6 LOAD_CONST           2 ('b')
 9 LOAD_CONST           3 ('c')
 12 BUILD_LIST           4
 15 RETURN_VALUE

然而,你不应该过分强调这些差异。在实践中,性能差异通常可以忽略不计,试图通过从列表切换到元组来挤出程序的额外性能可能是错误的方法。

普通元组的一个潜在缺点是,存储在其中的数据只能通过整数索引访问才能取出。不能给存储在元组中的单个属性命名。这可能会影响代码的可读性。

此外,一个元组总是一个特别的结构:很难确保两个元组中存储了相同数量的字段和相同的属性。

这很容易引入疏忽的错误,比如混淆了字段顺序。因此,我建议您尽可能减少存储在元组中的字段数量:

>>> # Fields: color, mileage, automatic
>>> car1 = ("red", 3812.4, True)
>>> car2 = ("blue", 40231.0, False)

>>> # Tuple instances have a nice repr:
>>> car1
('red', 3812.4, True)
>>> car2
('blue', 40231.0, False)

>>> # Get mileage:
>>> car2[1]
40231.0

>>> # Tuples are immutable:
>>> car2[1] = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

>>> # No protection against missing or extra fields
>>> # or a wrong order:
>>> car3 = (3431.5, "green", True, "silver")

编写自定义类:更多工作,更多控制

允许您为数据对象定义可重用的蓝图,以确保每个对象提供相同的字段集。

使用常规 Python 类作为记录数据类型是可行的,但是也需要手工操作才能获得其他实现的便利特性。例如,向__init__构造函数添加新字段是冗长且耗时的。

此外,从自定义类实例化的对象的默认字符串表示也没什么帮助。为了解决这个问题,你可能需要添加你自己的 __repr__ 方法,这通常也是相当冗长的,每次你添加一个新的字段时都必须更新。

存储在类上的字段是可变的,新字段可以自由添加,您可能喜欢也可能不喜欢。使用 @property 装饰器可以提供更多的访问控制并创建只读字段,但是这又需要编写更多的粘合代码。

每当您想使用方法向记录对象添加业务逻辑和行为时,编写自定义类是一个很好的选择。然而,这意味着这些对象在技术上不再是普通的数据对象:

>>> class Car:
...     def __init__(self, color, mileage, automatic):
...         self.color = color
...         self.mileage = mileage
...         self.automatic = automatic
...
>>> car1 = Car("red", 3812.4, True)
>>> car2 = Car("blue", 40231.0, False)

>>> # Get the mileage:
>>> car2.mileage
40231.0

>>> # Classes are mutable:
>>> car2.mileage = 12
>>> car2.windshield = "broken"

>>> # String representation is not very useful
>>> # (must add a manually written __repr__ method):
>>> car1
<Car object at 0x1081e69e8>

Remove ads

dataclasses.dataclass : Python 3.7+数据类

数据类在 Python 3.7 及以上版本中可用。它们为从头定义自己的数据存储类提供了一个很好的选择。

通过编写一个数据类而不是普通的 Python 类,您的对象实例获得了一些现成的有用特性,这将为您节省一些键入和手动实现工作:

  • 定义实例变量的语法更短,因为您不需要实现.__init__()方法。
  • 数据类的实例通过自动生成的.__repr__()方法自动获得好看的字符串表示。
  • 实例变量接受类型注释,使您的数据类在一定程度上是自文档化的。请记住,类型注释只是一些提示,如果没有单独的类型检查工具,这些提示是不会生效的。

数据类通常是使用@dataclass 装饰器创建的,您将在下面的代码示例中看到:

>>> from dataclasses import dataclass
>>> @dataclass
... class Car:
...     color: str
...     mileage: float
...     automatic: bool
...
>>> car1 = Car("red", 3812.4, True)

>>> # Instances have a nice repr:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

>>> # Accessing fields:
>>> car1.mileage
3812.4

>>> # Fields are mutable:
>>> car1.mileage = 12
>>> car1.windshield = "broken"

>>> # Type annotations are not enforced without
>>> # a separate type checking tool like mypy:
>>> Car("red", "NOT_A_FLOAT", 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)

要了解更多关于 Python 数据类的信息,请查看Python 3.7中数据类的终极指南。

collections.namedtuple:方便的数据对象

Python 2.6+中可用的 namedtuple 类提供了内置tuple数据类型的扩展。类似于定义一个定制类,使用namedtuple允许您为您的记录定义可重用的蓝图,确保使用正确的字段名称。

对象是不可变的,就像正则元组一样。这意味着在创建了namedtuple实例之后,您不能添加新字段或修改现有字段。

除此之外,namedtuple物体是,嗯。。。命名元组。存储在其中的每个对象都可以通过唯一的标识符来访问。这使您不必记住整数索引或求助于变通方法,如定义整数常量作为索引的助记符

对象在内部被实现为常规的 Python 类。就内存使用而言,它们也比常规类更好,内存效率与常规元组一样高:

>>> from collections import namedtuple
>>> from sys import getsizeof

>>> p1 = namedtuple("Point", "x y z")(1, 2, 3)
>>> p2 = (1, 2, 3)

>>> getsizeof(p1)
64
>>> getsizeof(p2)
64

对象是清理代码的一种简单方法,通过为数据实施更好的结构,使代码更具可读性。

我发现,从特定的数据类型(如具有固定格式的字典)到namedtuple对象有助于我更清楚地表达代码的意图。通常当我应用这种重构时,我会神奇地为我面临的问题想出一个更好的解决方案。

在常规(非结构化)元组和字典上使用namedtuple对象也可以让你的同事的生活变得更轻松,至少在某种程度上,这是通过让正在传递的数据自文档化来实现的:

>>> from collections import namedtuple
>>> Car = namedtuple("Car" , "color mileage automatic")
>>> car1 = Car("red", 3812.4, True)

>>> # Instances have a nice repr:
>>> car1
Car(color="red", mileage=3812.4, automatic=True)

>>> # Accessing fields:
>>> car1.mileage
3812.4

>>> # Fields are immtuable:
>>> car1.mileage = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

>>> car1.windshield = "broken"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute 'windshield'

typing.NamedTuple:改进的命名元组

Python 3.6 中新增, typing.NamedTuplecollections模块中namedtuple类的弟弟。它与namedtuple非常相似,主要区别是定义新记录类型的更新语法和增加对类型提示的支持。

请注意,如果没有像 mypy 这样的独立类型检查工具,类型注释是不会生效的。但是,即使没有工具支持,它们也可以为其他程序员提供有用的提示(或者,如果类型提示过时,就会变得非常混乱):

>>> from typing import NamedTuple

>>> class Car(NamedTuple):
...     color: str
...     mileage: float
...     automatic: bool

>>> car1 = Car("red", 3812.4, True)

>>> # Instances have a nice repr:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

>>> # Accessing fields:
>>> car1.mileage
3812.4

>>> # Fields are immutable:
>>> car1.mileage = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

>>> car1.windshield = "broken"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Car' object has no attribute 'windshield'

>>> # Type annotations are not enforced without
>>> # a separate type checking tool like mypy:
>>> Car("red", "NOT_A_FLOAT", 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)

Remove ads

struct.Struct:序列化的 C 结构

struct.Struct 类在 Python 值和序列化为 Python bytes对象的 C 结构之间进行转换。例如,它可以用于处理存储在文件中或来自网络连接的二进制数据。

使用基于格式字符串的迷你语言来定义结构,这允许你定义各种 C 数据类型的排列,如charintlong以及它们的unsigned变体。

序列化结构很少用于表示纯粹在 Python 代码中处理的数据对象。它们主要是作为一种数据交换格式,而不是一种仅由 Python 代码使用的在内存中保存数据的方式。

在某些情况下,将原始数据打包到结构中可能比将其保存在其他数据类型中使用更少的内存。然而,在大多数情况下,这将是一个非常高级的(可能是不必要的)优化:

>>> from struct import Struct
>>> MyStruct = Struct("i?f")
>>> data = MyStruct.pack(23, False, 42.0)

>>> # All you get is a blob of data:
>>> data
b'\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00(B'

>>> # Data blobs can be unpacked again:
>>> MyStruct.unpack(data)
(23, False, 42.0)

types.SimpleNamespace:花式属性访问

这里还有一个用 Python 实现数据对象的稍微晦涩的选择: types.SimpleNamespace 。这个类是在 Python 3.3 中添加的,提供了对其名称空间的属性访问。

这意味着SimpleNamespace实例将它们所有的键作为类属性公开。您可以使用obj.key点状属性访问,而不是常规字典使用的obj['key']方括号索引语法。默认情况下,所有实例都包含一个有意义的__repr__

顾名思义,SimpleNamespace简单!它基本上是一个允许属性访问和良好打印的字典。可以自由添加、修改和删除属性:

>>> from types import SimpleNamespace
>>> car1 = SimpleNamespace(color="red", mileage=3812.4, automatic=True)

>>> # The default repr:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)

>>> # Instances support attribute access and are mutable:
>>> car1.mileage = 12
>>> car1.windshield = "broken"
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')

Python 中的记录、结构和数据对象:摘要

如您所见,实现记录或数据对象有很多不同的选择。Python 中的数据对象应该使用哪种类型?通常,您的决定将取决于您的用例:

  • 如果只有几个字段,那么如果字段顺序容易记忆或者字段名是多余的,那么使用普通元组对象可能是没问题的。比如,想象三维空间中的一个(x, y, z)点。

  • 如果需要不可变的字段,那么普通元组、collections.namedtupletyping.NamedTuple都是不错的选择。

  • 如果您需要锁定字段名以避免输入错误,那么collections.namedtupletyping.NamedTuple就是您的朋友。

  • 如果您想保持简单,那么一个普通的字典对象可能是一个不错的选择,因为它的语法非常类似于 JSON

  • 如果您需要完全控制您的数据结构,那么是时候用@propertysetter 和 getter编写一个自定义类了。

  • 如果你需要给对象添加行为(方法),那么你应该从头开始写一个自定义类,或者使用dataclass装饰器,或者通过扩展collections.namedtupletyping.NamedTuple

  • 如果您需要将数据紧密打包以将其序列化到磁盘或通过网络发送,那么是时候阅读一下struct.Struct了,因为这是一个很好的用例!

如果您正在寻找一个安全的默认选择,那么我对在 Python 中实现普通记录、结构或数据对象的一般建议是在 Python 2.x 中使用collections.namedtuple,在 Python 3 中使用它的兄弟typing.NamedTuple

集合和多重集合

在这一节中,您将看到如何使用标准库中的内置数据类型和类在 Python 中实现可变和不可变的 set 和 multiset (bag)数据结构。

一个集合是不允许重复元素的无序对象集合。通常,集合用于快速测试集合中的成员资格值,从集合中插入或删除新值,以及计算两个集合的并集或交集。

在一个适当的 set 实现中,成员资格测试应该在快速的 O (1)时间内运行。并、交、差、子集运算平均要花 O ( n )时间。Python 标准库中包含的 set 实现遵循这些性能特征

就像字典一样,集合在 Python 中得到了特殊处理,并且有一些语法上的好处,使得它们易于创建。例如,花括号集合表达式语法和集合理解允许您方便地定义新的集合实例:

vowels = {"a", "e", "i", "o", "u"}
squares = {x * x for x in range(10)}

但是要小心:创建空集需要调用set()构造函数。使用空的花括号({})是不明确的,会创建一个空的字典。

Python 及其标准库提供了几个 set 实现。让我们来看看它们。

Remove ads

set:您的定位设置

set 类型是 Python 中内置的 set 实现。它是可变的,允许动态插入和删除元素。

Python 的集合由dict数据类型支持,并共享相同的性能特征。任何可散列的对象都可以存储在一个集合中:

>>> vowels = {"a", "e", "i", "o", "u"}
>>> "e" in vowels
True

>>> letters = set("alice")
>>> letters.intersection(vowels)
{'a', 'e', 'i'}

>>> vowels.add("x")
>>> vowels
{'i', 'a', 'u', 'o', 'x', 'e'}

>>> len(vowels)
6

frozenset:不可变集合

frozenset 类实现了set的不可变版本,该版本在被构造后不能被更改。

对象是静态的,只允许对其元素进行查询操作,不允许插入或删除。因为frozenset对象是静态的和可散列的,它们可以用作字典键或另一个集合的元素,这是常规(可变)set对象所不能做到的:

>>> vowels = frozenset({"a", "e", "i", "o", "u"})
>>> vowels.add("p")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'

>>> # Frozensets are hashable and can
>>> # be used as dictionary keys:
>>> d = { frozenset({1, 2, 3}): "hello" }
>>> d[frozenset({1, 2, 3})]
'hello'

collections.Counter:多重集

Python 标准库中的 collections.Counter 类实现了一个多重集合或包类型,允许集合中的元素出现不止一次。

如果您不仅需要跟踪某个元素是否是集合的一部分,还需要跟踪它在集合中被包含的次数,这将非常有用:

>>> from collections import Counter
>>> inventory = Counter()

>>> loot = {"sword": 1, "bread": 3}
>>> inventory.update(loot)
>>> inventory
Counter({'bread': 3, 'sword': 1})

>>> more_loot = {"sword": 1, "apple": 1}
>>> inventory.update(more_loot)
>>> inventory
Counter({'bread': 3, 'sword': 2, 'apple': 1})

Counter 类的一个警告是,在计算一个Counter对象中的元素数量时,你要小心。调用len()返回多重集中唯一元素的数量,而元素的总数可以使用sum()来检索:

>>> len(inventory)
3  # Unique elements

>>> sum(inventory.values())
6  # Total no. of elements

Python 中的集合和多重集合:摘要

集合是 Python 及其标准库包含的另一种有用且常用的数据结构。以下是决定使用哪种方法的一些指导原则:

  • 如果需要可变集合,那么使用内置的set类型。
  • 如果你需要可以用作字典或设置键的可散列对象,那么使用frozenset
  • 如果你需要一个多重集,或者包,数据结构,那么使用collections.Counter

堆栈(后进先出法)

一个是支持快速后进/先出 (LIFO)插入和删除语义的对象集合。与列表或数组不同,堆栈通常不允许对它们包含的对象进行随机访问。插入和删除操作通常也被称为推送弹出

栈数据结构的一个有用的现实类比是一堆盘子。新的盘子被放在盘子堆的最上面,因为这些盘子很珍贵也很重,所以只有最上面的盘子可以被移动。换句话说,堆叠的最后一个盘子必须是第一个取出的(LIFO)。为了够到堆叠中较低的板,最上面的板必须一个接一个地移开。

就性能而言,一个合适的栈实现预计会花费 O (1)的时间用于插入和删除操作。

堆栈在算法中有广泛的用途。例如,它们被用于语言解析和运行时内存管理,后者依赖于一个调用栈。使用堆栈的一个简短而漂亮的算法是在树或图数据结构上的深度优先搜索 (DFS)。

Python 附带了几个堆栈实现,每个实现都有略微不同的特征。让我们来看看他们,比较他们的特点。

Remove ads

list:简单的内置堆栈

Python 内置的list类型使得一个体面的栈数据结构,因为它支持在摊销O(1)时间内的推送和弹出操作。

Python 的列表在内部被实现为动态数组,这意味着当添加或删除元素时,它们偶尔需要为存储在其中的元素调整存储空间。列表过度分配了它的后备存储器,因此不是每次推送或弹出都需要调整大小。结果,你得到了这些操作的分摊的 O (1)时间复杂度。

不利的一面是,这使得它们的性能不如基于链表的实现所提供的稳定的 O (1)插入和删除(您将在下面的collections.deque中看到)。另一方面,链表确实提供了对栈上元素的快速随机访问,这是一个额外的好处。

当使用列表作为堆栈时,有一个重要的性能警告需要注意:为了获得插入和删除的分摊的 O (1)性能,必须使用append()方法将新的项目添加到列表的末端,并使用pop()再次从末端移除。为了获得最佳性能,基于 Python 列表的栈应该向更高的索引增长,向更低的索引收缩。

从前面添加和移除要慢得多,需要 O ( n )的时间,因为现有的元素必须四处移动才能为新元素腾出空间。这是一个您应该尽可能避免的性能反模式:

>>> s = []
>>> s.append("eat")
>>> s.append("sleep")
>>> s.append("code")

>>> s
['eat', 'sleep', 'code']

>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'

>>> s.pop()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: pop from empty list

collections.deque:快速和稳定的堆栈

deque 类实现了一个双端队列,支持在 O (1)时间(非摊销)内从两端添加和移除元素。因为 deques 同样支持从两端添加和删除元素,所以它们既可以用作队列,也可以用作堆栈。

Python 的deque对象被实现为双向链表,这使得它们在插入和删除元素时具有出色且一致的性能,但在随机访问堆栈中间的元素时,性能却很差 O ( n )。

总的来说, collections.deque是一个很好的选择如果你正在 Python 的标准库中寻找一个栈数据结构,它具有链表实现的性能特征:

>>> from collections import deque
>>> s = deque()
>>> s.append("eat")
>>> s.append("sleep")
>>> s.append("code")

>>> s
deque(['eat', 'sleep', 'code'])

>>> s.pop()
'code'
>>> s.pop()
'sleep'
>>> s.pop()
'eat'

>>> s.pop()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: pop from an empty deque

queue.LifoQueue:并行计算的锁定语义

Python 标准库中的 LifoQueue 栈实现是同步的,并提供锁定语义来支持多个并发生产者和消费者。

除了LifoQueue之外,queue模块还包含其他几个类,它们实现了对并行计算有用的多生产者、多消费者队列。

根据您的用例,锁定语义可能是有帮助的,或者它们可能只是招致不必要的开销。在这种情况下,最好使用一个list或一个deque作为通用堆栈:

>>> from queue import LifoQueue
>>> s = LifoQueue()
>>> s.put("eat")
>>> s.put("sleep")
>>> s.put("code")

>>> s
<queue.LifoQueue object at 0x108298dd8>

>>> s.get()
'code'
>>> s.get()
'sleep'
>>> s.get()
'eat'

>>> s.get_nowait()
queue.Empty

>>> s.get()  # Blocks/waits forever...

Python 中的堆栈实现:概要

如您所见,Python 附带了堆栈数据结构的几种实现。它们都有稍微不同的特征以及性能和使用权衡。

如果您不寻求并行处理支持(或者如果您不想手动处理锁定和解锁),那么您可以选择内置的list类型或collections.deque。区别在于幕后使用的数据结构和整体易用性。

list 由一个动态数组支持,这使得它非常适合快速随机访问,但需要在添加或删除元素时偶尔调整大小。

列表过度分配了它的后备存储,所以不是每次推送或弹出都需要调整大小,并且你得到了这些操作的分摊的 O (1)时间复杂度。但是您需要注意的是,只能使用append()pop()来插入和移除项目。否则,性能会下降到 O ( n )。

collections.deque 以双向链表为后盾,优化了两端的追加和删除,并为这些操作提供了一致的 O (1)性能。不仅其性能更稳定,而且deque类也更易于使用,因为您不必担心从错误的一端添加或删除项目。

总之,collections.deque是在 Python 中实现堆栈(LIFO 队列)的绝佳选择。

Remove ads

队列(先进先出)

在本节中,您将看到如何仅使用 Python 标准库中的内置数据类型和类来实现一个先进/先出 (FIFO)队列数据结构。

一个队列是支持插入和删除的快速 FIFO 语义的对象集合。插入和删除操作有时被称为入队出列。与列表或数组不同,队列通常不允许对它们包含的对象进行随机访问。

这里有一个 FIFO 队列的真实类比:

想象一下 PyCon 注册的第一天,一队 Pythonistas 在等着领取他们的会议徽章。当新人进入会场,排队领取胸卡时,他们在队伍的后面排队。开发人员收到他们的徽章和会议礼品包,然后在队列的前面离开队伍(出列)。

记住队列数据结构特征的另一种方法是把它想象成一个管道。你把乒乓球放在一端,它们会移动到另一端,然后你把它们移走。当球在队列中(一根实心金属管)时,你不能够到它们。与队列中的球进行交互的唯一方法是在管道的后面添加新的球(入队)或者在前面移除它们(出列)。

队列类似于堆栈。两者的区别在于如何移除物品。用一个队列,你移除最近添加最少的项目*(FIFO),但是用一个堆栈,你移除最近添加最多的项目(LIFO)。*

就性能而言,一个合适的队列实现预计会花费 O (1)的时间来执行插入和删除操作。这是对队列执行的两个主要操作,在正确的实现中,它们应该很快。

队列在算法中有广泛的应用,通常有助于解决调度和并行编程问题。使用队列的一个简短而漂亮的算法是在树或图数据结构上的广度优先搜索 (BFS)。

调度算法经常在内部使用优先级队列。这些是专门的队列。一个优先级队列检索优先级最高的元素,而不是按照插入时间检索下一个元素。单个元素的优先级由队列根据应用于它们的键的顺序来决定。

然而,常规队列不会对其携带的项目进行重新排序。就像在管道的例子中,你取出你放入的东西,并且完全按照那个顺序。

Python 附带了几个队列实现,每个实现都有稍微不同的特征。我们来复习一下。

list:非常慢的队列

有可能使用常规的list作为队列,但是从性能角度来看这并不理想。为此目的,列表非常慢,因为在开头插入或删除一个元素需要将所有其他元素移动一位,需要 O ( n )时间。

因此,我而不是建议在 Python 中使用list作为临时队列,除非你只处理少量元素:

>>> q = []
>>> q.append("eat")
>>> q.append("sleep")
>>> q.append("code")

>>> q
['eat', 'sleep', 'code']

>>> # Careful: This is slow!
>>> q.pop(0)
'eat'

collections.deque:快速和健壮的队列

deque类实现了一个双端队列,支持在 O (1)时间内(非摊销)从两端添加和移除元素。因为 deques 同样支持从两端添加和删除元素,所以它们既可以用作队列,也可以用作堆栈。

Python 的deque对象被实现为双向链表。这使得它们在插入和删除元素时具有出色且一致的性能,但在随机访问堆栈中间的元素时,性能却很差。

因此,如果您在 Python 的标准库中寻找队列数据结构,collections.deque是一个很好的默认选择:

>>> from collections import deque
>>> q = deque()
>>> q.append("eat")
>>> q.append("sleep")
>>> q.append("code")

>>> q
deque(['eat', 'sleep', 'code'])

>>> q.popleft()
'eat'
>>> q.popleft()
'sleep'
>>> q.popleft()
'code'

>>> q.popleft()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: pop from an empty deque

queue.Queue:并行计算的锁定语义

Python 标准库中的 queue.Queue 实现是同步的,并提供锁定语义来支持多个并发的生产者和消费者。

模块包含了其他几个实现多生产者、多消费者队列的类,这对于并行计算非常有用。

根据您的用例,锁定语义可能会有所帮助,也可能会带来不必要的开销。在这种情况下,最好使用collections.deque作为通用队列:

>>> from queue import Queue
>>> q = Queue()
>>> q.put("eat")
>>> q.put("sleep")
>>> q.put("code")

>>> q
<queue.Queue object at 0x1070f5b38>

>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'

>>> q.get_nowait()
queue.Empty

>>> q.get()  # Blocks/waits forever...

multiprocessing.Queue:共享作业队列

multiprocessing.Queue 是一个共享的作业队列实现,允许多个并发工作器并行处理排队的项目。基于进程的并行化在 CPython 中很流行,这是因为全局解释器锁 (GIL)阻止了在单个解释器进程上的某些形式的并行执行。

作为一个专门用于在进程间共享数据的队列实现,multiprocessing.Queue使得在多个进程间分配工作变得容易,从而绕过 GIL 限制。这种类型的队列可以跨进程边界存储和传输任何可拾取的对象:

>>> from multiprocessing import Queue
>>> q = Queue()
>>> q.put("eat")
>>> q.put("sleep")
>>> q.put("code")

>>> q
<multiprocessing.queues.Queue object at 0x1081c12b0>

>>> q.get()
'eat'
>>> q.get()
'sleep'
>>> q.get()
'code'

>>> q.get()  # Blocks/waits forever...

Python 中的队列:摘要

Python 包括几个队列实现,作为核心语言及其标准库的一部分。

对象可以用作队列,但由于性能较慢,通常不建议这样做。

如果您不寻求并行处理支持,那么由collections.deque提供的实现是在 Python 中实现 FIFO 队列数据结构的一个极好的默认选择。它提供了良好队列实现的性能特征,也可以用作堆栈(LIFO 队列)。

优先队列

一个优先级队列是一个容器数据结构,它管理一组具有全序关键字的记录,以提供对该组中具有最小或最大关键字的记录的快速访问。

您可以将优先级队列视为修改后的队列。它不是根据插入时间检索下一个元素,而是检索优先级最高的元素。单个元素的优先级由应用于它们的键的顺序决定。

优先级队列通常用于处理调度问题。例如,您可以使用它们来优先处理更紧急的任务。

考虑操作系统任务调度程序的工作:

理想情况下,系统中优先级较高的任务(比如玩实时游戏)应该优先于优先级较低的任务(比如在后台下载更新)。通过将未决任务组织在以任务紧急程度为关键字的优先级队列中,任务计划程序可以快速选择优先级最高的任务,并允许它们首先运行。

在本节中,您将看到一些选项,说明如何使用内置数据结构或 Python 标准库中包含的数据结构在 Python 中实现优先级队列。每种实现都有自己的优点和缺点,但在我看来,对于大多数常见的场景,都有一个明显的赢家。让我们找出是哪一个。

list:手动排序队列

你可以使用一个排序的list来快速识别和删除最小或最大的元素。缺点是向列表中插入新元素是一个缓慢的 O ( n )操作。

虽然可以使用标准库中的 bisect.insortO (log n )时间内找到插入点,但这总是由缓慢的插入步骤决定。

通过追加列表和重新排序来维持顺序也至少需要 O ( n log n )的时间。另一个缺点是,当插入新元素时,您必须手动重新排序列表。错过这一步很容易引入 bug,负担永远在你这个开发者身上。

这意味着排序列表仅在插入很少时适合作为优先级队列:

>>> q = []
>>> q.append((2, "code"))
>>> q.append((1, "eat"))
>>> q.append((3, "sleep"))
>>> # Remember to re-sort every time a new element is inserted,
>>> # or use bisect.insort()
>>> q.sort(reverse=True)

>>> while q:
...     next_item = q.pop()
...     print(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

heapq:基于列表的二进制堆

heapq 是一个通常由普通list支持的二进制堆实现,支持在 O (log n )时间内插入和提取最小元素。

对于用 Python 实现优先级队列的来说,这个模块是个不错的选择。由于heapq在技术上只提供了一个最小堆实现,必须采取额外的步骤来确保排序稳定性和其他实际优先级队列通常期望的特性:

>>> import heapq
>>> q = []
>>> heapq.heappush(q, (2, "code"))
>>> heapq.heappush(q, (1, "eat"))
>>> heapq.heappush(q, (3, "sleep"))

>>> while q:
...     next_item = heapq.heappop(q)
...     print(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

queue.PriorityQueue:漂亮的优先队列

queue.PriorityQueue 内部使用heapq,具有相同的时间和空间复杂度。不同之处在于PriorityQueue是同步的,并提供锁定语义来支持多个并发的生产者和消费者。

根据您的用例,这可能是有帮助的,或者可能只是稍微减慢您的程序。无论如何,你可能更喜欢由PriorityQueue提供的基于类的接口,而不是由heapq提供的基于函数的接口:

>>> from queue import PriorityQueue
>>> q = PriorityQueue()
>>> q.put((2, "code"))
>>> q.put((1, "eat"))
>>> q.put((3, "sleep"))

>>> while not q.empty():
...     next_item = q.get()
...     print(next_item)
...
(1, 'eat')
(2, 'code')
(3, 'sleep')

Python 中的优先级队列:摘要

Python 包括几个优先级队列实现,可供您使用。

queue.PriorityQueue 凭借一个漂亮的面向对象的界面和一个清楚表明其意图的名字脱颖而出。应该是你的首选。

如果你想避免queue.PriorityQueue的锁定开销,那么直接使用 heapq 模块也是一个不错的选择。

结论:Python 数据结构

这就结束了您对 Python 中常见数据结构的探索。有了这里学到的知识,您就可以实现适合您的特定算法或用例的高效数据结构了。

在本教程中,您已经学习了:

  • Python 标准库中内置了哪些常见的抽象数据类型
  • 最常见的抽象数据类型如何映射到 Python 的命名方案
  • 如何在各种算法中把抽象数据类型实际运用

如果你喜欢在这个例子中从 Python 技巧中学到的东西,那么一定要看看这本书的其余部分

如果你有兴趣温习你的一般数据结构知识,那么我强烈推荐史蒂文·s·斯基纳的《算法设计手册》。它在教您基本的(和更高级的)数据结构和向您展示如何在代码中实现它们之间取得了很好的平衡。史蒂夫的书对本教程的写作有很大的帮助。

立即观看本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 栈和队列:选择理想的数据结构*********

<think>好的,用户想要查找关于Python和Qt开发的教程或者最佳实践。我之前提供的引用里提到了PySide6和Qt5的相关资料,可能需要整理一下这些信息。首先,用户需要知道Python中常用的Qt绑定库有哪些,比如PySide6和PyQt5,这两者的区别和选择可能是一个关键点。 用户可能刚开始接触Qt,所以需要明确推荐哪个库更适合新手。根据引用内容,PySide6是Qt官方支持的,而PyQt是第三方维护的。可能应该推荐PySide6,因为官方支持可能更可靠,文档也更全面。另外,引用里提到PySide6的文档虽然可用,但建议参考Qt本身的文档,因为更完整,这点需要传达给用户。 接下来,用户需要教程和最佳实践。引用中提到了创建QApplication、QWidget、布局和按钮的基本步骤,可以整理一个简单的示例代码,展示如何搭建一个基本的GUI应用。同时,需要强调PySide6的命名规范与Python的PEP8不同,比如mixedCase的使用,这可能影响代码风格,需要提醒用户注意。 另外,用户可能需要了解资源推荐,比如官方文档、书籍、社区和视频教程。引用里提到了Qt文档的重要性,所以应该建议用户结合官方文档学习。书籍方面可以推荐《使用Python和Qt6创建GUI应用程序》这样的资源,以及在线教程和视频课程。 还有最佳实践部分,需要包括项目结构、信号与槽的使用、多线程处理、样式表、国际化以及调试工具。这些都是开发中常见的注意事项,能够帮助用户写出更高效、可维护的代码。 最后,生成相关问题需要考虑用户可能进一步的需求,比如库的选择、命名规范、跨平台问题、性能优化等。这些问题可以引导用户深入学习,解决实际开发中的疑惑。 总结一下,回答需要分步骤介绍库的选择、基础教程、资源推荐、最佳实践,并附上示例代码和相关问题,确保结构清晰,信息准确,同时结合之前的引用内容,突出Qt文档的重要性以及PySide6的特点。</think>### Python Qt开发教程与最佳实践 #### 一、Python Qt开发库选择 1. **PySide6**(推荐) - 由Qt官方维护的Python绑定库,支持Qt6最新特性[^1]。 - 完全开源(LGPL协议),适合商业项目。 - 命名规范与Qt C++一致(如`QWidget.setFixedSize()`),需注意与Python PEP8的差异[^1]。 2. **PyQt5** - 第三方维护的Qt5绑定库,功能成熟但需商业授权。 - 语法更接近Python风格(部分函数使用snake_case)。 #### 二、基础开发流程示例(PySide6) ```python # 安装库:pip install pyside6 from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton # 1. 创建应用实例 app = QApplication([]) # 2. 构建主窗口 window = QWidget() window.setWindowTitle("基础示例") layout = QVBoxLayout() # 3. 添加组件 button1 = QPushButton("点击我") button2 = QPushButton("退出") layout.addWidget(button1) layout.addWidget(button2) # 4. 绑定信号与槽 button2.clicked.connect(window.close) # 5. 显示窗口 window.setLayout(layout) window.show() app.exec() ``` ▲ 代码说明:通过信号槽机制实现交互逻辑,使用布局管理组件排列[^2]。 #### 三、核心学习资源推荐 1. **官方文档** - [Qt for Python官方文档](https://doc.qt.io/qtforpython/)(含API详解与C++示例对照) - [Qt框架通用文档](https://doc.qt.io/)(涵盖GUI设计、多线程等高级主题) 2. **书籍推荐** - 《使用Python和Qt6创建GUI应用程序》(配套PySide6实战案例) - 《Python Qt GUI与数据可视化编程》 3. **社区与教程** - Real Python的[Qt开发指南](https://realpython.com/python-pyqt-gui-calculator/) - 知乎专栏《PySide6全攻略》 4. **视频课程** - Udemy课程《PySide6 Masterclass》 - B站Qt官方中文教程系列 #### 四、最佳实践指南 1. **项目结构规范** ```bash myapp/ ├── main.py # 程序入口 ├── ui/ # Qt Designer生成的.ui文件 ├── views/ # 自定义窗口类 ├── models/ # 数据模型 └── resources.qrc # 资源文件(图标、样式表等) ``` 2. **信号与槽的高效使用** - 优先使用装饰器声明自定义槽函数: ```python from PySide6.QtCore import Slot @Slot() def handle_click(): print("按钮被点击") ``` 3. **多线程处理** - 使用`QThreadPool` + `QRunnable`处理耗时操作: ```python from PySide6.QtCore import QRunnable, QThreadPool class Worker(QRunnable): def run(self): # 执行后台任务 pass QThreadPool.globalInstance().start(Worker()) ``` 4. **样式表(QSS)优化** - 统一管理样式文件: ```css /* style.qss */ QPushButton { background-color: #2ecc71; border-radius: 5px; padding: 10px; } ``` - 动态加载样式: ```python with open("style.qss", "r") as f: app.setStyleSheet(f.read()) ``` 5. **国际化支持** - 使用`tr()`标记可翻译文本: ```python button = QPushButton(self.tr("Submit")) ``` - 通过`lupdate`工具生成.ts翻译文件 6. **调试工具** - 使用`QDebug`输出日志: ```python from PySide6.QtCore import qDebug qDebug("当前数值: {}".format(value)) ``` - 启用Qt Creator的调试器插件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值