三顾Python,整理后成了我的一次分享

2,028

这是一次在字节内部的新人分享🤣 ,初衷是关于官网不同SDK版本下ApiDiff数据的数据获取

关于Python的几个优势

更简单的编程方式

打印Hello World

Java:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

Python:

print("Hello World")

数组打印

Java:

String s = "i wanna print";
String[] strs = s.split(" ");
// 方案1
System.out.println(strs);
// 方案2
for (String res : strs) System.out.print(res + " ");

Python:

s = "i wanna print"
print(s.split())

MapList等的使用

Java:

// List
List<String> list = new ArrayList<>();
list.add("Mike");
list.add("Lily");
// Map
Map<String, String> map = new HashMap<>();
map.put("Mike","First");
map.put("Lily","Second");

Python:

// List
list = ["Mike", "Lily"]
list.append("Jack")
// Map
map = {"Mike":"First",
       "Lily":"Second"}
map["Jam"] = "third"
del map["Jam"]

PythonListMap用更像数组的方式存在,并使用。

以上只是Python中一小部分的简便用法,还有很多可以探索。。

强大的开源社区

  1. Py社区www.python88.com
  2. PythonTabwww.pythontab.com
  3. StackOverflowstackoverflow.com/questions/t…
  4. PyChinapychina.org

还有很多很多尚待你去发现的社区。。

但是为了论证Python背后社区的强大性,显然还需要一个对比,这里拿StackOverflow上的不同tag来进行比较。

从一个Q&A的数量级上,想必已经足以支撑这一点了。

来自语言本身的优势

脚本语言又被称为扩建的语言,或者动态语言,是一种编程语言,用来控制软件应用程序,脚本通常以文本(如ASCII)保存,只在被调用时进行解释或编译。

Java / C这种解释 / 编译型语言,在没有Idea / Clion这类工具时,我们使用命令行去需要经过这样的步骤:

  1. javac xxx.java,编译生成class文件
  2. java xxx,运行class文件

作为脚本语言,我们可以通过一些方式来直接使用已经写好的命令并在终端上进行运行,并且从代码的可读性上比JavaScript更加好。

JavaScript.minPython

因为Python的代码分层是通过Tab来确立层级关系,即便在非常糟糕的情况的,代码的层次感依旧的。

丰富多彩的库🌟

  • 如果你希望像Java一样去构建一个Web程序,DjangoFlask两个框架给了你这个机会。
  • 如果你希望用它去做一些关于机器学习和数据科学库的任务,TensorFlowKerasPandas等等
  • 如果你想自己去玩一些爬虫项目,BeautifulSoup4selenium外加requests库,基本可以完成你想要的基础功能。
  • 还有更多的库等待你去挖掘。。。

入门Python开发

整个开发应该来说围绕这几个问题来进行展开。

##如何进行库的导入 一般来说库的导入会分为几种形式:

  1. 将整个模块导入,格式为:import module
  2. 从某个模块中导入某个类,格式为:from module import a
  3. 从某个模块中导入多个类,格式为:from module import a,b,c
  4. 将某个模块中的全部类导入,格式为:from module import *

BeautifulSoup4requests为例,同样都是import module的方式来进行导入。

import requests
import bs4

requests.get("URL")
BeautifulSoup(indexHtml.text, "lxml") // 报错

requests一般使用的函数就直接是get()post(),通过上述代码调用是直接可以使用的。但是对于bs4来说,我们一般使用的是内部的类,如果直接使用上述写法会报错,有两种写法。

// 方案1
from bs4 import BeautifulSoup
// 方案2
bs4.BeautifulSoup

这里就上面的内容给出一个比较好的记法,推类到Java时,你可以将requests.get("URL")中的get()方法认为是一个Java类中的一个静态方法,而像BeautifulSoup这样的类,你可以认为是Java中的一个内部类。

如果获取命令行参数,并规范使用?

这一步的开展,主要是为了将Python在命令行中的使用更加趋近于ShellJava其实同样可以完成这项任务。

我们熟知的Javamain(String args[])其中所包含的就是从命令行中抓取到的数据。

其实Python本身已经提供了这样的库,他会对在命令行中已经传入的数据进行获取,然后通过既定的库来进行数据的抓取和使用。

import sys,argparse

对于上述的内容,也就是命令行数据的抓取,使用sys这个库就已经能够满足要求了,可以通过这样的命令循环去直接查看。

sys.argv.pop() // 弹出最顶层的数据
sys.argv.__len__ // 抓取的数据数量
// 。。。。。

但是如果说像是-h、-v。。。等等诸多一系列的高级使用能够存在呢?

当然我们完全是可以自己实现的,但显然是一个费时不讨好的活儿。这就让argparse这个库有了用武之地了,他就是一个参数解析的工具。

通过类似上述代码的设定,对命令行中的数据获取就有了一定的diy空间了。

args = parser.parse_args() // 用于进行数据的解析
args.v // 输入的数据通过参数来进行数据获取

常见的循环的几种写法

这里提for循环的原因是,写法确实和Java有比较不同的点。

fruits = ['banana', 'apple',  'mango']
# 第一个实例,对字符串的字符进行遍历
for letter in 'Python':
    print(letter)
# 第二个实例,像Java中的String s: strs一样对数据进行遍历
for fruit in fruits:
    print(fruit)
# 第三个实例,区间遍历,对标Java:for(int i=0; i<fruits.length; i++)
for fruit in range(0, len(fruits)):
    print(fruit)
# 第四个实例,加入了一个[e lse]
for fruit in range(0, len(fruits)):
    print(fruit)
else: # 运行会发生在for循环非break跳出的情况下
    print("final")

但是是否有发现,这其中的实例三和Java的形态有所不同,Java的写法for(int i=0; i<fruits.length; i++)中的int i其实很自然的给出了下标的概念,但是如果从Python刚刚的几种遍历手段下来观察,只有一种方案我们能够拿到下标,那就是在外层定义一个index来用作下标位。但是在Python中其实提供了另外一种方案可以参考 —— 转化为枚举,也就是以下的代码。

for index,fruit in enumerate(fruits):
    print(index)

通过上述的写法,就可以直接获取下标和数据。

作为面向对象的语言,三大特性如何用代码进行展现

从上文的代码中,我们可以感知到到其实一些方面和Java还是非常类似的,但是请注意Python同样是一门面向对象的语言,那这就需要从三大方面来进行论证。

封装

class FileWriter:

    def __init__(self, version):
        self.writeToFilePath = "Deprecated_Method_SDK_%d.xlsx" % version

    '''
    暂时只做了excel类型的文件输出
    '''

    def outputFile(self, datas):
        fileData = pd.DataFrame(index=datas)
        writer = pd.ExcelWriter(self.writeToFilePath)
        fileData.to_excel(writer)
        writer.save()
        print("**********************************************")
        print("Scratch is Over,the file name is  %s" % self.writeToFilePath)
        print("**********************************************")

继承

多态

Java中的多态一般我们可以这样去进行实现。

public void a(String a){}

public void a(int a){}

public void a(String a, int b){}

但是如果我们将它引入到Python中进行使用,会出现这样的情况。

def swim(self):
    print("this is Son's swim")

def swim(self, s):
    print("this is Son's Another swim")

函数的调用会以后者为准,可以将两个函数通过调换的方式来进行验证。

那要如何才能验证他的多态特性呢?

两种方案进行论述:

  1. 向上转型,使用代码来完成解释
class Person:
    def __init__(self, name):
        self.name = name

    def drink(self):
        print("Person:", self.name)

class Father(Person):
    def __init__(self, name):
        super(Father, self).__init__(name)

    def drink(self):
        print("Father:", self.name)

class Son(Person):
    def __init__(self, name):
        super(Son, self).__init__(name)

    def drink(self):
        print("Son:", self.name)

class Mom(Person):
    def commandDrink(self, person):
        person.drink()

son = Son("Tom")
mom = Mom("Ane")
mom.commandDrink(son)

Mom类中commandDrink()函数中的proson作为一个泛化的变量,能够接受下他的子类Son就是多态的一种体现。

  1. 动态语言本身就拥有的特征,传入不同的值已经能够实现多态的特性。

Python 2 -> 3

和很多产品一样,我们会把开发分为两个分支也就是stablebeta,前者稳定,而后者有着更丰富的功能。Python 2 和 3也是同样的道理。

但是需要注意这样一条消息。

Python2从今年的4月起就已经停止维护了,与此同时NumPyRequestsTensorFlow等库在2020年也将对Python2停止更新。这就意味着将来如果你手头的项目出了什么和Python基础库相关的问题,那就不会再有官方为你兜底了,三方库的新功能你也没机会再看到了。所以升级转型为Python3势在必行。

升级Python3可以,但是对于屎山工程而言,怎么样去有效的进行升级就是一个非常严肃的问题。总不会要直接重构把,以下将给出两个比较简单的案例对比:

  1. 最常见的就是print,一个加括号一个不加。
  2. 编码方式:Python2中,我们常见的一种情况就是中文乱码等奇奇怪怪的错误,这是由于Python2本身使用的编码是ASCII导致的,这就督促我们在写到中文时,要记得加上这样一段代码作为注释。
#coding=utf-8

Python3默认以utf-8作为编码格式,绝大多数时候可以忽视这个问题的存在。

当然还有很多很多问题也是存在的。

如何比较有效的完成版本的更新迭代呢?

官方给出了四种库,为尽可能多的代码移植,以及错误检查提供了保障。

  1. 兼容性库 six 为不同版本间的兼容提供可能
  2. 自动修复程序 python-modernize Python 2-> 3的代码移植工具
  3. C扩展的兼容性标头和指南 py3c 这是项目中如果涉及了C / C++编写模块时用于兼容的工具
  4. 自动检查器 pylint --py3k 一种静态代码分析器,可以捕获诸如初始化变量,未使用的导入和重复的代码之类的错误,并且能够标记与Python3不兼容的代码。

最后给出的一份关于如何进行Python 2 -> 3的迁移指南:portingguide.readthedocs.io/en/latest/

几个需要注意的大坑

版本号兼容问题

我们经常会使用pip来进行库的安装、卸载、查询当前的python环境中已经包含的库等操作,但是其实PyCharm给我们提供了更加快捷的手段。

  1. pip方法来完成一系列操作
使用pip查看当前已装好的库安装卸载之类的操作
  1. PyCharm -> Preferences
查看当前Python环境下已有的库比pip更直观的安装库方式,并且能够进行版本选择
查看当前Python环境下已有的库比pip更直观的安装库方式,并且能够进行版本选择

TensorflowNumpy为例。

下图是使用了Version 1.19.0 Numpy以及Version 1.14.0 Tensorflow后会产生的报错,原因就是版本不兼容

而将Numpy的版本修改为Version 1.14.0时就能解决。

环境污染

这是和Java一个非常不同的地方,像Android工程中我们会通过gradle来引入各种第三方库,但是这个时候引入的库是对于当前工程而言进行使用的,如果换一个工程,就需要重新设定依赖关系。用两张图来展示我个人理解中JavaPython的区别。

对于Java,引入第三方库的方案会通过Gradle / Maven等工具来完成集成,而这些第三方框架的单独引入运行情况时都是正常的,并且关于Java的配置,一般本地会配置不同的Version,可能是Java 7Java 8Java 11等等,并不会在同一版本下重复配置。

但是在Python中,同一版本重复配置是非常常见的,不然非常容易造成环境的污染,为了证明我的说法,下面是一张配图。还是以TensorflowNumpy,因为前者依赖后者使用,所以我们删去Numpy的库。

能够发现使用Tensorflow因为缺失Numpy的库而报错。

下方是使用Tensorflow的代码,可以用于之后复现使用。

import tensorflow as tf

matrix1 = tf.constant([3., 3.])

但是还是要回到说一个环境污染到底是怎么一回事。

一个案例:当我们的Project A明确只能用Version 1.14.0 Numpy,但是同时我们把环境同样的去给了Project B去进行使用,但是Project BNumpy Version要求使用1.19.0,那这个时候如果我们升级了话,对Project A的兼容就不复存在了,这就是一个环境污染的问题。

对于这些问题,我们一般给的解法想必你也有所遇见,就是在每一个工程中都放一个新的环境。我们有两种工具去进行创建:

  1. Anaconda

2. Virtualenv,这个方案已经集成在了Pycharm

多线程和GIL锁

什么是GIL锁?其实他就是一个用于控制多线程并发的同步机制。

关于这点,举两个案例用来论证,GIL锁,什么时候是成功的,什么时候又是失败的。

from threading import Thread
import time

def counter():
    i = 0
    for _ in range(100000000):
        i += 1
    return True

def main():
    start_time = time.time()
    t = Thread(target=counter)
    t.start()
    t.join()
    end_time = time.time()
    print("total time of single is: {}".format(end_time - start_time))

def multi_main():
    thread_all = []
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=counter)
        t.start()
        thread_all.append(t)
    for i in range(2):
        thread_all[i].join()
    end_time = time.time()
    print("total time of multi is: {}".format(end_time -start_time))

if __name__ == '__main__':
    main()
    multi_main()

这个案例是通过分别使用单线程和多线程下完成加法操作所用时间的统计,如果想放大实验结果可以改变counter函数中的range内的值来影响结果。如果按照正常预估,像Java在保证数据同步的前提下,一般都是多线程执行的肯定更加迅速。但是Python呢?

能够发现速度反而是没有单线程执行来的快的,这就是GIL锁带来的副作用,在观察它们最后的数据是什么样的?100000000,没错在没有任何操作下,自觉的完成了同步操作,这就是GIL锁在搞鬼了。

从这个角度看,是不是GIL锁就一无是处了?但是换个角度思考,如果一无是处,这个线程肯定是没有它存在的必要的,也就没有人会在用它了,从存在即合理的角度看,需要找到一个合适的理由去证明,那就是IO密集型来进行论证。

我们用线程去读取一定数量的文件来完成任务。

def find_all_files(dir):
    all_files = []
    for root, dirs, files in os.walk(dir):
        for file in files:
            all_files.append(root + "/" + file)

    return all_files

def main_file_read(files):
    start_time = time.time()
    pool = threadpool.ThreadPool(1)
    requests = threadpool.makeRequests(file_read, files)
    [pool.putRequest(request) for request in requests]
    pool.wait()
    end_time = time.time()
    print("total time of single is: {}".format(end_time - start_time))

def multi_main_file_read(files):
    start_time = time.time()
    pool = threadpool.ThreadPool(20)
    requests = threadpool.makeRequests(file_read, files)
    [pool.putRequest(request) for request in requests]
    pool.wait()
    end_time = time.time()
    print("total time of multi is: {}".format(end_time - start_time))


if __name__ == '__main__':
    files = find_all_files("/Users/admin/Desktop/1")

    main_file_read(files)
    multi_main_file_read(files)

从复现的角度上看其实有一定的难度,文件内存在数量级比较大的数据,且文件数量极多时才比较容易显示出差距,正常情况下可能你看到都是这样的情况。

主要原因就是文件内容太小了,导致从线程切换上耗时占比较大,而比单线程跑慢一点,但是IO密集型时整体下来,正常情况和单线程拉不开差距。

换成一个网络IO操作的项目进行比较可以比较明显的发现差距。

  • CPU密集型任务

  • IO密集型任务

绿色: 唤醒状态;红色: 阻塞状态,等待CPU调度;白色: 等待IO状态

因为Python语言本身的特性,正常的运行只会是一个核来进行处理。如果想突破单核的限制,可以使用multiprocess库来完成。

下面给出的一个项目可以用于抓取Google官网上提供的不同版本API变化数据,并导出为表格。

项目传送门