aniaan / aniaan.github.io Goto Github PK
View Code? Open in Web Editor NEWMy Blog
My Blog
最近工作上有个业务场景,简单描述就是用户在前端界面输入一条查询SQL,然后后端返回预览数据以及数据的schema,注意这里的schema要的是精确的schema,也就是说精确的schema信息必须由数据库层面给到我们,我们自己是确定不了的,我们只能拿到表结构基础字段的类型,但是用户填入的SQL是复杂多变的,查询出来的每列值类型我们不知道,但是数据库是知道的。
这个信息怎么拿,现在的python数据库驱动基本都实现PEP 249定义的规范,从规范中我们可以看到Cursor这个类型,这个类有一个属性 description,这个属性是由7个元素组成的元组,组成如下:
(name, typecode, display_size, internal_size, precision, scale, null_ok)
我们可以从这个属性中,拿到我们列类型,每个数据库驱动都维护了自己的typecode,我们要做的就是把typecode对应到我们应用中定义的类型
思路就是这样的一个思路,但是在具体实现的时候,还是踩了不少坑,目前踩的坑基本都来自各大驱动对于NUMBER类型的处理,下面依次介绍
这里我采用的驱动为pymysql,其他驱动也基本都是类似问题
INT类型的字段返回的typecode为3,这里pymysql会将他映射成LONG类型,LONG类型返回的typecode为252,在pymysql中会将他映射为BOLB类型,是不是有点神奇,一开始我以为是pymysql映射出错了,想着去MySQL官方那找点线索,然后我发现MySQL官方自己也维护着一个python驱动,mysql-connector-python,翻了翻这个库的源码,发现他的映射和pymysql是一致的,emmmm,MySQL不讲武德,一个类型映射都映射成这样。那现在我们要解决这个问题,把INT和LONG类型映射正确,通过测试发现,INT类型的typecode应该为3,所以我们只需要typecode=3的类型映射到INT就行了。
接下来就是要解决LONG类型的问题,MySQL把long和BLOB类型的typecode都定义为252,通过观察发现可以通过precision这个值来进行区分,precision=67108860的type应为LONG
from typing import Tuple
def convert_type(description: Tuple) -> str:
typecode = description[1]
if typecode == 3:
return "INT"
elif typecode == 252:
precision = description[4]
if precision == 67108860:
return "LONG"
else:
return "STRING"
else:
return "user custom mapping"
当我们确定一个类型为DECIMAL之后,还想拿到它具体的precision和scale值,从上面的元组组成我们可以看到是有precision和scale的定义的,直接取值可以了,但是事情没有这么简单,拿出来的值和我数据库中定义的有偏差,于是我定义了很多DECIMAL类型,一番观察之后,发现了规律
from typing import Tuple
def determine_decimal_type(description: Tuple) -> str:
precision, scale = description[4:6]
if scale == 0:
return f"DECIMAL({precision - 1},{scale})"
else:
return f"DECIMAL({precision - 2},{scale})"
我只想说,MySQL不讲武德
Oracle这里采用的是官方驱动 cx_Oracle
Oracle将所有的数字类型全部转换成了NUMBER,也就说从typecode上,我们只能知道他是一个数字类型,但是不能区分出INT,FLOAT这些,这就有点难受,查了一些资料,基本上都是说从precision, scale这两个字段信息下手,这里插入一个 链接,里面有不少解决方案,这里我因为业务需要和Spark确定出来的类型一致,所以要将NUMBER推断成了DECIMAL,然后不同的类型使用不同的精度。
from typing import Tuple
def determine_number_type(description: Tuple) -> str:
precision, scale = description[4:6]
if scale == 0:
return f"DECIMAL({precision},{scale})"
elif scale == -127:
return f"DECIMAL(38,10)"
else:
return f"DECIMAL({precision},{scale})"
SQL Server这里我采用的python驱动为 pymssql,这个库也是让人一言难尽,给我的感觉是社区基本已经不维护了,从18年到20年也就更新了一次,提的issue和pr基本没有人理会,现在说一下在使用pymssql中遇到一些问题。
在pymssql中,数字类型只细化了NUMBER和DECIMAL类型,但是这对于我来说,不够,我想要更细化的数字类型,于是我改了他的代码链接
pymssql中Cursor.description这个元组里面只填充了name和typecode,从规范来说,他这是符合规范,但是对于我来说,不够,只能通过改代码的方式,来拿到具体的precision, scale的值链接
社区目前给我的感觉基本处于不维护状态,鉴于此,我针对我打的这两个补丁,在pypi上重新发了一个包,可以通过 pip install pymssql-plus
来安装使用,代码仓库地址
简单的说,就是为每个应用提供了独立的lib,应用之间独享自己的lib
官方解释:
The venv module provides support for creating lightweight “virtual environments” with their own site directories, optionally isolated from system site directories. Each virtual environment has its own Python binary (which matches the version of the binary that was used to create this environment) and can have its own independent set of installed Python packages in its site directories.
python -m venv <name> # 创建
source <name>/bin/activate # 激活虚拟环境
这里的name一般来说大家都是命名为".venv"或者"venv", 创建虚拟环境的方式不只这一种,但是在python3.6之后,官方只推荐大家这么做,具体原因可以看一下官方解释
现在已经创建好虚拟环境,可以进去虚拟环境的目录看一下究竟
cd <venv_name>
tree -L 2 # 查看目录结构
.
├── bin
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── easy_install
│ ├── easy_install-3.6
│ ├── pip
│ ├── pip3
│ ├── pip3.6
│ ├── python -> /Users/beanan/.asdf/installs/python/3.6.8/bin/python
│ └── python3 -> python
├── include
├── lib
│ └── python3.6
└── pyvenv.cfg
4 directories, 11 files
上面我们进行了一步激活虚拟的环境的操作,可以看看这个activate文件里面做了什么样的事情
VIRTUAL_ENV="/Users/beanan/py_workspace/study/venv"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
这段命令也很简单,就是将$name/bin目录添加到了PATH这个环境变量上,并放到首位,这样的话,只要我们在激活的虚拟环境中,我们输入 "python" 这样的命令,那么python这个命令位置肯定是来自我们虚拟环境目录的python执行文件。
which python
/<project_path>/<venv_name>/bin/python
从上面venv目录结构可以看到venv下的python命令是我们系统全局python解释器的一个软链接,这样侧面就说明一件事情,虚拟环境只是提供了独立的lib包,我们用的python解释器还是用的我们系统中的。
这里有个知识点需要了解下,在python中解释器是如何寻找包的,答案是sys.path
,python根据sys.path
里面的路径来依次找包,现在我们打印一下全局环境和虚拟环境中sys.path的值
全局环境
python -c "import sys;print(sys.path)"
output:
[
'',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python36.zip',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python3.6',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python3.6/lib-dynload',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python3.6/site-packages'
]
虚拟环境
python -c "import sys;print(sys.path)"
output:
[
'',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python36.zip',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python3.6',
'/Users/beanan/.asdf/installs/python/3.6.8/lib/python3.6/lib-dynload',
'/Users/beanan/py_workspace/study/venv/lib/python3.6/site-packages'
]
通过对比,我们可以发现,两者的差异在于列表的最后一位,全局环境使用的是全局的site-package,虚拟环境中使用的venv site-package,那么python是怎么做到的,答案在python的site.py文件里面,简而言之,当python解释器启动时候,解释器是知道自己当前启动的工作目录是在哪个位置,他会找父目录下有没有"pyenv.cfg"文件,如果有,添加虚拟环境的site-package, 否则添加global site-package, 具体逻辑可以看下site.py文件中的内容,site.py文件在python解释器加载的时候就会执行,改变我们的sys.path,同时sys.prefix也会改变。
在研究flink中使用的类加载机制之前,需要先对jvm中的双亲委派模型有个大概了解。
在jvm中,想要加载一个类,加载的过程中涉及到的classloader并不只有一个,而是由使用的classloader以及它的parent classloader协作完成,当一个classloader收到一个加载类的请求之后,它会先去判断这个类是不是已经加载过了,如果没有,则提交给他的parent classloader来进行加载,同理,parent classloader也会做相同的处理,先提交给它的parent classloader来加载,这样的过程会一直持续到classloader这条链路的最顶端,只有说最顶端的parent classloader加载不了,才会逐级下降,让child classloader来判断是否能加载,加载不了继续下降让下一级的classloader加载,这样一个协作的过程,叫双亲委派模型。
一个classloader中有个关键属性,叫classpath,顾名思义,这代表着这个classloader能加载的类的范围,如果加载的类不在这个classloader的这个范围内,那肯定加载不了的。
jvm启动的时候已经帮我们提供了好了3个classloader,
bootstrap class loader <- extension class loader <- application class loader
上述就是jvm中使用到的双亲委派模型,为什么要用这种机制,而不是直接用一个classloader来完成,这个原因事后也比较容易想清楚
了解完jvm的类加载机制之后,来看一下flink中使用到的类加载机制,在flink-conf.yaml配置文件中,有这么一行配置classloader.resolve-order: child-first
, 这个参数有两个可选值,child-first
和parent-first
,默认值为child-first
, 这两个值主要是针对flink session模式来说的,下面依次介绍下不同值的含义。
Java default
,也就是说parent-first其实就是jvm中的双亲委派模型下面具体说一下两者在flink中使用的适用场景:
在session模式中,我们可以在在集群中放置我们的多个usercode jar包,然后可以根据需要去run指定的jar包,这里先大概介绍一下flink的run api的大概处理流程。
收到run请求后,建立一个ChildFirstClassLoader,这个classloader的classpath为将要运行的这个jar包的地址,他的parent classloader为flink jobmangager这个jvm的application classloader, application classloader主要的classpath就是为$FLINK_HOME/lib
目录下的jar包。
将当前threade的context classloader设置为ChildFirstClassLoader,然后对jar包的主类进行反射调用。在反射这个jar包的过程中,涉及到了类加载,那么ChildFirstClassLoader重写了默认的loadClass方法,会先去加载他自己的jar里面的类,加载不到才会让parent classloader加载。当然在这个过程中,也会有一些特殊处理,如果要加载的类是flink和hadoop相关类,那么始终会让parent classloader来进行加载。
优点
资源隔离,如果让flink parent classloader来统一加载这些jar的包,那么势必是会出现class冲突的问题,使用child-first是资源隔离的,每次只会去加载自己jar包中依赖,而不会用到别的jar的依赖。这里有个小知识点,class被加载到内存中之后,会以Class对象的形式存在jvm方法区中,Class对象在方法区的唯一性是由classloader + Class类名决定的,也就是说,不同的classloader加载同一个类,在方法区中,他们是不同的Class, 那么在run请求中,每次都会建立一个新的ChildFirstClassLoader,由它来加载自己的类, 就保证了,在每次run的过程中,使用到的类都是自己的,不用担心会与其他jar发生冲突。
适用场景
如果在session模式下,你有多个不同的jar包,那么采用这种方式是OK。
和child-first处理流程差不多,唯一的区别在于每次建立的不是ChildFirstClassLoader,而是ParentFirstClassloader,ParentFirstClassloader的parent classloader也是flink application classloader,只不过ParentFirstClassloader没有去重写默认的loadClass方法,还是用的双亲委派。
适用场景
在session模式下,如果你只有一个jar包,那么就用parent-first吧,同时把你的这个jar放到flink的lib目录下,为什么要用这个,child-first有个开销是,每次run都会去加载一次用到的类,如果有多个jar包,这样是不可避免的,毕竟要资源隔离,但是你只有一个jar包,每次run都重新加载一次,有点占用资源了,如果你invoke jar的速度是远快于jvm回收方法区的速度的,也就是说,你同时并发一上来,同时重复加载类,很容易直接该方法区out of memory,
用parent-first还有一个需要注意的点是,我们编译jar用的某个外部依赖版本可能会和flink本身用的这个外部依赖版本不一致,导致run的时候报错,解决这个问题,就是二者的依赖版本同步一下就好了,都用同一个。
上文讲到,在反射jar包之前,需要将thread context classloader设置为ChildFirstClassLoader,不设置会有什么后果,那当然是找不到对应jar包的类了,因为flink application classloader的classpath里面是没有我们自己的jar包的,设置了之后,反射就能成功,反推一下,反射用的classloader是thread context classloader,,我当时看这段代码的时候,也是很蒙,后续反过来一想,就明白了,代码如下
try {
Thread.currentThread().setContextClassLoader(userCodeClassLoader);
LOG.info(
"Starting program (detached: {})",
!configuration.getBoolean(DeploymentOptions.ATTACHED));
ContextEnvironment.setAsContext(
executorServiceLoader,
configuration,
userCodeClassLoader,
enforceSingleJobExecution,
suppressSysout);
StreamContextEnvironment.setAsContext(
executorServiceLoader,
configuration,
userCodeClassLoader,
enforceSingleJobExecution,
suppressSysout);
try {
// invoke jar
program.invokeInteractiveModeForExecution();
} finally {
ContextEnvironment.unsetAsContext();
StreamContextEnvironment.unsetAsContext();
}
} finally {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
python是一门强类型的的动态语言,在运行的时候值的类型是确定的,但是之前很长一段时间,我们在写python代码的时候,尤其是接手别人代码的时候,大部分情况下我们是不知道这个值到底是什么类型,是int还是str,如果是dict,那么这个dict的key是什么类型,key的参数名称是什么,value的类型是什么,这些具体的信息随着人员的更替,到最后就全靠摸索,给开发带来了很大的困扰。
如何让python代码更加可读,这个不同的人可能有不同的理解,也可能会有不同的方法,比如写文档,多写注释此类的,这个看团队的情况来决定吧,只要是大家能认可且能落实执行下去的,就是好方法。
下面我会介绍我目前我在使用的方法,也是目前开源社区python项目大家都在用的方式,Type Hints + Mypy
Type Hints俗称类型注释,是python3.5引入进来的语法,使用方式也比较简单
a: int = 1
b: str = "b"
def demo(c: int, d: str):
pass
可以看到只是名称后面加name: {type}
def demo(c: int, d: str) -> str:
return "demo"
可以看到只需要加一个箭头后面跟上类型即可
1. IDE或者编辑器正确的自动提示
最早写Java的时候,用的IDEA,虽然说Java本身写起来可能多少有点啰嗦,但是依赖于IDE的自动提示,基本不用自己默写API什么的,很方便。
一开始写python的时候,比较痛苦,因为你的类型是在运行时才确定,对于pycharm或者vscode来说,他们就是在推测你的类型是什么,然后给到你提示,但是很多时候,推测不准,就需要自己默写API了,很痛苦。
加上了Type Hints了之后,分析器就有足够的信息来确定这个类型,从而给到我们正确的提示。
2. 代码即文档
使用了Type Hints之后,你的变量,你的方法参数都是有具体类型的,通过合理的参数名称和类型注释,可以让人很清晰的知道这些参数的用法。
3. 利用Mypy来做类型检查
Mypy是一个静态类型检查器,可以帮助你发现你程序中因为类型错误可能导致的bug,我们可以先看一段程序
def compute(a):
if a == 0:
return None
return 1
result = 0
result += compute(a=1)
result += compute(a=0)
这是一段很简单的程序,试着运行一下就会报错TypeError: unsupported operand type(s) for +=: 'int' and 'NoneType'
,原因很简单,int不能和None相加,可以想一下,这种错误,在我们接手的项目中是不是比较容易出现,当然有方法避免,比如写注释,告诉调用者可能的返回值是什么类型的,但是现在我们可以用更优雅的方式来解决这个问题。
# a.py
from typing import Optional
def compute(a: int) -> Optional[int]:
if a == 0:
return None
return 1
result = 0
result += compute(a=1)
result += compute(a=0)
我们声明了这个函数的返回值为Optional[int],这个Optional[int]代表着你这个值可能为int类型,但是也可能为None,
在运行该代码之前,我们拿mypy跑一下这个文件 mypy a.py
,我们会收到以下错误
a.py:12: error: Unsupported operand types for + ("int" and "None")
a.py:12: note: Right operand is of type "Optional[int]"
a.py:13: error: Unsupported operand types for + ("int" and "None")
a.py:13: note: Right operand is of type "Optional[int]"
Found 2 errors in 1 file (checked 1 source file)
mypy可以和vscode进行集成,不需要进行run mypy的,在你写代码的时候,就能收到该提示,同时pycharm自带的类型检查工具也能发现该问题。同时我们可以在mypy加到CI中,做类型检查。
大概意思就是说Optional[int]不能和int相加,说明我们的程序存在出bug的风险,那么这个时候,我们应该进行修复,如下
from typing import Optional
def compute(a: int) -> Optional[int]:
if a == 0:
return None
return 1
result = 0
r1 = compute(a=0)
if r1 is not None:
result += r1
可以根据我们的业务需求来对该None做出处理,这样就能避免运行时候因为类型不匹配导致的问题,
python的typing模块中提供了我们常用的类型,下面介绍一下该模块中我常用的
int, str, list, dict内建类型,不需要从typing中导入
Optional
这个类型很重要,推荐大家使用
比如List[int], List[str], List[List[int]]
比如 Dict[str, int], Dict[str, List[int]]
这是一个让人很惊喜的类型,也可以称之为异构字典,在python3.8之后可以在typing包中找到,python3.8之前的版本可以pip install typing-extensions
,然后从typing-extensions导入使用,简单介绍一下为什么我们需要这个类型,没有他之前,{"a": 1, "b": "str"}
只能用Dict[str, Any]来声明,同时这个参数给到下游之后,下游方法也不知道这个字典里面都有哪些key,所以只能这样写data.get(key)
,然后得到一个Optional类型,再去做特殊处理,比较麻烦,不够清晰,有了TypedDict之后,我们就可以这样做了
from typing import TypedDict
class Data(TypedDict):
a: int
b: str
data: Data = {"a": 1, "b": "str"}
TypedDict本质上还是一个Dict,只不过允许我们更加细粒度去声明这个Dict里面的情况。
本文只是简单介绍了一个python的Type Hints,具体更多用法需要自己下去多研究研究了,核心目的其实只有一个,让我们的代码的可读性和健壮性更好
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.