欢迎关注 ,专注Python、数据分析、数据挖掘、好玩工具!
一、前言
经常有人会问:Python 有哪些让你相见恨晚的技巧?我今天准备把这个问题认真回答一下。我会先讨论什么是优美的代码;然后,我会给出一些我压箱底的好东西;最后,我会讨论怎么写出优美的代码。
二、什么是优美(优雅)的代码
什么是优美或优雅的代码实现呢?在Python里面,我们一般称之为Pythonic。Pythonic并没有一个确切的定义,一直以来都是只能意会,不能言传的东西。为了帮助新同学理解,我对Pythonic给出了明确的定义:所谓Pythonic,就是用Python的方式写出简洁优美的代码。
有了Pythonic以后,不同的工程师之间,也依然无法对优美的代码达成一致的意见。因为,美本身是一个主观感受,每个人对美的感受是不一样的。比如,有些人觉得汤唯更美,有些人觉得范冰冰最漂亮,还有些人居然喜欢AngelaBaby。而我,依然最喜欢刘涛。我在这篇文章中,会给出很多具体的例子,来说明怎样写代码是’美’的,由于美是一种主观感受,所以,这里的回答可能会引起大家的争议。
另外,在这篇文章中,我们只讨论优美的Python代码实现,并不讨论Python中存在的坑。我估计Python里面有很多坑大家都没有注意到,比如:
>>> a = 4.2
>>> b = 2.1
>>> print a+b == 6.3
False
三、优美的代码实现
在这一部分,我们会依次讨论一些美的代码。由于内容较多,所以,我进行了简单地分类,包括:
3.1 善用内置函数
enumerate类
enumerate是一个类,但是用起来却跟函数一样方便,为了表述方便,我们后面统称为函数。不使用enumerate可能是Python新手最容易被吐槽的地方了。enumerate其实非常简单,接收一个可迭代对象,返回index和可迭代对象中的元素的组合。
对于Python新手,推荐使用ipython(还有bpython和ptpython,感兴趣的同学也可以了解一下)交互式地测试各个函数的效果,并且,我们可以在函数后面输入一个问号,然后回车,就能够获得这个函数的帮助文档了。如下所示:
In [1]: enumerate?
Type: type
String Form:<type 'enumerate'>
Namespace: Python builtin
Docstring:
enumerate(iterable[, start]) -> iterator for index, value of iterable
Return an enumerate object. iterable must be another object that supports
iteration. The enumerate object yields pairs containing a count (from
start, which defaults to zero) and a value yielded by the iterable argument.
enumerate is useful for obtaining an indexed list:
(0, seq[0]), (1, seq[1]), (2, seq[2]), ...
关于enumerate的效果,我们一起来看一下,你就知道为什么不使用enumerate会被吐槽了。这是不使用enumerate的时候,打印列表中的元素和元素在列表中的位置代码:
from __future__ import print_function
L = [ i*i for i in range(5) ]
index = 0
for data in L:
index += 1
print(index, ':', data)
这是使用enumerate的Python代码:
from __future__ import print_function
L = [ i*i for i in range(5) ]
for index, data in enumerate(L):
print(index + 1, ':', data)
这是正确使用enumerate的姿势:
from __future__ import print_function
L = [ i*i for i in range(5) ]
for index, data in enumerate(L, 1):
print(index, ':', data)
去除import语句和列表的定义,实现同样的功能,不使用enumerate需要4行代码,使用enumerate只需要2行代码。如果想把代码写得简洁优美,那么,大家要时刻记住:在保证代码可读性的前提下,代码越少越好。显然,使用enumerate效果就好很多。
reversed
对Python熟悉的同学知道,Python中的列表支持切片操作,可以像L[::-1]这样去reverse列表。如下所示:
[1, 2, 3, 4]
>>> for item in L[::-1]:
... print(item)
...
4
3
2
1
与此同时,我们也可以使用内置的reversed函数,如下所示:
>>> for item in reversed(L):
... print(item)
...
4
3
2
1
我的观点是,L[::-1]不如使用reversed好,因为,L[::-1]是一个切片操作。我们看到这个代码的第一反应是序列切片,然后才是切片的效果是reverse列表。对于reversed函数,即使是刚接触Python的同学,也能够一眼看出来这个函数是要做什么事情。也就是说,实现同样的功能,L[::-1]比reversed多绕了一个弯。我们这个问题是如何写出优美的代码,而我认为,优美的代码就应该简洁、直接、少绕弯。
读者如果对我这里的解释表示怀疑的话,我表示理解。但是,我还是想劝你认可我的说法。因为我认为,不管我们使用代码还是文字,都是在表达某些东西。而我的表达能力,也是读研究生以后写论文锻炼出来的。就我目前比大多数人强的表达能力来说,我以我母校的荣誉保证,reversed确实比L[::-1]好。
any
在内置函数中,sort、sum、min和max是大家用的比较多的,也比较熟悉的。像any和all这种函数,是大家都知道,并且觉得很简单,但是使用的时候就想不起来的。我们来看一个具体的例子。
我们现在的需求是判断MySQL中的一张表是否存在主键,有主键的情况,如下所示:
mysql> show index from t;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)
我们再来看一个没有主键的例子,如下所示:
mysql> show index from t;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t | 0 | id | 1 | id | A | 0 | NULL | NULL | | BTREE | | |
| t | 1 | idx_age | 1 | age | A | 0 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
在这个没有主键的例子中,虽然没有显示定义主键,但是,它有一个非空的唯一索引。在InnoDB中,如果存在非空的唯一约束,那么,这一列将会被当作主键。综合前面两种情况的输出,我们知道,我们要判断一张表是否存在主键,我们不能通过是否存在一个key_name名为PRIMARY的索引来判断,而应该通过Non_unique为0和Null列不为YES来判断。说完了需求,我们来看一下具体的实现。使用pymysql连接数据库,数据库中的每一行,将会以元组的形式返回,如下所示:
(('t', 0, 'PRIMARY', 1, 'id', 'A', 0, None, None, '', 'BTREE', '', ''),)
也就是说,我们现在要遍历一个二维的元组,然后判断是否存在Non_unique为0,Null列不为YES的记录。详细了解了具体实现以后,我们写下了下面的代码:
def has_primary_key():
for row in rows:
if row[1] == 0 and row[9] != 'YES':
return True
return False
非常的简单,但是,如果我们使用any函数的话,代码将会更短。如下所示:
def has_primary_key():
return any(row[1] == 0 and row[9] != 'YES' for row in rows):
从这一节大家可以看到,即使内置函数这么简单的知识,我们也要充分掌握,灵活使用,才能够写出优美的代码。
3.2 Python中的小细节
这一节我们来看3个很小的知识点。
raise SystemExit
假设你现在要实现一个需求,在程序检测到某种错误的时候,打印错误信息,并退出程序。在Python中,我们可以是SystemExit,如下所示:
import sys
sys.stderr.write('It failed!\n')
raise SystemExit(1)
但是,你其实可以直接这么用的:
raise SystemExit('It failed!')
后面的这个操作会直接将信息打印到标准错误输出,然后使用退出码为1来退出程序,以表示程序没有正常退出。
文件的x模式
大家应该知道,如果我们以w模式打开一个文件进行写入的话,文件的内容将会被我们覆盖掉。假设你现在有这样一个需求:写一个文件,如果该文件已经存在,则不写。实现方式也很简单,我们先判断一下文件是否存在,如果已经存在,则打印提示信息并跳过,否则,我们就以w模式打开文件,然后写入内容。如下所示:
>>> import os
>>> if not os.path.exists('somefile'):
... with open('somefile', 'wt') as f:
... f.write('Hello\n')
... else:
... print('File already exists!')
...
File already exists!
如果我们使用x模式的话,代码能够好看很多,如下所示:
>>> with open('somefile', 'xt') as f:
... f.write('Hello\n')
ConfigParser
上面两个例子知道的人可能比较多,这个例子知道的人可能就不多了。在大部分服务中,会将如数据库连接参数这样的配置,写到配置文件中,然后使用ConfigParser来管理。连接数据库的时候,我们可以读取配置参数,然后生成连接字符串。其实,ConfigParser本身就提供了生成连接字符串的功能,如下所示:
$cat db.conf
[DEFAULT]
conn_str = %(dbn)s://(%user)s:%(pw)s@%(host)s:%(port)s/%(db)s
dbn = mysql
user = root
pw = root
host = localhost
port = 3306
db = test
这里给出了几个Python中的小细节,可能很多人会觉得没啥用,又或者大家其实已经知道了。但是,我还是把这一节放上来了,只要对一个人有用,那么,这就是有意义的。
3.3 合理使用数据结构
这一节中,我们会讨论Python中的部分数据结构的用法,可能是对大家最有用的章节。
字典的get可以传递默认值
很多人是这么给参数赋默认值的:
port = kwargs.get('port')
if port is None:
port = 3306
其实,我们完全不用这么麻烦,因为,字典的get方法支持提供默认参数,在字典没有值的情况下,将会返回用户提供的默认参数,所以,优美的代码应该是这样的:
port = kwargs.get('port', 3306)
我们再来看一个类似的例子,获取元素且删除:
L = [1, 2, 3, 4]
last = L[-1]
L.pop()
这里,我们又多此一举了。因此,在调用L.pop()函数的时候,本身就会返回给我们需要pop的数据。也就是说,我们可以这样:
defaultdict & Counter
我们来看两个不是Python内置的数据类型,而是Python标准库里面的例子。
假设我们的字典中的value是一个list。大家知道,list也是元素的集合。所以,我们会见到很多下面这样的代码,先判断key是否已经存在,如果不存在,则新建一个list并赋值给key,如果已经存在,则调用list的append方法,将值添加进去。
d = {}
for key, value in pairs:
if key not in d:
d[key] = []
d[key].append(value)
如果我们知道defaultdict,那就不用这么麻烦了,如下所示:
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)
我们接着上面的defaultdit的例子来讨论。现在有一个需求,需要统计一个文件中,每个单词出现的次数。很多新同学拿到这个题目,首先想到的是使用字典,所以,写出来下面这样的代码:
d = {}
with open('/etc/passwd') as f:
for line in f:
for word in line.strip().split(':'):
if word not in d:
d[word] = 1
else:
d[word] += 1
如果我们使用前面介绍的defaultdict,代码能够减少3行,会被认为更加Pythonic。如下所示:
d = defaultdict(int)
with open('/etc/passwd') as f:
for line in f:
for word in line.strip().split(':'):
d[word] += 1
对于这个问题,其实还有一个更好的解决办法,使用collections中的Counter。如下:
word_counts = Counter()
with open('/etc/passwd') as f:
for line in f:
word_counts.update(line.strip().split(':'))
可以看到,使用Counter以后,我们的代码更加短小了。我们先把代码重8行重构到5行,然后又重构到4行。记住我前面给出的实践方法:要想把代码写得优美,在保证可读性的前提下,代码越短越好。对于这个问题,使用Counter还有其他的一些理由,那就是其他相关的需求。比如,现在还有第二个需求,打印出现次数最多的三个单词。如果我们使用字典,那么,我们需要这样:
result = sorted(zip(d.values(), d.keys()), reverse=True)[:3]
for val, key in result:
print(key, ':', val)
使用Counter就简单了,因为Counter直接就为我们提供了相应的函数,如下所示:
for key, val in (word_counts.most_common(3)):
print(key, ':', val)
是不是代码更短,看起来更加清晰呢。而且,统计每个单词出现的次数和出现次数最多的单词,这两个需求相关性实在是太强了,几乎会同时出现。所以,我们使用了Counter模块和该模块的most_common方法。如果Counter没有提供这个方法,那才是要被吐槽的!
这个例子就充分说明了,善用标准库的重要性。我们再来看一个标准库的例子。
nametuple
我们要写一个监控系统,该系统要监控主机的方方面面。当然,也包括磁盘相关的监控。我们可以从/proc/diskstats中获取磁盘的详细信息。/proc/diskstats 的内容像下面这样
"""
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
What: /proc/diskstats
The /proc/diskstats file displays the I/O statistics
of block devices. Each line contains the following 14
fields:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
"""
$ cat /proc/diskstats
254 0 vda 24471 251 818318 33200 37026 64382 2289256 394516 0 24868 427704
254 1 vda1 24143 229 815530 33156 36072 64382 2289256 394428 0 24748 427572
254 32 vdc 614 0 22180 408 51203 2822 1857922 1051716 0 40792 1052064
/proc/diskstats文件有很多列,如果我们使用下标访问的话,肯定需要借助我们的手指头。并且,也不一定能数清楚。就算你算术特别厉害,能够轻易地数清楚,如果下一个人要来修改你写的代码,对他来说,将会是一个噩梦。
作为一个有追求的程序员,我们当然要寻找更好的办法。对于这个问题,我们可以使用Python中的命名元组,也就是collections中的namedtuple。我们定义命名元组:
DiskDevice = collections.namedtuple('DiskDevice', 'major_number minor_number device_name read_count read_merged_count'
' read_sections time_spent_reading write_count write_merged_count '
'write_sections time_spent_write io_requests time_spent_doing_io'
' weighted_time_spent_doing_io')
有了命名元组以后,如果我们要返回某个磁盘的请求数据,就返回一个命名元组。调用者通过该命名元组,就能够通过属性的方式,而不是下标的方式访问各个字段。获取磁盘监控的代码如下:
def get_disk_info(disk_name):
with open("/proc/diskstats") as f:
for line in f:
if line.split()[2] == disk_name:
#返回给调用者的是一个命名元祖(namedtuple)
return DiskDevice(*(line.split()))
3.4 使用高级并发工具
数据结构先讲到这边,我们来看一个并发的例子。即生产者和消费者模型。分别创建生产者和消费者,生产者向队列中放东西,消费者从队列中取东西。创建一个锁来保证线程间操作的互斥性,当队列满的时候,生产者进入等待状态,当队列空的时候,消费者进入等待状态。下面是一个简单的实现Producer-consumer problem in Python:
from threading import Thread, Condition
import time
import random
queue = []
MAX_NUM = 10
condition = Condition()
class ProducerThread(Thread):
def run(self):
nums = range(5)
global queue
while True:
condition.acquire()
if len(queue) == MAX_NUM:
print "Queue full, producer is waiting"
condition.wait()
print "Space in queue, Consumer notified the producer"
num = random.choice(nums)
queue.append(num)
print "Produced", num
condition.notify()
condition.release()
time.sleep(random.random())
class ConsumerThread(Thread):
def run(self):
global queue
while True:
condition.acquire()
if not queue:
print "Nothing in queue, consumer is waiting"
condition.wait()
print "Producer added something to queue and notified the consumer"
num = queue.pop(0)
print "Consumed", num
condition.notify()
condition.release()
time.sleep(random.random())
ProducerThread().start()
ConsumerThread().start()
对于这一类同步问题,其实,我们应该直接使用Queue。Queue提供了线程安全的队列,特别适合用来解决生产者和消费者问题。这里我再强调一下,Queue本身是线程安全的,而且支持阻塞读、阻塞写。如下所示:
from threading import Thread
import time
import random
from Queue import Queue
queue = Queue(10)
class ProducerThread(Thread):
def run(self):
nums = range(5)
global queue
while True:
num = random.choice(nums)
queue.put(num)
print "Produced", num
time.sleep(random.random())
class ConsumerThread(Thread):
def run(self):
global queue
while True:
num = queue.get()
queue.task_done()
print "Consumed", num
time.sleep(random.random())
ProducerThread().start()
ConsumerThread().start()
可以看到,使用Queue以后,代码量少了很多,可维护性也强了不少。当然了,我这里只是举了一个特别简单的例子,只是想说明,我们应该使用高级的并发工具。
我们再来看一个并发的例子。很多时候,我们要用并发编程,并不用自己手动启动一个线程或进程,完全可以使用Python提供的并发工具,如下所示:
内置的map是单线程运行的,如果涉及到网络请求或者大量的cpu计算,则速度相对会慢很多,因此, 出现了并发的map,如下所示:
import requests
from multiprocessing import Pool
def get_website_data(url):
r = requests.get(url)
return r.url
def main():
urls = ['http://www.google.com',
'http://www.baidu.com',
'http://www.163.com']
pool = Pool(2)
print pool.map(get_website_data, urls)
main()
为了与线程兼容,该模块还提供了multiprocessing.dummy,用以提供线程池的实现,如下所示:
from multiprocessing.dummy import Pool
使用Pool,我们可以快速的在线程池和进程池之间来回切换,最重要的是,使用高级的并发工具不那么容易出错。
3.5 使用装饰器
装饰器可能是Python面试中被问的最多的知识了。之所以如此频繁的出现,是因为,装饰器确实是个好东西。举个例子,我们有两个模块,A模块要发消息给B模块,B模块检查A模块发送过来的参数,没有问题就进行处理,对于检查参数这个操作,如果我们使用装饰器的话,代码将会是下面这样:
import inspect
import functools
def check_args(parameters):
"""check parameters of action"""
def decorated(f):
"""decorator"""
@functools.wraps
def wrapper(*args, **kwargs):
"""wrapper"""
func_args = inspect.getcallargs(f, *args, **kwargs)
msg = func_args.get('msg')
for item in parameters:
if msg.body_dict.get(item) is None:
return False, "check failed, %s is not found" % item
return f(*args, **kwargs)
return wrapper
return decorated
使用的时候:
class AsyncMsgHandler(MsgHandler):
@check.check_args(['ContainerIdentifier', 'MonitorSecretKey', "InstanceID", "UUID"])
def init_container(self, msg):
pass
这样很多好处,例如,我们不用为不同的消息,编写不同的参数检查函数。我们将参数检查和业务逻辑完全分离,使得代码更加简洁明了。当然,装饰器还有各种用处,我就不多说了。
3.6 Python中的设计模式
设计模式本来是与编程语言不相关的,属于程序架构的范畴。但是,有些设计模式,在Python中,完全可以使用Python的一些特性,实现得更加简洁优美。
单例模式
模式是最简单也比较常用的一种模式,所谓单例模式,就是要保证系统中一个类只有一个实例。我们来看一个典型的单例实现:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面这段java代码大家可以轻易地转换成Python的实现,然而,并不Pythonic。给大家一种思路,提供了一个Borg类,只要继承该类,就会成为单例。
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self.__shared_state
这个实现的思路是"实例的唯一性并不是重要的,我们应该关注的是实例的状态,只要所有的实例共享状态,行为一致,那就达到了单例的目的"。通过Borg模式,可以创建任意数量的实例,但因为它们共享状态,从而保证了行为一致。
对Python的模块熟悉的人会知道,在Python中,模块只初始化一次,所有变量归属于某个模块,import机制是线程安全的。所以,模块本身是天然的单例实现。因此,我们可以借助模块来实现单例模式。
下面这个例子创建了一个函数,令其返回含有货币汇率的字典,这个函数可以被多次调用,但是,大部分情况下,汇率数据只获取一次就够了,无须每次调用时都获取一遍。
def get():
if not get.rates:
_URL = 'http://www.bankofcanada.ca/stats/assets/csv/fx-seven-day.csv'
with urllib.request.urlopen(_URL) as file:
for line in file:
# get.rates[key] = float(data)
pass
return get.rates
get.rates = {}
在这段代码中,我们创建了名为rates的字典,用于保存私有数据,并将该字典设置成Rates.get()函数的属性。第一次执行公开的get()函数时,会下载全新的汇率数据;其他时候,只需要把最近下载的那份数据返回就行了。尽管没有引入类,但我们依然把汇率数据做成了"单例数据值"。大家可以看到,在Python中,单例可以直接使用模块来实现,非常的简单。
工厂模式
我们再来看另外一个非常常用的设计模式,工厂模式。下面这段代码是C++编程思想 (豆瓣)中的工厂模式:
class Shape
class Circle: public Shape
class Square: public Shape
Shape* Shape::factory(const string &type)
{
if (type == "Circle")
{return new Circle;}
if (type == "Square")
{return new Square;}
}
说实话,实现得非常好,可读性很强。我们再看一下,同样的需求,在Python中如何实现:
class Shape:pass
class Circle(Shape):pass
class Square(Shape):pass
for name in ["Circle", "Square"]:
cls = globals()[name]
obj = cls()
要理解Python中的工厂模式,关键是要知道,Python中的类也是可调用的对象。而且,我们import以后,存在于当前的命名空间中,所以,我们可以先通过名字获取到"类",再用类构造出对象。比较这里的C++实现和Python实现可以发现,当我们需要新增一个图形的时候,C++的实现需要去修改Shape::factory函数,Python就不需要这么麻烦,也就是说,Python版本的工厂模式比C++版本的工厂模式少了一个需要维护的函数。从这个角度来说,Python程序员是幸福的。
四、如何写出优美(优雅)的代码
关于这个问题,我想说的还有很多,然而,我编辑了好几天也没有编辑完。总是有个结束的时候,那就到此为止吧。
关于如何能够写出Pythonic的代码,我的观点是:不管用什么语言,你都应该努力写出简洁优美的代码。如果不能,那我推荐你看看《重构》和《代码整洁之道》。虽然这两本书使用的是java语言,但是,并不影响作者要传递的思想。
此外,我也有一些经验传授给大家,希望能够帮助新同学快速地写出还不错的代码。
像写报纸一样写代码
准确无歧义
完整无废话
注意排版以引导读者
注意标点符号以帮助读者
保证可读性的前提下,代码尽可能短小
其中,“保证代码可读性的前提下,代码尽可能短小”,这个我已经反复强调,相信大家已经有点感觉了。我们来看一个"注意标点符号以帮助读者",在Python里面,空行是会被忽略的,也就说,有没有空行,有多少空行,对Python来说都是一样的,但是,对程序员来说可就不一样了,我们来看一个例子。随机生成1000个0~999之间的整数,然后求他们的和。
这里是没有空行的例子:
import random
def sum_num(num, min_num, max_num):
i = 0
data = []
for i in range(num):
data.append(random.randint(min_num, max_num))
total = 0
for item in data:
total += item
return total
if __name__ == '__main__':
print sum_num(1000, 0, 1000)
这里是有空行的例子:
import random
def sum_num(num, min_num, max_num):
i = 0
data = []
for i in range(num):
data.append(random.randint(min_num, max_num))
total = 0
for item in data:
total += item
return total
if __name__ == '__main__':
print sum_num(1000, 0, 1000)
可以看到,有空行的代码,明显看起来更加舒适愉悦。在大家用汉语写作文的时候,老师一直教导我们要合理的使用标点符号,合理的分段。其实,写代码和写文章是一样的。在代码中,大家可以这样想象:换行是逗号,空一行是句号,空两行是分段。至于逗号,由于我们总是在一行中写一条语句,所以,逗号是可以忽略的,如果你在一行中写了多条语句,就好比在写作文的时候没有正确的使用逗号,也让人难以理解。如果你从来不空行,所有代码纠缠在一起,就好比没有句号,让人读起来很累,同理,不分段也不是 好习惯。
参考链接:https://www.zhihu.com/question/48755767/answer/130286487
在这篇回答中,我给出了很多Python优雅实现的例子,然后,讨论了如何才能写出优美的代码,希望对大家有帮助。
欢迎转载、收藏、有所收获点赞支持一下!
目前开通了技术交流群,群友超过2000人,添加方式如下:
如下方式均可,添加时最好方式为:来源+兴趣方向,方便找到志同道合的朋友
- 方式一、发送如下图片至微信,进行长按识别,回复加群;
- 方式二、直接添加小助手微信号:pythoner666,备注:来自CSDN
- 方式三、微信搜索公众号:Python学习与数据挖掘,后台回复:加群