是时候兑现我们使用依赖倒置原则的承诺,将其作为将核心逻辑与基础设施关注点解耦的一种方式。
我们将引入仓储模式,这是一种对数据存储的简化抽象,允许我们将模型层与数据层解耦。我们将提供一个具体的示例,演示这种简化抽象如何通过隐藏数据库的复杂性来使我们的系统更具可测试性。
在引入仓储模式之前和之后,展示了我们即将构建的一小部分内容:一个仓储对象,位于我们的领域模型和数据库之间。
这一章的代码存储在 GitHub 上的 “chapter_02_repository” 分支中。
git clone https://github.com/cosmicpython/code.git
cd code
git checkout chapter_02_repository
# or to code along, checkout the previous chapter:
git checkout chapter_01_domain_model
持久化我们的领域模型
在[第一章领域模型]中,我们构建了一个简单的领域模型,用于将订单分配给一批库存。对于这段代码,我们可以轻松编写测试,因为没有任何依赖关系或基础设施需要设置。如果我们需要运行数据库或API并创建测试数据,那么编写和维护测试将会更加困难。
不幸的是,在某个时候,我们将需要将我们完美的小模型交给用户,并应对电子表格、Web浏览器和竞争条件等现实世界的情况。在接下来的几章中,我们将探讨如何将我们理想化的领域模型连接到外部状态。
我们期望以敏捷的方式工作,因此我们的优先目标是尽快达到最小可行产品。在我们的情况下,这将是一个Web API。在一个真实的项目中,您可能会直接使用一些端到端测试,开始插入一个Web框架,并在外部进行测试驱动开发。
但是我们知道,无论如何,我们都需要某种形式的持久存储,这是一本教材,因此我们可以允许自己进行更底层的开发,并开始考虑存储和数据库。
一些伪代码:我们需要什么?
这句话表明在下文中将提供一些伪代码来探讨我们在将理想化的领域模型连接到外部状态时可能需要的东西。这可以理解为在计划阶段讨论需要做什么的一种方式,但具体的代码尚未编写。
当我们构建第一个API端点时,我们知道我们将会有一些类似以下的代码。
我们的第一个API端点将会是怎么样的
@flask.route.gubbins
def allocate_endpoint():
# extract order line from request
line = OrderLine(request.params, ...)
# load all batches from the DB
batches = ...
# call our domain service
allocate(line, batches)
# then save the allocation back to the database somehow
return 201
我们使用了Flask因为它轻量级,但您不需要是Flask的用户才能理解本书。事实上,我们将向您展示如何将框架的选择变成一个次要的细节。
我们需要一种从数据库中检索批次信息并从中实例化我们的领域模型对象的方法,还需要一种将它们保存回数据库的方法。
什么?哦,“gubbins” 是英国英语中表示 “东西” 的词汇。您可以忽略它。这只是伪代码,好吗?
应用依赖倒置原则(DIP)到数据访问
如在介绍中提到的,分层架构是一种常见的用于组织具有用户界面、一些逻辑和数据库的系统的方法(参见分层架构)。
Django的模型-视图-模板(Model-View-Template)结构与模型-视图-控制器(Model-View-Controller,MVC)密切相关。无论哪种情况,目标都是保持各层分离(这是一件好事),并使每一层仅依赖于下面的一层。
但是,我们希望我们的领域模型完全没有任何依赖关系。我们不希望基础设施关注点渗透到我们的领域模型中,从而减慢我们的单元测试或我们进行更改的能力。
相反,正如在介绍中讨论的那样,我们将认为我们的模型位于“内部”,依赖关系向其流入;这就是人们有时称之为洋葱架构(Onion architecture)的概念。
这个概念涉及到一些与软件架构模式相关的术语,包括 “Ports and Adapters”、“Hexagonal Architecture”、“Onion Architecture” 和 “Clean Architecture”。虽然有些人可能会对它们之间的差异进行挑剔,但它们基本上都是同一概念的不同名称,它们都归结为依赖倒置原则:高级模块(领域)不应该依赖于低级模块(基础设施)。
在本书的后面,我们将深入探讨一些关于 “依赖于抽象” 以及是否存在与 Python 等效的接口的具体细节。另请参阅文章 “What Is a Port and What Is an Adapter, in Python?”。
回顾我们的模型
让我们回顾一下我们的领域模型:分配是将订单行(OrderLine)与批次(Batch)关联起来的概念。我们将这些分配存储为批次对象(Batch)上的一个集合
让我们看看如何将其转换为关系数据库
“正常” 的ORM方式:模型依赖于ORM
现在,你的团队成员几乎不可能手动编写他们自己的SQL查询。相反,你几乎肯定在使用某种框架来根据你的模型对象生成SQL。
这些框架被称为对象关系映射器(ORMs),因为它们存在的目的是弥合对象和领域建模的世界与数据库和关系代数的世界之间的概念差距。
ORM给予我们最重要的东西是"持久性无知":也就是说,我们精心设计的领域模型不需要知道任何关于如何加载或持久化数据的细节。这有助于保持我们的领域模型不依赖于特定的数据库技术。
但如果你按照典型的SQLAlchemy教程进行操作,你最终会得到类似于以下的东西
SQLAlchemy “declarative” 语法,模型依赖于ORM(orm.py)
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Order(Base):
id = Column(Integer, primary_key=True)
class OrderLine(Base):
id = Column(Integer, primary_key=True)
sku = Column(String(250))
qty = Integer(String(250))
order_id = Column(Integer, ForeignKey('order.id'))
order = relationship(Order)
class Allocation(Base):
...
您不需要了解SQLAlchemy,就可以看到我们的模型现在充满了对ORM的依赖,而且看起来非常混乱。我们真的能说这个模型对数据库一无所知吗?当我们的模型属性直接与数据库列耦合在一起时,它如何能与存储关注点分离开来呢?
Django的ORM基本上也是一样的,但更加受限。
如果您更习惯于Django,前面的"declarative" SQLAlchemy代码段大致可以翻译成以下类似的形式:
class Order(models.Model):
pass
class OrderLine(models.Model):
sku = models.CharField(max_length=255)
qty = models.IntegerField()
order = models.ForeignKey(Order)
class Allocation(models.Model):
...
重点是一样的——我们的模型类直接继承自ORM类,因此我们的模型依赖于ORM。我们希望情况相反。
Django不提供类似SQLAlchemy经典映射器的功能,但请参阅[附录_django]以了解如何将依赖倒置和仓储模式应用于Django的示例。
反转依赖关系:ORM依赖于模型
幸运的是,这不是使用SQLAlchemy的唯一方式。另一种方式是分别定义模式(schema),并为如何在模式和我们的领域模型之间进行转换定义一个显式的映射,这就是SQLAlchemy称之为"经典映射"(classical mapping)的方式:
使用SQLAlchemy的Table对象进行显式ORM映射(orm.py)
from sqlalchemy.orm import mapper, relationship
import model #(1)
metadata = MetaData()
order_lines = Table( #(2)
"order_lines",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("sku", String(255)),
Column("qty", Integer, nullable=False),
Column("orderid", String(255)),
)
...
def start_mappers():
lines_mapper = mapper(model.OrderLine, order_lines) #(3)
- ORM引入(或者说"依赖于"或"了解")领域模型,而不是相反。
- 我们使用SQLAlchemy的抽象来定义数据库表和列。
- 当我们调用mapper函数时,SQLAlchemy会进行处理,将我们的领域模型类与我们定义的各种表绑定在一起。
最终的结果是,如果我们调用start_mappers函数,我们将能够轻松地从数据库加载和保存领域模型实例。但如果我们从未调用过这个函数,那么我们的领域模型类将毫不知情地保持独立于数据库。
这为我们带来了使用SQLAlchemy的所有好处,包括使用alembic进行迁移的能力,以及使用我们的领域类进行透明查询的能力,正如我们将在后面看到的。
当您首次尝试构建ORM配置时,编写针对它的测试可能会很有用,就像以下示例中所示:
直接测试ORM(一次性测试)(test_orm.py)
def test_orderline_mapper_can_load_lines(session): #(1)
session.execute(
"INSERT INTO order_lines (orderid, sku, qty) VALUES "
'("order1", "RED-CHAIR", 12),'
'("order1", "RED-TABLE", 13),'
'("order2", "BLUE-LIPSTICK", 14)'
)
expected = [
model.OrderLine("order1", "RED-CHAIR", 12),
model.OrderLine("order1", "RED-TABLE", 13),
model.OrderLine("order2", "BLUE-LIPSTICK", 14),
]
assert session.query(model.OrderLine).all() == expected
def test_orderline_mapper_can_save_lines(session):
new_line = model.OrderLine("order1", "DECORATIVE-WIDGET", 12)
session.add(new_line)
session.commit()
rows = list(session.execute('SELECT orderid, sku, qty FROM "order_lines"'))
assert rows == [("order1", "DECORATIVE-WIDGET", 12)]
- 如果您没有使用过pytest,那么这个测试中的session参数需要解释一下。在本书的目的上,您不需要担心pytest或其fixture的详细信息,但简要的解释是,您可以将测试的共同依赖项定义为"fixtures",pytest将通过查看测试函数的参数来将它们注入到需要它们的测试中。在这种情况下,它是一个SQLAlchemy数据库会话(session)。这种做法有助于将共享的资源和配置传递给测试,以确保测试的一致性和可重复性。
您可能不会保留这些测试,正如您马上会看到的,一旦您采取了反转ORM和领域模型的依赖关系的步骤,就只需额外小小的一步来实现另一个称为"仓储模式"(Repository pattern)的抽象,这将更容易编写测试,并提供了一个在后续测试中更容易模拟的简单接口。
但我们已经实现了我们反转传统依赖的目标:领域模型保持"纯净",不受基础设施关注点的干扰。我们可以丢弃SQLAlchemy并使用不同的ORM,或者完全不同的持久化系统,而领域模型根本不需要改变。
根据您在领域模型中的工作内容,特别是如果您偏离了面向对象的范式,您可能会发现很难让ORM生成您需要的确切行为,可能需要修改您的领域模型。正如在架构决策中经常发生的那样,您需要权衡考虑。正如Python之禅所说,“实用性胜于纯粹性!”
不过,此时,我们的API端点可能看起来类似以下内容,我们可以让它正常工作:
在我们的API端点中直接使用SQLAlchemy
@flask.route.gubbins
def allocate_endpoint():
session = start_session()
# extract order line from request
line = OrderLine(
request.json['orderid'],
request.json['sku'],
request.json['qty'],
)
# load all batches from the DB
batches = session.query(Batch).all()
# call our domain service
allocate(line, batches)
# save the allocation back to the database
session.commit()
return 201
引入仓储模式
仓储模式是对持久性存储的抽象。它通过假装所有数据都在内存中来隐藏数据访问的琐碎细节。
如果我们的笔记本电脑具有无限的内存,那么我们就不需要笨重的数据库了。相反,我们可以随时使用我们的对象。那会是什么样子呢?
你必须从某个地方获取数据
import all_my_data
def create_a_batch():
batch = Batch(...)
all_my_data.batches.add(batch)
def modify_a_batch(batch_id, new_quantity):
batch = all_my_data.batches.get(batch_id)
batch.change_initial_quantity(new_quantity)
即使我们的对象在内存中,我们仍然需要将它们放在某个地方,以便能够再次找到它们。我们的内存中的数据允许我们添加新的对象,就像添加到列表或集合一样。因为对象在内存中,所以我们从不需要调用.save()方法;我们只需获取我们关心的对象,并在内存中修改它。
抽象中的仓储(The Repository in the Abstract)
最简单的仓储模式只有两个方法:add() 用于将新项目放入仓储,get() 用于返回先前添加的项目。我们坚决使用这些方法来进行数据访问,无论是在我们的领域层还是在服务层。这种自我强制的简单性阻止了我们将领域模型与数据库耦合在一起。
以下是我们的仓储的抽象基类(ABC)的示例:
最简单的可能仓储(repository.py)
class AbstractRepository(abc.ABC):
@abc.abstractmethod #(1)
def add(self, batch: model.Batch):
raise NotImplementedError #(2)
@abc.abstractmethod
def get(self, reference) -> model.Batch:
raise NotImplementedError
- Python提示:@abc.abstractmethod是Python中使抽象基类(ABCs)实际"工作"的少数要素之一。Python将拒绝允许您实例化未实现其父类中定义的所有抽象方法的类。
- 引发NotImplementedError很好,但既不是必需的也不足够。实际上,您的抽象方法可以具有实际的行为,子类可以调用这些行为,如果您真的需要的话。
抽象基类、鸭子类型和协议
在这本书中,我们使用抽象基类(Abstract Base Classes,ABCs)是为了教学的目的:我们希望它们有助于解释仓储抽象的接口是什么。 在实际应用中,有时我们会从生产代码中删除ABCs,因为Python使得忽略它们变得太容易,它们最终变得不易维护,最糟糕的情况下会误导人。实际上,我们通常依赖于Python的鸭子类型(duck typing)来实现抽象。对于Python程序员来说,仓储就是具有add(thing)和get(id)方法的任何对象。 一个可供考虑的替代方案是PEP 544协议。这些协议为您提供了具有类型信息但不具备继承性的功能,这会特别受到"优先使用组合而不是继承"的支持者
什么是权衡?
他们说经济学家知道一切的价格,但对价值一无所知。而程序员知道一切的好处,但却不懂取舍。
— Rich Hickey
每当我们在这本书中引入一个架构模式时,我们总会问自己:“我们因此获得了什么?以及这需要付出什么代价?”
通常情况下,至少我们会引入额外的抽象层,尽管我们可能希望它能在总体上降低复杂性,但它确实在局部增加了复杂性,并且在移动部件的数量和持续维护方面有一定的成本。
存储库模式可能是本书中最容易选择的模式之一,尤其是如果您已经在实践领域驱动设计(DDD)和依赖反转方向。就我们的代码而言,实际上只是将SQLAlchemy的抽象(session.query(Batch))与我们设计的另一个抽象(batches_repo.get)进行了替换。
每当我们添加一个新的领域对象以便检索时,我们需要在存储库类中编写一些代码,但作为回报,我们得到了一个简单的抽象,可以控制我们的存储层。存储库模式将使我们能够轻松对存储方式进行基本更改(参见[附录_csvs]),正如我们将看到的,它也易于在单元测试中模拟。
此外,在领域驱动设计(DDD)世界中,存储库模式非常常见,因此,如果您与那些从Java和C#世界转入Python的程序员合作,他们很可能会认识到它。存储库模式是一个很好的例子。
和往常一样,我们从编写测试开始。这个测试可能被分类为集成测试,因为我们正在检查我们的代码(存储库)是否与数据库正确集成;因此,这些测试往往混合了原始SQL与对我们自己代码的调用和断言。
提示:
与之前的ORM测试不同,这些测试更适合长期保留在您的代码库中,特别是如果您的领域模型的某些部分意味着对象关系映射非常复杂。
用于保存对象的存储库测试(test_repository.py)
def test_repository_can_save_a_batch(session):
batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=None)
repo = repository.SqlAlchemyRepository(session)
repo.add(batch) #(1)
session.commit() #(2)
rows = session.execute( #(3)
'SELECT reference, sku, _purchased_quantity, eta FROM "batches"'
)
assert list(rows) == [("batch1", "RUSTY-SOAPDISH", 100, None)]
- 这里要测试的方法是 repo.add()
- 我们将 .commit() 操作留在存储库之外,并将其作为调用者的责任。这样做有一些利弊;当我们来到[第六章_工作单元]时,一些原因会变得更加清晰。
- 我们使用原始的SQL来验证是否已正确保存了数据。
下一个测试涉及检索批次和分配,因此更复杂:
用于检索复杂对象的存储库测试(test_repository.py)
def insert_order_line(session):
session.execute( #(1)
"INSERT INTO order_lines (orderid, sku, qty)"
' VALUES ("order1", "GENERIC-SOFA", 12)'
)
[[orderline_id]] = session.execute(
"SELECT id FROM order_lines WHERE orderid=:orderid AND sku=:sku",
dict(orderid="order1", sku="GENERIC-SOFA"),
)
return orderline_id
def insert_batch(session, batch_id): #(2)
...
def test_repository_can_retrieve_a_batch_with_allocations(session):
orderline_id = insert_order_line(session)
batch1_id = insert_batch(session, "batch1")
insert_batch(session, "batch2")
insert_allocation(session, orderline_id, batch1_id) #(2)
repo = repository.SqlAlchemyRepository(session)
retrieved = repo.get("batch1")
expected = model.Batch("batch1", "GENERIC-SOFA", 100, eta=None)
assert retrieved == expected # Batch.__eq__ only compares reference #(3)
assert retrieved.sku == expected.sku #(4)
assert retrieved._purchased_quantity == expected._purchased_quantity
assert retrieved._allocations == { #(4)
model.OrderLine("order1", "GENERIC-SOFA", 12),
}
- 这个测试针对读取操作,因此原始SQL是准备要被 repo.get() 读取的数据。
- 我们将略过 insert_batch 和 insert_allocation 的详细信息;关键是要创建一些批次,并且对于我们感兴趣的批次,有一个现有的订单行已分配给它。
- 这就是我们在这里进行验证的内容。第一个 assert == 检查类型是否匹配,以及引用是否相同(因为你记得,Batch 是一个实体,我们为它编写了自定义的 __eq__ 方法)。
- 因此,我们还明确检查了其重要属性,包括 ._allocations,它是一个包含 OrderLine 值对象的 Python 集合。
是否要费心为每个模型编写测试是一个判断的问题。一旦你为一个类编写了测试来测试创建/修改/保存操作,你可能会很高兴继续为其他类编写一个最简单的往返测试,甚至根本不需要测试,如果它们都遵循类似的模式。在我们的情况下,设置 ._allocations 集合的ORM配置有点复杂,因此它值得进行特定的测试。
最终,你会得到类似这样的东西:
典型的存储库(repository.py)
class SqlAlchemyRepository(AbstractRepository):
def __init__(self, session):
self.session = session
def add(self, batch):
self.session.add(batch)
def get(self, reference):
return self.session.query(model.Batch).filter_by(reference=reference).one()
def list(self):
return self.session.query(model.Batch).all()
现在我们的Flask端点可能看起来类似以下内容:
在我们的API端点中直接使用我们的存储库
@flask.route.gubbins
def allocate_endpoint():
batches = SqlAlchemyRepository.list()
lines = [
OrderLine(l['orderid'], l['sku'], l['qty'])
for l in request.params...
]
allocate(lines, batches)
session.commit()
return 201
这是一个读者的练习。
最近在一个领域驱动设计(DDD)会议上,我们遇到了一个朋友,他说:“我已经有10年没有使用ORM了。”存储库模式和ORM都是在原始SQL前面的抽象层,因此在其中一个后面使用另一个并不是真正必要的。为什么不尝试在不使用ORM的情况下实现我们的存储库呢?你可以在GitHub上找到相应的代码。
我们留下了存储库测试,但确定要编写什么样的SQL查询是由你来完成的。也许比你想象的要难,也许会更容易。但好的一点是,您的应用程序的其余部分根本不关心它。
现在为测试构建一个虚拟的存储库变得非常容易!
这就是存储库模式的一个最大好处之一:
使用集合创建一个简单的虚拟存储库(repository.py)非常容易。
class FakeRepository(AbstractRepository):
def __init__(self, batches):
self._batches = set(batches)
def add(self, batch):
self._batches.add(batch)
def get(self, reference):
return next(b for b in self._batches if b.reference == reference)
def list(self):
return list(self._batches)
由于它只是一个简单的集合包装器,所以所有的方法都只有一行代码。
在测试中使用虚拟存储库非常容易,而且我们有一个简单的抽象层,易于使用和理解:
虚拟存储库的示例用法(test_api.py):
fake_repo = FakeRepository([batch1, batch2, batch3])
您将在下一章中看到这个虚拟存储库的实际应用。
提示:为您的抽象构建虚拟实现是获得设计反馈的绝佳方法:如果难以构建虚拟实现,那么这个抽象可能过于复杂。
在Python中,"端口"和"适配器"是什么意思?
我们不想在这里过多讨论术语,因为我们主要关注依赖反转,而您使用的技术细节并不太重要。此外,我们知道不同的人可能会使用稍微不同的定义。
"端口(Ports)"和"适配器(Adapters)"这两个概念源自面向对象(OO)编程,而我们所坚持的定义是,"端口"是我们的应用程序与我们希望抽象的任何内容之间的接口,而"适配器"是该接口或抽象背后的实现。
现在,Python本身没有明确的接口(interface),因此虽然通常很容易识别适配器,但定义端口可能会更困难。如果您使用抽象基类(Abstract Base Class),那么它就是端口。如果没有,端口只是您的适配器所遵循的"鸭子类型",以及您的核心应用程序所期望的函数和方法名称、它们的参数名称和类型。
具体来说,在本章中,AbstractRepository 是端口,而SqlAlchemyRepository和FakeRepository是适配器。
总结
在牢记Rich Hickey的引言的同时,我们在每一章中总结了我们介绍的每个架构模式的成本和收益。我们要明确的是,我们并不是在说每个应用程序都需要以这种方式构建;只有在应用程序和领域的复杂性使得值得投入时间和精力来添加这些额外的间接层时,才会这样做。
考虑到这一点,存储库模式和持久性无关性:权衡显示了存储库模式和我们的持久性无关模型的一些优点和缺点。
表格 1. 存储库模式和持久性无关性的权衡
优点 | 缺点 |
---|---|
我们在持久性存储和领域模型之间建立了一个简单的接口 | 使用ORM确实已经带来了一定程度的解耦。更改外键可能会有一定困难,但如果需要的话,应该可以很容易地在MySQL和Postgres之间切换 |
这使得我们能够轻松地创建用于单元测试的虚拟存储库版本,或者切换不同的存储解决方案,因为我们已经将模型与基础设施关注点完全解耦 | 手动维护ORM映射需要额外的工作和额外的代码 |
在考虑持久性之前编写领域模型帮助我们集中精力解决手头的业务问题。如果我们希望从根本上改变我们的方法,我们可以在模型中进行更改,而不必担心外键或迁移,直到以后再考虑这些问题 | 任何额外的间接层都会增加维护成本,并对之前未见过存储库模式的Python程序员产生"意外因素" |
我们的数据库架构非常简单,因为我们完全控制如何将对象映射到表格 |
领域模型的权衡作为一种图示表明了基本论点:是的,对于简单的情况来说,一个解耦的领域模型可能比简单的ORM/ActiveRecord模式更加繁琐。
提示:如果您的应用程序只是一个简单的CRUD(创建-读取-更新-删除)数据库包装器,那么您可能不需要领域模型或存储库。
但是,领域越复杂,投资于摆脱基础设施关注点将在轻松进行更改方面带来更多收益。
我们的示例代码并不复杂,不能详细展示图表的右侧是什么样子,但其中有一些提示。例如,想象一下,如果有一天我们决定希望将分配从批次对象转移到订单行(OrderLine)上:如果我们正在使用Django,我们需要在运行任何测试之前定义和思考数据库迁移。但是,由于我们的模型只是纯粹的Python对象,所以我们可以将一个set()更改为一个新属性,而不需要考虑数据库,直到以后再处理。
存储库模式回顾
将依赖反转应用到您的ORM
我们的领域模型应该摆脱基础设施关注点,因此您的ORM应该导入您的模型,而不是反过来。存储库模式是对永久性存储的简单抽象
存储库为您提供了一个将内存中对象看作集合的幻象。它使得轻松创建用于测试的虚拟存储库,并在不干扰核心应用程序的情况下更改基础设施的基本细节成为可能。请参见[附录_csvs]以获取示例。
您可能会想知道,我们如何实例化这些存储库,无论是虚拟的还是真实的?我们的Flask应用程序实际上会是什么样子?在下一个令人兴奋的章节,即"服务层模式"中,您将找到答案。
但首先,让我们进行一个简短的插曲。
以上就是“python架构模式(Architecture Patterns with Python)-第一部分(2.存储库模式)”的全部内容,希望对你有所帮助。
关于Python技术储备
学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
三、Python视频合集
观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
四、实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
五、Python练习题
检查学习结果。
六、面试资料
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
最后祝大家天天进步!!
上面这份完整版的Python全套学习资料已经上传至CSDN官方,朋友如果需要可以直接微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】。