Python 进阶指南(编程轻松进阶):八常见的 Python 陷阱
Posted 布客飞龙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 进阶指南(编程轻松进阶):八常见的 Python 陷阱相关的知识,希望对你有一定的参考价值。
虽然 Python 是我最喜欢的编程语言,但它也不是没有缺陷。每种语言都有缺点(有些比其他的多),Python 也不例外。新的 Python 程序员必须学会避免一些常见的“陷阱”程序员学习这类知识是随机的,来自经验,但本章把它收集在一个地方。了解这些陷阱背后的编程知识可以帮助您理解为什么 Python 有时行为怪异。
这一章解释了当你修改列表和字典等可变对象的内容时,它们的行为会如何异常。您将了解到sort()
方法是如何不按照字母顺序对项目进行排序的,以及浮点数是如何产生舍入误差的。当你将不等式操作符!=
链接在一起时,它们会有不寻常的行为。并且在编写包含单个项目的元组时,必须使用尾随逗号。本章告诉你如何避免这些常见的陷阱。
不要在遍历列表时添加或删除项目
在用for
或while
循环遍历(即迭代)列表时,从列表中添加或删除项目很可能会导致 bug。考虑这样一个场景:您想要遍历一个描述衣服的字符串列表,并通过每次在列表中找到一只袜子时插入一只匹配的袜子来确保有偶数只袜子。这个任务看起来很简单:遍历列表中的字符串,当在一个字符串中找到'sock'
,比如'red sock'
,将另一个'red sock'
字符串追加到列表中。
但是这个代码不行。它陷入了一个无限循环,你必须按下Ctrl+C
来中断它:
>>> clothes = ['skirt', 'red sock']
>>> for clothing in clothes: # Iterate over the list.
... if 'sock' in clothing: # Find strings with 'sock'.
... clothes.append(clothing) # Add the sock's pair.
... print('Added a sock:', clothing) # Inform the user.
...
Added a sock: red sock
Added a sock: red sock
Added a sock: red sock
`--snip--`
Added a sock: red sock
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
KeyboardInterrupt
你会在autbor.com/addingloop
看到这段代码的可视化执行。
问题是,当您将'red sock'
追加到clothes
列表中时,列表现在有了一个新的第三项,它必须迭代:['skirt', 'red sock', 'red sock']
。for
循环在下一次迭代中到达第二个'red sock'
,因此它追加另一个'red sock'
字符串。这使得列表['skirt', 'red sock', 'red sock', 'red sock']
成为 Python 迭代的另一个字符串。这将继续发生,如图 8-1 中的所示,这就是为什么我们会看到永不停止的'Added a sock.'
消息流。只有当计算机耗尽内存并使 Python 程序崩溃时,或者直到您通过按下Ctrl+C
来中断它,循环才会停止。
图 8-1:在for
循环的每一次迭代中,一个新的'red sock'
被添加到列表中,clothing
在下一次迭代中引用它。这个循环永远重复。
要点是不要在遍历列表时向列表中添加条目。取而代之的是,为新的、修改过的列表的内容使用一个单独的列表,比如本例中的newClothes
:
>>> clothes = ['skirt', 'red sock', 'blue sock']
>>> newClothes = []
>>> for clothing in clothes:
... if 'sock' in clothing:
... print('Appending:', clothing)
... newClothes.append(clothing) # We change the newClothes list, not clothes.
...
Appending: red sock
Appending: blue sock
>>> print(newClothes)
['red sock', 'blue sock']
>>> clothes.extend(newClothes) # Appends the items in newClothes to clothes.
>>> print(clothes)
['skirt', 'red sock', 'blue sock', 'red sock', 'blue sock']
这段代码的可视化执行在autbor.com/addingloopfixed
进行。
我们的for
循环遍历了clothes
列表中的条目,但是没有修改循环内部的clothes
。而是改了一个单独的列表,newClothes
。然后,在循环之后,我们通过用newClothes
的内容扩展来修改clothes
。你现在有了一个匹配袜子的clothes
列表。
同样,你不应该在遍历列表时删除列表中的条目。考虑这样一段代码,在这段代码中,我们想要从列表中移除任何不是'hello'
的字符串。最简单的方法是遍历列表,删除不匹配的条目:
>>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello']
>>> for i, word in enumerate(greetings):
... if word != 'hello': # Remove everything that isn't 'hello'.
... del greetings[i]
...
>>> print(greetings)
['hello', 'hello', 'yello', 'hello']
这段代码的可视化执行在autbor.com/deletingloop
进行。
名单里好像还剩下'yello'
。原因是当for
循环检查索引2
时,它从列表中删除了'mello'
。但是这将列表中所有剩余的条目下移一个索引,将'yello'
从索引3
移到索引2
。循环的下一次迭代检查索引3
,它现在是最后一个'hello'
,如图 8-2 中的所示。那根'yello'
字符串浑浑噩噩的溜走了!不要在遍历列表的时候从列表中删除项目。
图 8-2:当循环删除'mello'
时,列表中的项目下移一个索引,导致i
跳过'yello'
。
相反,创建一个新列表,复制除要删除的项目之外的所有项目,然后替换原始列表。对于前一个示例的无错误等效物,请在交互式 Shell 中输入以下代码。
>>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello']
>>> newGreetings = []
>>> for word in greetings:
... if word == 'hello': # Copy everything that is 'hello'.
... newGreetings.append(word)
...
>>> greetings = newGreetings # Replace the original list.
>>> print(greetings)
['hello', 'hello', 'hello']
这段代码的可视化执行在autbor.com/deletingloopfixed
进行。
请记住,因为这段代码只是一个创建列表的简单循环,所以您可以用列表推导式来替换它。列表推导式不会运行得更快或使用更少的内存,但它更短,但不会失去太多的可读性。在交互式 Shell 中输入以下内容,这相当于前面示例中的代码:
>>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello']
>>> greetings = [word for word in greetings if word == 'hello']
>>> print(greetings)
['hello', 'hello', 'hello']
不仅对列表的理解更加简洁,还避免了在迭代列表时改变列表时出现的问题。
引用、内存使用和sys.getsizeof()
这看起来像是创建一个新的列表而不是修改原来的列表浪费内存。但是请记住,就像变量在技术上包含对值的引用而不是实际值一样,列表也包含对值的引用。前面显示的newGreetings.append(word)
行没有复制word
变量中的字符串,只是复制了对字符串的引用,这要小得多。
您可以通过使用sys.getsizeof ()
函数看到这一点,该函数返回传递给它的对象在内存中占用的字节数。在这个交互式 Shell 示例中,我们可以看到短字符串'cat'
占用了 52 个字节,而长字符串占用了 85 个字节:
>>> import sys
>>> sys.getsizeof('cat')
52
>>> sys.getsizeof('a much longer string than just "cat"')
85
(在我使用的 Python 版本中,string 对象的开销占用 49 个字节,而字符串中的每个实际字符占用 1 个字节。)但是包含这些字符串中任何一个的列表都要占用 72 个字节,不管字符串有多长:
>>> sys.getsizeof(['cat'])
72
>>> sys.getsizeof(['a much longer string than just "cat"'])
72
原因是,从技术上讲,列表不包含字符串,而只是对字符串的引用,无论引用的数据大小如何,引用的大小都是一样的。类似于newGreetings.append(word)
的代码并没有复制word
中的字符串,而是复制了对该字符串的引用。如果你想知道一个对象及其引用的所有对象占用了多少内存,Python 核心开发者 Raymond Hettinger 为此编写了一个函数,你可以在code.activestate.com/recipes/577504-compute-memory-footprint-of-an-object-and-its-cont
访问这个函数。
所以你不应该觉得创建一个新的列表而不是在迭代时修改原来的列表是在浪费内存。即使您的列表修改代码看似有效,它也可能是需要很长时间才能发现和修复的细微错误的来源。浪费一个程序员的时间远比浪费一台计算机的内存更昂贵。
尽管在遍历列表(或任何可迭代对象)时不应该添加或删除列表中的项目,但是修改列表的内容是很好的。例如,我们有一个字符串形式的数字列表:['1', '2', '3', '4', '5']
。我们可以在遍历列表时将这个字符串列表转换成整数列表[1, 2, 3, 4, 5]
:
>>> numbers = ['1', '2', '3', '4', '5']
>>> for i, number in enumerate(numbers):
... numbers[i] = int(number)
...
>>> numbers
[1, 2, 3, 4, 5]
这段代码的可视化执行在autbor.com/covertstringnumbers
进行。修改列表中的项目就可以了;它改变了列表中容易出错的条目的数量。
在列表中安全地添加或删除条目的另一种可能的方法是从列表的末尾向后迭代到开头。这样,您可以在遍历列表时从列表中删除项,或者向列表中添加项,只要将它们添加到列表的末尾。例如,输入下面的代码,它从someInts
列表中删除偶数整数。
>>> someInts = [1, 7, 4, 5]
>>> for i in range(len(someInts)):
...
... if someInts[i] % 2 == 0:
... del someInts[i]
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
>>> someInts = [1, 7, 4, 5]
>>> for i in range(len(someInts) - 1, -1, -1):
... if someInts[i] % 2 == 0:
... del someInts[i]
...
>>> someInts
[1, 7, 5]
这段代码之所以有效,是因为循环将来迭代的所有项的索引都没有改变。但是在删除的值之后,值的重复上移使得这种技术对于长列表来说效率很低。这段代码的可视化执行在autbor.com/iteratebackwards1
进行。你可以在图 8-3 中看到向前迭代和向后迭代的区别。
图 8-3:向前(左)和向后(右)迭代时从列表中删除偶数
类似地,当您向后遍历列表时,您可以将项目添加到列表的末尾。在交互式 Shell 中输入以下内容,它会将someInts
列表中任何偶数的副本附加到列表的末尾:
>>> someInts = [1, 7, 4, 5]
>>> for i in range(len(someInts) - 1, -1, -1):
... if someInts[i] % 2 == 0:
... someInts.append(someInts[i])
...
>>> someInts
[1, 7, 4, 5, 4]
这段代码的可视化执行在autbor.com/iteratebackwards2
进行。通过向后迭代,我们可以在列表中添加或删除条目。但是这可能很难做到正确,因为对这一基本技术的微小改变最终可能会引入错误。创建新列表比修改原始列表简单得多。正如 Python 核心开发者 Raymond Hettinger 所说:
- 问:循环遍历列表时修改列表的最佳实践是什么?
- 答:不要这么做。
不要在不使用copy.copy()
和copy.deepcopy()
的情况下复制可变值
最好将变量视为引用对象的标签或名称标记,而不是包含对象的盒子。这个心智模型在修改可变对象时特别有用:列表、字典和集合等对象,它们的值可以发生变化(即改变)。当将引用可变对象的一个变量复制到另一个变量,并认为正在复制实际的对象时,会出现一个常见的问题。在 Python 中,赋值语句从不复制对象;它们只复制对一个对象的引用。(Python 开发者 Ned Batchelder 在 PyCon 2015 上有一个关于这个想法的精彩演讲,题目是“关于 Python 名称和值的事实和误解”在youtu.be/_AEJHKGk9ns
观看。)
例如,在交互式 Shell 中输入以下代码,注意,即使我们只更改了spam
变量,cheese
变量也会更改:
>>> spam = ['cat', 'dog', 'eel']
>>> cheese = spam
>>> spam
['cat', 'dog', 'eel']
>>> cheese
['cat', 'dog', 'eel']
>>> spam[2] = 'MOOSE'
>>> spam
['cat', 'dog', 'MOOSE']
>>> cheese
['cat', 'dog', 'MOOSE']
>>> id(cheese), id(spam)
2356896337288, 2356896337288
这段代码的可视化执行在autbor.com/listcopygotcha1
进行。如果你认为cheese = spam
复制了列表对象,你可能会惊讶于cheese
似乎已经改变了,尽管我们仅仅是修改了spam
。但是赋值语句从不复制对象,只复制对象的引用。赋值语句cheese = spam
使cheese
引用与spam
在计算机内存中相同的列表对象。它不会复制列表对象。这就是为什么改变spam
也会改变cheese
:两个变量引用同一个列表对象。
同样的原则也适用于传递给函数调用的可变对象。在交互式 Shell 中输入以下内容,注意全局变量spam
和局部参数(记住,参数是在函数的def
语句中定义的变量)theList
都指向同一个对象:
>>> def printIdOfParam(theList):
... print(id(theList))
...
>>> eggs = ['cat', 'dog', 'eel']
>>> print(id(eggs))
2356893256136
>>> printIdOfParam(eggs)
2356893256136
这段代码的可视化执行在autbor.com/listcopygotcha2
进行。注意,id()
为eggs
和theList
返回的标识是相同的,这意味着这些变量引用同一个列表对象。eggs
变量的列表对象没有复制到theList
;相反,引用是复制的,这就是为什么两个变量引用同一个列表。一个引用的大小只有几个字节,但是想象一下如果 Python 复制了整个列表而不仅仅是引用。如果eggs
包含十亿个条目而不是三个,那么将它传递给printIdOfParam()
函数将需要复制这个巨大的列表。仅仅是做一个简单的函数调用,就要消耗掉千兆字节的内存!这就是为什么 Python 赋值只复制引用,从不复制对象。
防止这种情况的一种方法是用copy.copy()
函数复制列表对象(不仅仅是引用)。在交互式 Shell 中输入以下内容:
>>> import copy
>>> bacon = [2, 4, 8, 16]
>>> ham = copy.copy(bacon)
>>> id(bacon), id(ham)
(2356896337352, 2356896337480)
>>> bacon[0] = 'CHANGED'
>>> bacon
['CHANGED', 4, 8, 16]
>>> ham
[2, 4, 8, 16]
>>> id(bacon), id(ham)
(2356896337352, 2356896337480)
这段代码的可视化执行在autbor.com/copycopy1
上。ham
变量引用一个复制的列表对象,而不是由bacon
引用的原始列表对象,所以它不会受到这个问题的影响。
但是就像变量像标签或名字标签而不是包含对象的盒子一样,列表也包含引用对象而不是实际对象的标签或名字标签。如果您的列表包含其他列表,copy.copy()
仅复制对这些内部列表的引用。在交互式 Shell 中输入以下内容来查看该问题:
>>> import copy
>>> bacon = [[1, 2], [3, 4]]
>>> ham = copy.copy(bacon)
>>> id(bacon), id(ham)
(2356896466248, 2356896375368)
>>> bacon.append('APPENDED')
>>> bacon
[[1, 2], [3, 4], 'APPENDED']
>>> ham
[[1, 2], [3, 4]]
>>> bacon[0][0] = 'CHANGED'
>>> bacon
[['CHANGED', 2], [3, 4], 'APPENDED']
>>> ham
[['CHANGED', 2], [3, 4]]
>>> id(bacon[0]), id(ham[0])
(2356896337480, 2356896337480)
这段代码的可视化执行在autbor.com/copycopy2
进行。虽然bacon
和ham
是两个不同的列表对象,但是它们引用相同的[1, 2]
和[3, 4]
内部列表,所以对这些内部列表的更改会在两个变量中得到反映,即使我们使用了copy.copy()
。解决方案是使用copy.deepcopy()
,它将复制被复制的列表对象中的任何列表对象(以及那些列表对象中的任何列表对象,等等)。在交互式 Shell 中输入以下内容:
>>> import copy
>>> bacon = [[1, 2], [3, 4]]
>>> ham = copy.deepcopy(bacon)
>>> id(bacon[0]), id(ham[0])
(2356896337352, 2356896466184)
>>> bacon[0][0] = 'CHANGED'
>>> bacon
[['CHANGED', 2], [3, 4]]
>>> ham
[[1, 2], [3, 4]]
这段代码的可视化执行在autbor.com/copydeepcopy
进行。虽然copy.deepcopy()
比copy.copy()
稍微慢一点,但是如果你不知道被复制的列表是否包含其他列表(或者其他可变对象,比如字典或者集合),那么使用它会更安全。我的一般建议是总是使用copy.deepcopy()
:它可能会防止细微的错误,并且你的代码可能不会被察觉。
不要使用可变值作为默认参数
Python 允许您为您定义的函数中的参数设置默认参数。如果用户没有显式设置参数,函数将使用默认参数执行。当对函数的大多数调用使用相同的参数时,这很有用,因为默认的参数使参数成为可选的。例如,为split()
方法传递None
会使其在空白字符上分割,但None
也是默认参数:调用'cat dog'.split()
与调用'cat dog'.split(None)
做同样的事情。该函数使用默认参数作为参数的参数,除非调用方传入一个参数。*
但是你不应该设置一个可变对象,比如一个列表或者字典,作为默认参数。要了解这是如何导致错误的,请看下面的例子,它定义了一个addIngredient()
函数,将一个配料字符串添加到一个代表三明治的列表中。因为这个列表的第一项和最后一项通常是'bread'
,所以可变列表['bread', 'bread']
被用作默认参数:
>>> def addIngredient(ingredient, sandwich=['bread', 'bread']):
... sandwich.insert(1, ingredient)
... return sandwich
...
>>> mySandwich = addIngredient('avocado')
>>> mySandwich
['bread', 'avocado', 'bread']
但是使用一个可变的对象,比如像['bread', 'bread']
这样的列表作为默认参数有一个微妙的问题:列表是在函数的def
语句执行时创建的,而不是在每次调用函数时创建的。这意味着只创建了一个['bread', 'bread']
列表对象,因为我们只定义了一次函数。但是每个函数调用到addIngredient()
都会重用这个列表。这会导致意外的行为,如下所示:
>>> mySandwich = addIngredient('avocado')
>>> mySandwich
['bread', 'avocado', 'bread']
>>> anotherSandwich = addIngredient('lettuce')
>>> anotherSandwich
['bread', 'lettuce', 'avocado', 'bread']
因为addIngredient('lettuce')
最终使用了与之前调用相同的默认参数列表,其中已经添加了'avocado'
,而不是['bread', 'lettuce', 'bread']
,所以函数返回['bread', 'lettuce', 'avocado', 'bread']
。因为sandwich
参数列表与最后一次函数调用相同,所以'avocado'
字符串再次出现。只创建了一个['bread', 'bread']
列表,因为函数的def
语句只执行一次,而不是每次调用函数时都执行。这段代码的可视化执行在autbor.com/sandwich
进行。
如果需要使用列表或字典作为默认参数,Python 风格的解决方案是将默认参数设置为None
。然后编写代码来检查这一点,并在调用该函数时提供新的列表或字典。这确保了每次调用函数时,函数都会创建一个新的可变对象,而不是在定义函数时只调用一次函数,如下例所示:
>>> def addIngredient(ingredient, sandwich=None):
... if sandwich is None最全Android Kotlin入门教程(Kotlin 入门指南高级Kotlin强化实战Kotlin协程进阶实战)
Kotlin 是一种新型的静态类型编程语言,有超过 60% 的专业 Android 开发者在使用,它有助于提高工作效率、开发者满意度和代码安全性。不仅可以减少常见代码错误,还可以轻松集成到现有应用中。
目前在安卓开发中,当你查看源码的时候,你会发现大量的Kotlin 源码,在使用一些 jetpack 框架的时候,大部分也都是Kotlin 语言。相信不久之后,Kotlin 将会成为 Android 开发的首选语言。
在这里为了方便大家系统的学习Kotlin,这里特意联合了阿里P7架构师和谷歌技术团队共同整理了一份Kotlin全家桶学习资料(点击文末卡片免费领取~)。
内容概要:Kotlin 入门教程指南、高级Kotlin强化实战和史上最详Android版kotlin协程入门进阶实战 。
内容特点:条理清晰,含图像化表示更加易懂。
《Kotlin入门教程指南》
第一章 Kotlin 入门教程指南
- 前言
第二章 概述
- 使用 Kotlin 进行服务器端开发
- 使用 Kotlin 进行 Android 开发
- Kotlin JavaScript 概述
- Kotlin/Native 用于原生开发
- 用于异步编程等场景的协程
- Kotlin 1.1 的新特性
- Kotlin 1.2 的新特性
- Kotlin 1.3 的新特性
第三章 开始
- 基本语法
- 习惯用法
- 编码规范
第四章 基础
- 基本类型
- 包
- 控制流:if、when、for、while
- 返回和跳转
第五章 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类与内部类
- 枚举类
- 对象表达式与对象声明
- Inline classes
- 委托
第六章 函数与 Lambda 表达式
- 函数
- 高阶函数与 lambda 表达式
- 内联函数
第七章 其他
- 解构声明
- 集合:List、Set、Map
- 区间
- 类型的检查与转换“is”与“as”
- This 表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 类型安全的构建器
- 类型别名
- 多平台程序设计
- 关键字与操作符
第八章 Java 互操作与 JavaScript
- 在 Kotlin 中调用 Java 代码
- Java 中调用 Kotlin
- JavaScript 动态类型
- Kotlin 中调用 JavaScript
- JavaScript 中调用 Kotlin
- JavaScript 模块
- JavaScript 反射
- JavaScript DCE
第九章 协程
- 协程基础
- 取消与超时
- 通道 (实验性的)
- 组合挂起函数
- 协程上下文与调度器
- 异常处理
- select 表达式(实验性的)
- 共享的可变状态与并发
第十章 工具
- 编写 Kotlin 代码文档
- Kotlin 注解处理
- 使用 Gradle
- 使用 Maven
- 使用 Ant
- Kotlin 与 OSGi
- 编译器插件
- 不同组件的稳定性
第十一章 常见问题总结
- FAQ
- 与 Java 语言比较
- 与 Scala 比较【官方已删除】
《高级Kotlin强化实战》
第一章 Kotlin 入门教程
- Kotlin 概述
- Kotlin 与 Java 比较
- 巧用 Android Studio
- 认识 Kotlin 基本类型
- 走进 Kotlin 的数组
- 走进 Kotlin 的集合
- 完整代码
- 基础语法
第二章 Kotlin 实战避坑指南
- 方法入参是常量,不可修改
- 不要 Companion、INSTANCE?
- Java 重载,在 Kotlin 中怎么巧妙过渡一下?
- Kotlin 中的判空姿势
- Kotlin 复写 Java 父类中的方法
- Kotlin “狠”起来,连TODO都不放过!
- is、as` 中的坑
- Kotlin 中的 Property 的理解
- also 关键字
- takeIf 关键字
- 单例模式的写法
第三章 项目实战《Kotlin Jetpack 实战》
- 从一个膜拜大神的 Demo 开始
- Kotlin 写 Gradle 脚本是一种什么体验?
- Kotlin 编程的三重境界
- Kotlin 高阶函数
- Kotlin 泛型
- Kotlin 扩展
- Kotlin 委托
- 协程“不为人知”的调试技巧
- 图解协程:suspend
《史上最详Android版kotlin协程入门进阶实战》
第一章 Kotlin协程的基础介绍
- 协程是什么
- 什么是Job 、Deferred 、协程作用域
- Kotlin协程的基础用法
第二章 kotlin协程的关键知识点初步讲解
- 协程调度器
- 协程上下文
- 协程启动模式
- 协程作用域
- 挂起函数
第三章 kotlin协程的异常处理
- 协程异常的产生流程
- 协程的异常处理
第四章 kotlin协程在Android中的基础应用
- Android使用kotlin协程
- 在Activity与Framgent中使用协程
- ViewModel中使用协程
- 其他环境下使用协程
第五章 kotlin协程的网络请求封装
- 协程的常用环境
- 协程在网络请求下的封装及使用
- 高阶函数方式
- 多状态函数返回值方式
第六章 深入kotlin协程原理(一)
- suspend的花花肠子
- 藏在身后的-Continuation
- 村里的希望-SuspendLambda
第七章 深入kotlin协程原理(二)
- 协程的那些小秘密
- 协程的创建过程
- 协程的挂起与恢复
- 协程的执行与状态机
第八章 Kotlin Jetpack 实战
- 从一个膜拜大神的 Demo 开始
- Kotlin 写 Gradle 脚本是一种什么体验?
- Kotlin 编程的三重境界
- Kotlin 高阶函数
- Kotlin 泛型
- Kotlin 扩展
- Kotlin 委托
- 协程“不为人知”的调试技巧
- 图解协程原理
第九章 Kotlin + 协程 + Retrofit + MVVM优雅的实现网络请求
- 项目配置
- 实现思路
- 协程实现
- 协程 + ViewModel + LiveData实现
- 后续优化
- 异常处理
- 更新Retrofit 2.6.0
《Kotlin入门教程指南》完整版可点击文末卡片查看获取方式!!
以上是关于Python 进阶指南(编程轻松进阶):八常见的 Python 陷阱的主要内容,如果未能解决你的问题,请参考以下文章
《算法竞赛进阶指南》0x27A* 八数码问题 POJ1077
4万字Python高级编程保姆式教学,进阶感觉到吃力?学完这些就轻松了