作者:Chancel Yang, 创建:2023-07-18, 字数:39948, 已阅:498, 最后更新:2023-09-15
Hi,欢迎你看到这个页面,这里是关于Python的一点基础内容,是我在多年的实际开发中总结关于Python的一些常见用法,也许互联网上的Python的中文资料已经是多如牛毛,连官方文档也已经全面中文化了,所以本文以实际用途出发,介绍在实际开发中比较常见的入门语法与常用库
也算是对自己做一次关于Python基础的查漏补缺,也作为日常开发参考的手册
以下所涉及到的库与语法均在 Python3.7.2
实践,请自行甄别不同版本之间的差异
Python的开发环境搭建非常简单,在 Python官网 中根据需要下载对应系统架构安装即可
下载安装包,双击安装包,记得勾选 “添加到PATH” 选项,然后点击立即安装
部分发行版可以直接通过库管理安装,这里以编译安装为例
安装编译工具 Debian10
sudo apt update
sudo apt install libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev zlib1g-dev make gcc
Cent7 OS
sudo yum update
sudo yum -y install wget xz tar gcc make tk-devel sqlite-devel zlib-devel readline-devel openssl-devel curl-devel tk-devel gdbm-devel xz-devel bzip2-devel
Alpine
sudo apk update
sudo apk add wget curl vim git build-base openssl-dev zlib-dev libffi-dev make automake gcc g++ subversion python3-dev
解压并编译安装到 /usr/local/python3.7.2
中
mkdir /tmp/python3.7.2 && cd /tmp/python3.7.2
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tgz
tar -zxvf Python-3.7.2.tar.gz && cd Python-3.7.2
./configure --prefix=/usr/local/python3.7.2
sudo make
sudo make install
将Python3.7.2添加到系统环境中(Path)中
sudo ln -s /usr/local/python3.7.2/bin/python3 /usr/bin/python3.7.2
这里我写一个简易的安装任意的Python脚本,使用如下,将3.7.2替换成任意Python3版本均可
curl https://files.chancel.me/files/Bash/install-py.sh | bash -s 3.7.2
Python的主要IDE有以下几个
如果对Python项目运行配置感兴趣可以考虑使用Visual Studio Code(免费开源),如果有JetBrains全家桶或者Pycharm授权,那首选JetBrains家的IDE,省心省事,以Visual Studio Code为例(以下简称为VSC),在官网下载
Visual Studio Code下载:https://code.visualstudio.com
下载安装后运行,在界面最左侧选择Extensions,搜索 Python
如下图是我常用的Python插件
安装完成后重新运行VSC,然后创建一个文件夹并使用VSC打开该文件夹,建立一个 main.py
文件
内容如下,按F5运行
print("Hello World") # Hello World
到这里开发环境基本配置完成
配置VSC环境后,按F5可启动当前文件DEBUG调试,类似于下面的效果
python3 main.py
但是项目通常会伴随着一些启动参数,如下列代码
import argparse
parser = argparse.ArgumentParser(description='Test for argparse')
parser.add_argument('--name', '-n', help='your name', default='chancel')
parser.add_argument('--age', '-a', help='your age', default=100)
args = parser.parse_args()
print('your name is %s, your age is %s' % (args.name, args.age))
启动时带入参数
python3 main.py --name lisa --age 18
# 输出如下
your name is lisa, your age is 18
在VSC中,我们可以为上面这样的自定义参数启动创建启动配置文件,如下图
创建Python的配置文件类似如下
基于上面的代码,我们可以修改项目启动配置单如下
program
指定了程序入口,args
指定了启动的配置参数
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${workspaceRoot}/main.py",
"console": "integratedTerminal",
"args": ["--name=lisa","--age=18"],
"justMyCode": true
}
]
}
更复杂的用法可以参考官网示例结合Google搜索,下面的Flask应用启动配置文件可供参考
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"host": "0.0.0.0",
"console": "internalConsole", // 输出重定向到Console窗口
"python": "/home/chancel/codes/python/chancel-blog/.env/bin/python", // 指定了具体的Python版本
"env": {
"FLASK_APP": "${workspaceRoot}/src/flaskr",
"FLASK_ENV": "development",
"FLASK_DEBUG": "1",
"APP_CONF":"${workspaceRoot}/conf/app.conf"
},
"args": [
"run",
"--no-debugger",
"--no-reload",
"--host=0.0.0.0",
"--port=8013",
],
"jinja": true
}
]
}
Python的数据类型有 number
, String
, List
, Dictionary
, Tuple
, Set
几种
数字类型包含整数、浮点数、负数,其中整数0等价于false,1等价于true,数字类型可做运算符运算
比较常用的是前4种,常见使用如下
user_age = 100
user_name = 'chancel'
user_type = ['admin','common']
user_info = {'gender':'male'}
基础类型与其他编程语言相差不大,但语法便利性上Python要强大得多,如下是常见的语法
保留字
Python中的保留字即语法关键字,不能用作任何标识符(如变量名称),保留字列表可以从自带模块 keyword
中获取
import keyword
print(keyword.kwlist) # ['False', 'None', 'True', ..., 'yield']
mutable and immutable
Python中,string
、number
、tuple
均是不可变对象(immutable),list
与dict
、set
则是可变对象(mutable)
可变对象:重新赋值时,会重新在内存空间生成新的对象;函数传递时,修改其内容会影响到调用者的入参对象本身 不可变对象:重新赋值时,不会重新在内存空间生成新的对象;函数传递时,修改其内容不会影响到调用者的入参对象本身
示例如下
mutable_str = 'hello'
immutable_dict = {'hello': 'world'}
def change_str(my_str: str):
my_str = my_str + my_str
def change_dict(my_dict: dict):
my_dict['new'] = 'object'
print('str:%s, dict:%s' % (mutable_str, immutable_dict)) # str:hello, dict:{'hello': 'world'}
change_str(mutable_str)
change_dict(immutable_dict)
print('str:%s, dict:%s' % (mutable_str, immutable_dict)) # str:hello, dict:{'hello': 'world', 'new': 'object'}
可以看到在函数传递时,string
的值不会被改变,而dict
的值则被改变了!
切片
Python中的切片语法非常便于处理数据,如对字符串做切割
letters = 'abcdefghijklmnopqrst'
# 截取前7个字母
print(letters[0:7]) # abcdefg
# 截取后7个字母
print(letters[-7:]) # nopqrst
# 每2个元素取出一次
print(letters[0::2]) # acegikmoq
切片支持的数据类型非常广泛,包括 string
, list
, range
, tuple
, unicode
等
每一次切片会生成一个新的对象以方便操作
List
list是Python中使用非常频繁的数据类型,除了支持切片外,还包括以下特性
my_list = ['Love', 'you', 2008, True]
print('index 2 value is', my_list[2]) # index 2 value is 2008
my_list.append('Just')
print('append value is', my_list[-1]) # append value is Just
del my_list[0]
print('now index 2 value is', my_list[2]) # now index 2 value is True
可以看到List非常灵活,但除了这些意外,List对象常常用到如下方法
Dict
Dict是另外一种非常常见的数据类型,当前非常流行的Json数据类型在Python中可以很轻易的转换为Dict
Dict是经典的键值类型,Key值类型通常为String,也支持 number
, tuple
等不可变数据类型
my_dict = {'name': 'chancel'}
my_dict['age'] = 300
print('%s age is %d' % (my_dict['name'], my_dict['age'])) # chancel age is 300
del my_dict['age']
my_dict['real_age'] = 10
print('%s age is %d' % (my_dict['name'], my_dict['real_age'])) # chancel age is 10
my_dict['gender'] = 'male'
for key, value in my_dict.items():
print('dict key %s, value is %s' % (key, value))
# dict key name, value is chancel
# dict key real_age, value is 10
# dict key gender, value is male
编程语言的语法最常用语法是循环与判断,以下代码举例if
、while
、for
的用法
# 空值
str_1 = None
# Bool值
bool_1 = True
# 数字类型
number_1 = 9
# 数组语法
list_1 = []
# 判断语法
if len(list_1) < 1:
print('list_1 is empty list') # list_1 is empty list
# 循环语法
for i in range(10):
list_1.append(i)
print('list_1 value is ', list_1) #list_1 value is [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 关键字in
if number_1 in list_1:
print('number_1 contain in list_1') # number_1 contain in list_1
# 关键字not表相反
if not str_1:
print('str_1 is empty') # str_1 is empty
# and表示2个条件成真则为真
if bool_1 and not str_1:
print('bool_1 is True and str_1 is empty') # bool_1 is True and str_1 is empty
# or 表示2个条件其中1个为真的整体为真
if bool_1 or str_1:
print('bool_1 is True or str_1 is empty') # bool_1 is True or str_1 is empty
# continue表退出当前此次循环,brak表示退出整个循环
for i in range(10):
list_1.append(i)
if i == 3:
continue
if i == 5:
break
print('list_1 value is ', list_1) # list_1 value is [0, 1, 2, 3, 4, 5...]
while len(list_1) > 0:
print(list_1.pop()) # 5,4,3,2,1...
函数在Python中使用def进行定义,如下是一个简单的函数
def get_name() -> str:
return 'chancel'
其中->
非强制要求,是函数返回值的注释,在入参方面Python与其他编程语言没有区别,可携带默参,比较特殊的是可采用关键字传参,而不是顺序传参
def area(height: int = 3, width: int = 4) -> int:
return height * width
print(area()) # 12
# 通过关键字传参,对参数顺序无要求
print(area(width=5, height=6)) # 30
Python的函数支持可变参数的写法,其中*args
和**kwargs
代表可变参数和可变参数键值对,args和kwargs是约定俗成的写法,重点在于*
和**
,示例如下
def args(*args, **kwargs):
for arg in args:
print(arg)
for key, value in kwargs.items():
print('key:%s, value:%s' % (key, value))
args('1', '2', '3', name='chancel', age=18)
# 1
# 2
# 3
# key:name, value:chancel
# key:age, value:18
Python同样支持lambda表达式,示例如下
double = lambda x: x * 2
print(double(18)) # 36
在Python中,异常是一种特殊事件,一般情况下,在Python无法按照正常流程处理时就会抛出异常,如网络异常/文件被占用/端口被占用等情况,在Python中,捕捉异常可以使用try
、except
、finally
,示例代码如下
import os
file_name = os.path.join(r'C:\Windows', r'win.ini')
f = open(file_name, 'r')
try:
f.write('s')
except Exception as e:
print(e) # not writable
finally:
f.close()
print('file close') # file close
BaseException
是Python中所有异常的基类,需要自定义异常的可以继承这个基类,except
后可以接多个异常类型来代表捕捉多个可能的异常
我们使用raise
来抛出自定义错误试试
class NotStrError(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
try:
a = 100
if type(a) != str:
raise NotStrError("value is not string")
except NotStrError as e:
print(e) # value is not string
Python的标准库非常庞大,其组件范围也十分庞大,在不同的操作系统下也各不相同,所以这里着重了解以下日常常见的标准库,日常标准库详细文档可参考library - python.org
os库是日常最常用的库之一,用于与系统交互,其中最常见便是os.path
模块,例如读写文件时,我们可以借助join
来合并文件路径
import os
file_name = os.path.join(r'C:\Windows', r'win.ini')
with open(file_name, 'r') as f:
print(f.readline()) # ; for 16-bit app support
此外os.path
常见的使用还包括
更多用法可参考 os.path - 官方文档
os库除了读写文件外,日常使用中还包括获取环境变量以及大量日常与操作系统API相关的操作
sys是Python中提供与解释器相关操作的标准库,下面介绍几个常见的用法
sys.argv
是传递到Python解释器中的参数,类似于Bash语法中的$1
/$2
的作用,使用如下sys.exit(0)
用于中断程序退出,0
代表正常退出,其他参数代表异常退出sys.path
中存储模块的默认搜索路径sys.platform
获取运行平台执行python3 main.py hello
,代码与输出如下
import sys
print(sys.argv[0]) #main.py
print(sys.argv[1]) #hello
print('sys.path:%s' % sys.path) #sys.path:['/tmp/demo', ..., /usr/lib/python3.10/site-packages']
print('sys.platform:%s' % sys.platform) #sys.platform:linux
sys.exit(0)
shutil是相较于os库提供更多高阶的文件和文件集合操作方法,在Python中,shutil常用于文件的拷贝、移动、删除、解压缩等操作,以下面的例子说明shutil中几个常用方法copy
、move
、copytree
、rmtree
import shutil
import os
dir_path = '/tmp/shutil'
new_dir_path = '/tmp/new_shutil'
os.mkdir(dir_path) # 创建测试文件夹
file_path = '/tmp/shutil/hi.txt'
new_file_path = '/tmp/shutil/new_hi.txt'
with open(file_path, 'w') as f:
f.write('hi') # 向文件写入字符串'hi'
shutil.copy(file_path, new_file_path) # 若目标文件已存在,复制会覆盖
try:
shutil.move(file_path, new_file_path) # 若目标文件已存在,移动会报错
except Exception as e:
print('move file error: %s' % str(e))
shutil.copytree(dir_path, new_dir_path) # 复制文件夹
shutil.rmtree(dir_path) # 删除文件夹
shutil.make_archive(dir_path, 'zip', new_dir_path) # 压缩文件夹成shutil.zip
shutil.rmtree(new_dir_path) # 删除文件夹/tmp/shutil
shutil.unpack_archive(dir_path + '.zip', new_dir_path) # 解压shutil.zip
json是Python中用来处理Json数据最常用的库,Python中json
库中常用方法包括了dumps
、loads
import json
data = {
"name": "chancel",
"age": "100",
"interest": [
"badmintor",
"python"
],
'msg': '偷得浮生半日闲'
}
print(json.dumps(data))
# 若有中文,则需要指定ensure_ascii参数为False,否则将以ascii编码输出中文导致乱码
print(json.dumps(data, ensure_ascii=False))
with open('/tmp/json.json', 'w') as f:
f.write(json.dumps(data))
# load与loads的区别在于前者接收json文件流,后者接收json字符串
with open('/tmp/json.json', 'r') as f:
print(json.load(f))
with open('/tmp/json.json', 'r') as f:
print(json.loads(f.read()))
输出如下
{"name": "chancel", "age": "100", "interest": ["badmintor", "python"], "msg": "\u5077\u5f97\u6d6e\u751f\u534a\u65e5\u95f2"}
{"name": "chancel", "age": "100", "interest": ["badmintor", "python"], "msg": "偷得浮生半日闲"}
{'name': 'chancel', 'age': '100', 'interest': ['badmintor', 'python'], 'msg': '偷得浮生半日闲'}
{'name': 'chancel', 'age': '100', 'interest': ['badmintor', 'python'], 'msg': '偷得浮生半日闲'}
time
与datetime
模块是Python中提供日期与时间处理的模块,下面是关于时间的一些处理例子,包括获取9位/13位时间戳以及获取当前时间、转换字符串成日期对象等
import datetime
import time
# 9位的时间戳 - 1658387394.2734408
print(time.time())
# 13位的时间戳 - 1658387394273
print(int(round(time.time()*1000))) #
# time.struct_time(tm_year=2022, tm_mon=7, tm_mday=21, tm_hour=15, tm_min=9, tm_sec=54, tm_wday=3, tm_yday=202, tm_isdst=0)
print(time.localtime())
# 2022-07-21 15:09:54
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
# 2022-07-21 15:09:54.273676
print(datetime.datetime.today())
# 2022-07-21 15:09:54.273741
print(datetime.datetime.now())
# 2022-07-21 15:09:54.273764
print(datetime.datetime.fromtimestamp(time.time()))
# 2022-07-21 14:04:35
print(datetime.datetime.strptime('2022-07-21 14:04:35', '%Y-%m-%d %H:%M:%S'))
# 3
print(datetime.datetime.now().weekday())
Python中,module(模块)是值以.py
结尾的文件,创建一个user.py
,然后定义一些方法与变量,在main.py
引入试试看
user.py
name = 'chancel'
_age = 100
def print_name():
print(name)
def get_age() -> int:
return _age
class User:
pass
if __name__ == '__main__':
print('i\'m free')
main.py
import user
print(user.get_age()) # 100
user.print_name() # chancel
my_user = user.User()
print(type(my_user)) # <user.User object at 0x000002B0D0221BA8>
module也不仅限于.py
文件,事实上 .so
, .pyo
, .pyc
, .dll
都可以作为module引入
如果单独执行user.py
,则可以发现输出了“i'm free",这是因为
__name__
变量值为main__name__
变量值为文件名称借助这个特性可以非常方便的在模块中撰写测试代码
当import一个module时,Python解释器会依顺序检查
如果将module看成文件,则Package可以看成操作系统中的目录,package可以包含module与package,在大型项目中,需要用到大量的module,这个时候通常会封装成package便于管理,导入package时,会隐式的执行 __init__.py
文件
我们创建一个models
文件夹,并将user.py
移入其中,再创建一个空的__init__.py
文件
运行以下代码
from models.user import User
user = User()
print(user) # <models.user.User object at 0x0000027044B2A2B0>
可以看到,顺利的导入了User对象,如果我们希望以 from models import User
呢?那么可以在__init__.py
中这样写
from models.user import User
在实际开发时,因为项目新旧关系,使用多个Python版本的需求非常常见,简单的处理办法是区分安装路径,设置不同的Path来区分不同的版本,实际上这样做不但麻烦,而且很容易出错,而Python有非常多的版本管理程序可供选择,最具代表性的便是Anaconda
、miniconda
、pyenv
接下来以pyenv
为例,参考在Debian10上如何管理多个Python版本问题
安装编译准备(pyenv
可以在没有任何python的系统环境下安装)
sudo apt update
sudo apt -y install git gcc libgdbm-dev make patch zlib1g.dev libssl-dev
sudo apt -y install libsqlite3-dev libbz2-dev libreadline-dev build-essential python-dev python-setuptools
sudo apt -y install zlib1g-dev openssl libffi-dev
对于主流操作系统如Windows、MacOS、Debian、CentOS等pyenv
都提供自动化安装脚本
curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash
添加pyenv
的环境变量
# 如果是zsh shell,这里~/.bashrc需要修改成~/.zshrc
cat << EOF >>~/.bashrc
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
EOF
pyenv
的常见使用如下
# 所有可安装的版本列表
pyenv install -list
# 下载制定版本的Python
pyenv install 3.7.2
# 查看当前已下载的版本
pyenv versions
# 设置系统的Python版本
pyenv global 3.7.2
# 设置当前目录为某个Python版本
pyenv local 3.7.2
# 更新pyenv
pyenv update
安装pyenv
后,使用pyenv
安装3.7.2版本,并设置当前文件夹的Python环境为Python3.7.2
pyenv install 3.7.2
pyenv local 3.7.2
以后只要进入了当前文件夹(demo)则默认会启用3.7.2的Python版本
➜ pwd
/home/chancel/codes
➜ python -V
Python 2.7.17
➜ cd /home/chancel/codes/demo
➜ python -V
Python 3.7.2
设置后,只要进入这个目录,Python的版本都将会是3.7.2
总结一下pyenv
对比手工设置Python版本的优势
pyenv
虽然很方便,但如果2个项目使用相同的Python版本,而项目依赖的第三方库插件版本却又不同,这可能会带来一些问题,例如requirements.txt
通常用来指示Python项目依赖的第三方库列表与其版本号,如果一个Python版本对应多个项目,则B项目的requirements.txt
会包含着用不上的A项目的第三方库
为此,我们需要为每一个项目单独创建一个Python的虚拟环境,Python官方支持虚拟环境的创建
我们使用一个第三方库requests
来做例子,代码如下
import requests
r = requests.get('https://www.biying.com')
print('request status code is %d, request text length is %d' %
(r.status_code, len(r.text)))
在项目下创建虚拟环境并激活虚拟环境,然后使用pip
安装requests后运行main.py
PS C:\Codes\Python\Demo> python -m venv .venv
PS C:\Codes\Python\Demo> .\.venv\Scripts\Activate.ps1
(.venv) PS C:\Codes\Python\Demo> pip3 install requests
Collecting requests
...
Successfully installed certifi-2022.6.15 charset-normalizer-2.1.0 idna-3.3 requests-2.28.1 urllib3-1.26.9
(.venv) PS C:\Codes\Python\Demo> python .\main.py
request status code is 200, request text length is 2443
查看项目的.venv
文件夹,可以看到requests库已经位于其中,.venv
即是当前项目的开发环境
(.venv) PS C:\Codes\Python\Demo> ls -l .\.venv\Lib\site-packages\
Mode LastWriteTime Length Name
---- ------------- ------ -d----- 2022/7/5 11:44 certifi
d----- 2022/7/5 11:44 certifi-2022.6.15.dist-info
d----- 2022/7/5 11:44 charset_normalizer
d----- 2022/7/5 11:44 charset_normalizer-2.1.0.dist-info
d----- 2022/7/5 11:44 idna
d----- 2022/7/5 11:44 idna-3.3.dist-info
d----- 2022/7/5 11:43 pip
d----- 2022/7/5 11:43 pip-18.1.dist-info
d----- 2022/7/5 11:43 pkg_resources
d----- 2022/7/5 11:44 requests
d----- 2022/7/5 11:44 requests-2.28.1.dist-info
d----- 2022/7/5 11:43 setuptools
d----- 2022/7/5 11:43 setuptools-40.6.2.dist-info
d----- 2022/7/5 11:44 urllib3
d----- 2022/7/5 11:44 urllib3-1.26.9.dist-info
d----- 2022/7/5 11:43 __pycache__
-a---- 2022/7/5 11:43 126 easy_install.py
我们现在可以将项目依赖的第三方插件列表打印成requirements.txt
文件作为项目依赖插件列表了
(.venv) PS C:\Codes\Python\Demo> pip3 freeze > requirements.txt
大部分编程语言的日志库都是基础设施之一,因为日志记录是程序部署到生产环境正常运行极其重要的一环
日志记录对于程序来说,包括不限于以下3点作用
logging
是Python自带的日志库,拥有非常强大的功能以及极强的扩展性
logging
是Python自带的日志库,无需安装
创建一个main.py
文件,内容如下
import logging
import auxiliary_module
# 创建一个名为hello的日志模块并设置输出级别最低为DEBUG级别
logger = logging.getLogger('hello')
logger.setLevel(logging.DEBUG)
# 创建一个记录器Handler对象用于控制日志输出到文件
fh = logging.FileHandler('hello.log')
fh.setLevel(logging.DEBUG)
# 创建一个记录器Handler对象用于控制日志输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# 创建一个日志格式的对象并赋予2个记录器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 添加2个记录器到hello的日志模块中
logger.addHandler(fh)
logger.addHandler(ch)
# 测试日志输出
logger.info('我创建了一个日志输出工具')
try:
a = '10'
c = a / 10
except Exception:
logger.exception('计算c的值出错')
运行 main.py
文件,可以得到一个hello.log
文件,内容如下
2022-07-21 15:57:21,764 - hello - INFO - 我创建了一个日志输出工具
2022-07-21 15:57:21,766 - hello - ERROR - 计算c的值出错
Traceback (most recent call last):
File "/home/chancel/codes/python/hello-world/main-1.py", line 25, in <module>
c = a / 10
TypeError: unsupported operand type(s) for /: 'str' and 'int'
从上面的代码,我们可以看到logging内置了几个对象
在Python中,正则表达式的使用非常常见,Python的标准库是完全支持正则表达式的,由于Python中采用反斜杠\
来表达转义,所以在书写Python的正则表达式时,尽量使用r'string'
的写法表示不进行Python字符串的转义,此文不涉及正则表达式的基础语法,若有兴趣请参考其他教程说明,
Python对正则表示式的解析由import re
来处理,下面介绍一些常见re库的方法
re.search
: 扫描字符串并返回匹配第一个相对应的对象,如匹配不成功则返回Nonere.match
: 扫描字符串并从第一个字符开始匹配,如匹配不成功则返回Nonere.fullmatch
: 如果整个字符串完全匹配规则则返回字符串,如匹配不成功则返回Nonere.sub
: 匹配规则进行替换,如不匹配将返回原始字符串,支持指定替换次数(缺省值为0,表示全部替换)re.split
: 扫描字符串根据规则切割字符串,可指定切割次数(缺省值为0,表示全部切割)re.compile
: 正则表达式的预编译,可以用于匹配,如需多次反复使用建议提前使用此方法进行预编译提高性能上述方法的示例如下
import re
phone = '(020)3306-8081'
search_result = re.search(r'\d{3}', phone)
match_result = re.search(r'\(020\)', phone)
fullmatch_result = re.fullmatch(r'\(020\)\d+-\d+', phone)
sub_result = re.sub(r'\d{3}', '999', phone, 1)
split_result = re.split(r'\(|\)', phone)
re_compile = re.compile(r'\d{3}')
print('search area code %s' % search_result.group()) # search area code 020
print('match area code %s' % match_result.group()) # match area code (020)
print('fullmatch phone %s' % fullmatch_result.group()) # fullmatch phone (020)3306-8081
print('replace area code %s to %s' % (phone, sub_result)) # replace area code (020)3306-8081 to (999)3306-8081
print('split area code and phone %s' % split_result[1:3]) # split area code and phone ['020', '3306-8081']
print('compile pattern result %s' % re.sub(re_compile, '999', phone)) # compile pattern result (999)9996-999
线程是操作系统进行运算调度的最小单位,也称之为轻量级进程,在Python3中启动一个线程非常简单
Python中的线程会在一个单独的系统级线程中执行(比如说一个 POSIX 线程或者一个 Windows 线程)
代码如下
import datetime
import time
from threading import Thread
letters = ['a', 'b', 'c', 'd', 'e', 'f']
def PrintLetter(sleep: int = 5):
while len(letters) > 0:
print('%s Pop letter: %s' % (datetime.datetime.now().strftime('%H:%M:%S'), letters.pop()))
time.sleep(sleep)
print('Print success')
t = Thread(target=PrintLetter, args=(1, ))
t.start()
# 17:30:10 Pop letter: f
# 17:30:11 Pop letter: e
# 17:30:12 Pop letter: d
# 17:30:13 Pop letter: c
# 17:30:14 Pop letter: b
# 17:30:15 Pop letter: a
在Python中,多线程一样会面临着锁的问题,以下面代码为例
from threading import Thread
count = 0
def Add():
global count
i = 0
while i < 500000:
count += 1
i += 1
t1 = Thread(target=Add)
t1.start()
t2 = Thread(target=Add)
t2.start()
# Wait t1 and t2 completed
t1.join()
t2.join()
print('Count value : %d' % count) # Count value : 804263
理论上应该输出1000000,但实际上输出结果不定,这就是原子性操作被破坏的体现
同样我们可以加锁来解决这个情况(损失一定性能)
from threading import Thread, Lock
count = 0
lock = Lock()
def Add():
global count
i = 0
while i < 500000:
with lock:
count += 1
i += 1
t1 = Thread(target=Add)
t1.start()
t2 = Thread(target=Add)
t2.start()
# Wait t1 and t2 completed
t1.join()
t2.join()
print('Count value : %d' % count) # Count value : 1000000
协程的内容较复杂,可参考以下这篇文章,写的非常简单易懂
多进程或者多线程效率高,却仍有缺点
那么协程是什么?
协程本质上是一个线程执行多个任务,协程之间永远是数据安全的,且协程是用户级的,不由操作系统控制,所以也不需要”线程锁“
Python语言中存在GIL锁,因此Python进程中任何时刻都只有单条线程在执行,Python解释器在多线程这一点上,对IO任务进行优化工作,所以多线程执行IO任务时效率仍是正常的(相对地,CPU密集型任务采用多线程是不会有任何提升的),而协程,则是提供给开发者去更好地解决IO堵塞问题的方法(即在单条线程中处理多个IO任务)
在Python3.X早期的版本中,实现协程的方法是yield
和send
语法,在Python3.5的版本后,实现协程的方式async
和await
语法,同时提供了asyncio
的IO堵塞方式,这里重点了解async
和await
语法以及asyncio
库
//TODO...
在Python中,赋值操作符=
是直接赋值,即不拷贝对象,而是创建目标与对象之间的绑定关系
举例如下
a = 100
b = a
print('a value: %s, id: %d\nb value: %s, id: %d' % (a, id(a), b, id(b)))
a = 200
print('a value: %s, id: %d\nb value: %s, id: %d' % (a, id(a), b, id(b)))
输出如下,可以看到当修改不可变类型number
时会创建一个新的对象
a value: 100, id: 140132334490208
b value: 100, id: 140132334490208
a value: 200, id: 140132334493408
b value: 100, id: 140132334490208
将上述代码中的a
、b
改成可变对象dict
,输出则是如下,id值不发生任何变化
a value: {'letter': 'xyz'}, id: 140385347304400
b value: {'letter': 'xyz'}, id: 140385347304400
a value: {'letter': 'pro'}, id: 140385347304400
b value: {'letter': 'pro'}, id: 140385347304400
上述可得2个赋值操作的结论
那么如果我们需要创建一个可变对象的副本呢?
答案是使用浅拷贝copy.copy
来创建一个不完整副本
不可变对象使用copy.copy时与赋值操作无异
举例如下
import copy
a = {'letter': 'a'}
b = copy.copy(a)
print('a value: %s, id: %s\nb value: %s, id: %s\nc value: %s, id: %s' %
(a, id(a), b, id(b), c, id(c)))
b['letter'] = 'b'
c['letter'] = 'c'
print('a value: %s, id: %s\nb value: %s, id: %s\nc value: %s, id: %s' %
(a, id(a), b, id(b), c, id(c)))
输出如下,可以看到使用copy创建的对象id值与赋值操作创建的对象id值不一样,是一个全新的副本,修改其中一个副本并不影响其他副本的值
a value: {'letter': 'a'}, id: 140697187191976
b value: {'letter': 'a'}, id: 140697267923560
a value: {'letter': 'a'}, id: 140697187191976
b value: {'letter': 'b'}, id: 140697267923560
那为什么copy.copy
称之为浅拷贝呢?
因为当可变对象里包含可变对象时,浅复制只会创建一个最上层的可变对象的副本,而可变对象中的可变对象仍然是同一个对象
要创建一个可变对象的完整副本,我们需要使用深拷贝
举例如下
import copy
a = {'letter': ['a']}
b = copy.copy(a)
c = copy.deepcopy(a)
print('a value: %s, id: %s\nb value: %s, id: %s\nc value: %s, id: %s' %
(a, id(a), b, id(b), c, id(c)))
b['letter'].append('b')
c['letter'].append('c')
print('a value: %s, id: %s\nb value: %s, id: %s\nc value: %s, id: %s' %
(a, id(a), b, id(b), c, id(c)))
输出如下,可以看到ID值都一样,但浅拷贝的字典b修改自身字典中的数组时影响到了字典a中的数组,而深拷贝字典c则不会影响到字典a/b
a value: {'letter': ['a']}, id: 139933494184768
b value: {'letter': ['a']}, id: 139933574920808
c value: {'letter': ['a']}, id: 139933494314976
a value: {'letter': ['a', 'b']}, id: 139933494184768
b value: {'letter': ['a', 'b']}, id: 139933574920808
c value: {'letter': ['a', 'c']}, id: 139933494314976
在使用Python操作可变对象时候,需要注意赋值操作、浅拷贝、深拷贝之间的差异,避免带来错误
列表推导式(list-comprehensions)是Python中用来创建列表的一种技巧
例如给定一个包含字符串的列表,要求筛选出字符串列表中所有包含z的字符串组成新的列表
传统解决方法
list_1 = ['zenith', 'apple', 'shutdown',
'zebra', 'list', 'zip', 'shift', 'zeal']
list_2 = []
for item in list_1:
if 'z' in item:
list_2.append(item)
print(list_2) # ['zenith', 'zebra', 'zip', 'zeal']
列表推导式则相对简洁很多
list_1 = ['zenith', 'apple', 'shutdown',
'zebra', 'list', 'zip', 'shift', 'zeal']
list_2 = [item for item in list_1 if 'z' in item]
print(list_2) # ['zenith', 'zebra', 'zip', 'zeal']
其核心语法如下
newlist = [expression for item in iterable if condition == True]
列表推导式不仅可以推导列表,也可以用于创建字典,expression
允许填入任何表达式,如下
list_1 = ['zenith', 'apple', 'shutdown',
'zebra', 'list', 'zip', 'shift', 'zeal']
list_2 = {item: True for item in list_1 if 'z' in item}
print(list_2) # {'zenith': True, 'zebra': True, 'zip': True, 'zeal': True}
列表推导式语法非常强大,可以在实际开发中多多体验
迭代器是Python内实现了迭代操作的对象,迭代操作与列表相比更节省程序内存空间,在元素被获取之前不会生成
__iter__
方法用于获取一个迭代器的方法,对于基础对象 list
, tuple
, dict
都可以使用__iter__
来获取其迭代器,如下
list_1 = [1, 2, 3, 4]
iter_1 = iter(list_1)
print(next(iter_1)) # 1
print(next(iter_1)) # 2
print(next(iter_1)) # 3
print(next(iter_1)) # 4
print(next(iter_1)) # Traceback (most recent call last):...StopIteration
StopIteration
用于结束迭代器,如上述代码,在超出元素列表后,迭代器就不可用了
在Python3中,任何实现了__next__
方法和__iter__
方法的对象都可以称之为迭代器对象
实现一个自动步进+1的例子如下
class MyIterator:
def __init__(self) -> None:
self._count = 0
def __iter__(self):
return self
def __next__(self):
self._count += 1
if self._count > 10:
raise StopIteration
return self._count
if __name__ == '__main__':
mi = MyIterator()
mi_iter = iter(mi)
while True:
print(next(mi_iter))
从上述可知,用__next__
和__iter__
可以为类配置迭代器的代码
那么如果是函数需要使用迭代器呢?这个时候就可以使用生成器了
def generator(n:int):
n += 1
yield n
if __name__ == '__main__':
number = 0
while number < 10:
number = next(generator(number))
print(number) # 1...10
生成器可以理解为延迟操作
,跟Golang中的defer非常相似,在计算时才获得具体的值
上述的例子或许不太明显,结合列表推导式就可以看出其用法
def generator(n:int):
n += 1
yield n
if __name__ == '__main__':
generators = [generator(i) for i in range(10)]
for g in generators:
print(next(g)) # 1...10
代码中的generators
在遍历之前并不会计算其中真正的值,这就是使用生成器的好处
生成器相对而言比较灵活,而且仅能遍历一次,限制性较大
假设需要实现一个累加算法,以面向对象的实现方法如下
class Sum:
def __init__(self) -> None:
self._numbers = []
def add(self, number: int) -> int:
self._numbers.append(number)
sum = 0
for number in self._numbers:
sum += number
return sum
if __name__ == '__main__':
s = Sum()
print(s.add(1)) # 1
print(s.add(5)) # 6
print(s.add(10)) # 16
sum
类的numbers
成员是希望对外界隐藏的,将数据封装在类中不被访问是非常常见的操作
如果希望采用函数来实现类似的功能该如何隐藏numbers
的数据呢?
def add():
numbers = []
def func(number: int = 0):
numbers.append(number)
sum = 0
for number in numbers:
sum += number
return sum
return func
if __name__ == '__main__':
s = add()
print(s(1)) # 1
print(s(5)) # 6
print(s(10)) # 16
以上代码的实现便是闭包
,具备了嵌套函数
且嵌套函数
引用了封闭函数的变量
以上代码的实现并不寻常,因为在add()
结束后,numbers
变量的生命周期也应该结束了,然而numbers
在嵌套函数中依然可以被引用
什么情况下使用闭包?
class
(oop)在Python中,装饰器也是大量使用闭包的体现
在Python中,所有内容都是对象,包括函数也是一等函数
,函数可以作为参数值传递给函数,也可以作为返回值返回,在Python中,任何实现了__call__
方法的对象都是可调用函数
Python中的Object是对数据的抽象
在Python中,装饰器是也一种函数,本质上是接受一个函数,添加一些额外的功能,然后返回给调用者,实现如下
def sum(x, y):
return x+y
def verify(func):
def _(*args):
for arg in args:
if type(arg) != int:
return -1
return func(*args)
return _
if __name__ == '__main__':
s = verify(sum)
print(s(10, 100)) # 110
print(s(10, 'a')) # -1
函数verify
本质上就是个装饰器,可以为所有的sum
方法调用进行参数检查,每次调用sum都需要显式调用verify
是比较麻烦的,Python提供@
进行函数装饰
标准装饰器写法如下
def verify(func):
def _(*args):
for arg in args:
if type(arg) != int:
return -1
return func(*args)
return _
@verify
def sum(x, y):
return x+y
if __name__ == '__main__':
print(sum(10, 100)) # 110
print(sum(10, 'a')) # -1
使用@
对函数进行装饰,以较低的代码入侵实现对函数的功能修改
CPython解释器采用一种叫做“GIL全局解释锁”的互斥锁,全称是Global Interpreter Lock,用于防止多线程并发执行机器码
关于GIL在这里不展开,其结论有2点
举例如下
import time
from threading import Thread
def Add(n: int):
count = 1
for i in range(2, n):
count = count + i
print('Count: %s' % count)
start = time.time()
Add(100000000) # Count: 4999999950000000
Add(100000000) # Count: 4999999950000000
print('Run time: %0.3f\n' % (time.time() - start)) # Run time: 15.024
start = time.time()
t1 = Thread(target=Add, args=(100000000, )) # Count: 4999999950000000
t1.start()
t2 = Thread(target=Add, args=(100000000, )) # Count: 4999999950000000
t2.start()
# Wait t1 and t2 completed
t1.join()
t2.join()
print('Run time of thread: %0.3f' % (time.time() - start)) 3 Run time of thread: 15.406
可以看到,对于计算100000000以内的累加算法,双线程的速度甚至慢于单线程的,这就是CPU计算密集型为何不能采用线程处理的原因,但事实上这跟Python解释器有关,对于JPython
而言是没有这个问题的
更多资料可以参考Python官网文档
requests是Python中一个优雅的同步请求网络工具库
reqeusts非Python3自带库,使用pip安装如下
pip3 install requests
GET请求示例
import requests
r = requests.get('https://api.chancel.me/rest/api/v1/ip')
print(r.status_code) # 200
print(r.json()) # {'status': 1, 'msg': 'Query success', 'data': {'ip': '14.145.139.148'}}
POST请求示例
import requests
r = requests.post('https://api.chancel.me/rest/api/v1/telegram')
print(r.status_code) # 200
print(r.json()) # {'status': 0, 'msg': 'Not Found', 'data': None}
在发起模拟请求时,通常需要在HTTP的Header中添加自定义头
如下代码,在POST请求的Header中添加一个TOKEN令牌
import requests
headers = {
'TOKEN': 'JUST DO IT'
}
r = requests.post('https://api.chancel.me/rest/api/v1/telegram',headers=headers)
print(r.status_code) # 200
print(r.json()) # {'status': 0, 'msg': 'Not Found', 'data': None}
除了headers以外,通常网站认证还会采用cookies作为用户认证
import requests
data = {'id': 91}
cookies = {'IDTAG': '17ab96bd8ffbe8ca58a78657a918558'}
r = requests.post('https://www.chancel.me/manage/favorites', json=data, cookies=cookies)
print(r.status_code) # 200
print(r.json()) # {'status': 0, 'msg': 'Not Login!', 'data': None}
下面的代码展示2种不同的POST数据请求
注意,有时根据Server端的区别,需要在Header中修改 Content-Type
类型才可以正常请求
import requests
# 该接口只能处理JSON数据,请求成功
json_data = {'apiKey': '382749223:asdu23ASFDCD98hdnn1csd93DASy',
'chatId': '442447203', 'msgText': 'hi'}
r = requests.post(
'https://api.chancel.me/rest/api/v1/telegram', json=json_data)
print(r.status_code) # 200
print(r.json()) # {'status': 1, 'send success': 'data': {'chat': {...}}
# 该接口只能处理JSON数据,请求失败
form_data = {'apiKey': '382749223:asdu23ASFDCD98hdnn1csd93DASy',
'chatId': '442447203', 'msgText': 'hi'}
r = requests.post(
'https://api.chancel.me/rest/api/v1/telegram', data=form_data)
print(r.status_code) # 200
print(r.json()) # {'status': 0, 'msg': 'Not Found', 'data': None}
使用 r = requests.get()
的写法很方便,不过涉及到cookies认证时则不太方便
处理方案常见的是存储本地Cookies,然后在每一次请求时手动加载cookies,再进行请求
这样做非常麻烦,那么能不能像浏览器一样自动处理cookies?答案是可以的
使用 requests.session
就可以自动化处理cookies
import requests
# 以下2次请求Cookies值均发生了变化,说明服务端认为这是2次全新的请求
r = requests.get('https://www.chancel.me')
print(r.cookies) # <RequestsCookieJar[<Cookie IDTAG=b6db65da-01bc-11ed-94db-00163ec8adc0 for www.chancel.me/>]>
r = requests.get('https://www.chancel.me')
print(r.cookies) # <RequestsCookieJar[<Cookie IDTAG=c0e7b190-01bc-11ed-8c4c-00163ec8adc0 for www.chancel.me/>]>
# 以下2次请求的Cookies值不变,对服务端来说相当于是同一个用户请求2次
s = requests.session()
r = s.get('https://www.chancel.me')
print(s.cookies) # <RequestsCookieJar[<Cookie IDTAG=0c36c265-01bd-11ed-b6ac-00163ec8adc0 for www.chancel.me/>]>
r = s.get('https://www.chancel.me')
print(s.cookies) # <RequestsCookieJar[<Cookie IDTAG=0c36c265-01bd-11ed-b6ac-00163ec8adc0 for www.chancel.me/>]>
Python非常适合写小工具,小工具在涉及到数据存储的通常可以选择sqlite
或者文件存储
但实现文件读写存储要考虑诸多细节,例如有效期、写入冲突、序列化对象等问题
diskcache
为处理这种小型数据存储提供非常便利的选择
仓库地址:https://github.com/grantjenks/python-diskcache 参考手册:https://grantjenks.com/docs/diskcache/tutorial.html
使用pip
安装如下
pip install diskcache
使用非常简单,示例如下
from diskcache import Cache
class Man:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
cache = Cache('cache')
cache.set('admin', Man(name='chancel', age=80)) # or add() but key must not already
user = cache.get('admin')
print('admin name is %s, age is %d' % (user.name, user.age)) # admin name is chancel, age is 80
使用diskcache
可以快速的将对象数据写入本地磁盘,在下次启动时读取数据
设置数据时候可以携带参数expire
限制数据的有效期,单位是秒,代码如下
import time
from diskcache import Cache
class Man:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
cache = Cache('cache')
cache.set('admin', Man(name='chancel', age=80),expire=3)
user = cache.get('admin')
print('admin name is %s, age is %d' % (user.name, user.age)) # admin name is chancel, age is 80
time.sleep(5)
user = cache.get('admin')
print('admin name is %s, age is %d' % (user.name, user.age)) # exception 'NoneType' object has no attribute 'name'
程序如果写的是客户端,往往运行环境就无法考虑源码运行了,以二进制形式分发软件比较常见,而Python程序在这一块相对弱势,并且也不支持交叉编译,打包成二进制文件需要借助第三方库pyinstaller
来完成,好在pyinstaller
打包大部分Python程序还是比较顺利的
为了方便说明,假设文件目录如下
├── requirements.txt
└── src
├── main.py
└── utils.py
使用pip
安装如下
pip3 install pyinstaller
如果你的Python环境是使用pyenv
进行管理的,可能需要在安装上添加共享库打包选项,否则使用pyinstaller
打包时可能会遇到so
错误提示,pyenv
安装Python时添加共享库命令如下
env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.7.2
程序入口是main.py
文件,其中引用了utils.py
模块,则打包方法如下
pyinstaller -F src/main.py
-F
参数表示打包压缩成一个文件,与之对应的是默参-D
,表示打包程序,打包后目录如下
├── build
│ └── main
│ ├── Analysis-00.toc
│ ├── base_library.zip
│ ├── EXE-00.toc
│ ├── main.pkg
│ ├── PKG-00.toc
│ ├── PYZ-00.pyz
│ ├── PYZ-00.toc
│ ├── warn-main.txt
│ └── xref-main.html
├── dist
│ └── main.exe
├── main.spec
├── requirements.txt
├── logging.yaml
└── src
└── main.py
可以看到多了build
和dist
文件夹以及一个main.spec
文件,在dist
目录下可以看到一个main.exe
二进制文件,这就是我们打包后的二进制文件,他可以在不安装任何Python环境下直接运行
如果不使用-F
参数,则可以额外看到一个dist/main/
文件夹,里面存放了一些.dll
和.so
文件用来支持exe运行
-F
参数本质上只是打包.dll
和.so
文件到exe中,在运行时释放出来
main.spec
里面则是存放了打包时的选项,下一次打包我们可以直接使用这个配置文件进行打包,也可以通过更改这个配置文件来实现更多的打包效果,使用main.spec
打包方法如下
pyinstaller ./main.spec
main.spec
的配置具体可以参考PyInstaller Manual,无论需要什么样的配置,都可以查阅官方手册,然后手动修改main.spec
文件来实现打包二进制文件的效果
对于每一个需要打包的程序来说,引用资源文件是常见的,如conf
、db
等文件等,这里以项目中的logging.yaml
日志配置文件为例,看看如何引入一个普通资源文件,在main.py
代码中是这样引用logging.yaml
文件的
with open('logging.yaml', 'r', encoding='utf-8') as f:
logging_config = yaml.load(f, Loader=yaml.FullLoader)
config.dictConfig(logging_config)
logger = logging.getLogger('main.common')
以上写法在开发阶段没有问题,只要保证运行目录中有logging.yaml
文件即可,打包之后由于-F
参数影响,实际运行目录中是没有logging.yaml
文件的,我们想要实现的效果是将logging.yaml
文件也打包到exe中,并且将代码中的路径也改成释放.so
文件的目录中去,最终实现一个exe执行程序的效果,代码方面改动如下
def get_resource(relative_path: str):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
with open(get_resource('logging.yaml'), 'r', encoding='utf-8') as f:
logging_config = yaml.load(f, Loader=yaml.FullLoader)
config.dictConfig(logging_config)
logger = logging.getLogger('main.common')
添加了get_resource
方法来获取资源文件,然后修改打包配置文件main.spec
,将logging.yaml
文件一并打包到exe中
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['src/main.py'],
pathex=['logging.yaml'],
binaries=[],
datas=[('logging.yaml','.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='main.exe',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )
这样,打包出来的exe中就含有资源文件logging.yaml
了,我们可以将一个exe单独交给客户而无需考虑如何在客户的操作系统中安装Python
分发二进制文件,通常需要防止反编译,这里展开讲的话会比较复杂,常见的手段从低到高如下
pyinstaller
打包程序添加加密选项,这种加密能基本杜绝pyc文件反编译,效果较好,但仍然是存在pyc文件,所以反编译依然是可能的Nuitka
工具转化打包后的Python程序为C程序,进一步提高反编译的难度在pytinstaller
中,修改main.spec
的block_cipher
参数添加aes密钥即可
# -*- mode: python ; coding: utf-8 -*-
block_cipher = pyi_crypto.PyiBlockCipher(key='18sh@t73bkasd932')
a = Analysis(['src/main.py'],
pathex=['logging.yaml'],
binaries=[],
datas=[('logging.yaml','.')],
hiddenimports=[],
hookspath=[],
...
此时可以试试解压exe再反编译pyc文件查看代码内容是否已经被加密