menu Chancel's blog
rss_feed
Chancel's blog
秋雨一何碧,山色倚晴空。

Python3使用指南

作者:Chancel Yang, 时间:2022 Jul 21, 阅读:108

Hi,本文是我在实际工作中总结的关于Python的博文

互联网上的Python的中文资料多如牛毛,连官方文档的中文也非常齐全了

本文以实际用途出发,记录Python中常用的语法与第三方库

既做一次知识的查漏补缺,也作为日常参考手册的使用

注意,以下所涉及到的库与语法均在 Python3.7.2 实践,请甄别不同版本之间的差异

1. 入门

1.1. 安装配置

1.1.1. 开发环境

Python的开发环境搭建非常简单,在 Python官网 中根据需要下载对应系统架构安装即可

64位Windows

下载安装包,双击安装包,记得勾选 “添加到PATH” 选项,然后点击立即安装

  • https://www.python.org/ftp/python/3.7.2/python-3.7.2-amd64.exe

2022-07-04-12-51-23.png

64位Linux

部分发行版可以直接通过库管理安装,这里以编译安装为例

安装编译工具
Debian10

sudo apt install libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev zlib1g-dev make gcc

Cent7 OS

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

解压并编译安装到 /usr/local/python3.7.2

mkdir /tmp/python3 && cd /tmp/python3
wget https://www.python.org/ftp/python/3.7.1/Python-3.7.1.tgz
tar -zxvf Python-3.7.1.tar.gz && cd Python-3.7.1
./configure --prefix=/usr/local/python3.7
sudo make
sudo make install

需要请添加到系统环境中(Path)中

sudo ln -s /usr/local/python3.7/bin/python3 /usr/bin/python3.7

1.1.2. IDE选择

Python的主要IDE有以下几个

  • PyCharm
  • Visual Studio
  • Jupyter
  • Spyder
  • PyDev

如果对Python项目运行配置感兴趣可以考虑使用Visual Studio Code(免费开源)

如果有JetBrains全家桶或者Pycharm授权,那首选JetBrains家的IDE,省心省事

以Visual Studio Code为例(以下简称为VSC),在官网下载

  • https://code.visualstudio.com

下载安装后运行,在界面最左侧选择Extensions,搜索 Python

如下图是我常用的Python插件

2022-07-04-12-20-19.png

安装完成后重新运行VSC,然后创建一个文件夹并使用VSC打开该文件夹,建立一个 main.py 文件

内容如下,按F5运行

print("Hello World") # Hello World

到这里开发环境基本配置完成

1.1.3. Visual Studio Code Python Project

配置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指定了启动的配置参数

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "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应用启动配置文件可供参考

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "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
        }
    ]
}

1.2. 语法入门

1.2.1. 数据类型

Python的数据类型有 number, String, List, DictionaryTuple, 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中,stringnumbertuple均是不可变对象(immutable),listdictset则是可变对象(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对象常常用到如下方法

  • extend: 合并数组
  • index: 获取指定元素的下标
  • remove: 删除指定下标元素(与Del相似)
  • copy: 创建列表副本(非浅复制)

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

1.2.2. 循环与判断

编程语言的语法最常用语法是循环与判断,以下代码举例ifwhilefor的用法

# 空值
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...

1.2.3. 函数

函数在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

1.2.4. 异常捕捉

在Python中,异常是一种特殊事件

一般情况下,在Python无法按照正常流程处理时就会抛出异常,如网络异常/文件被占用/端口被占用等情况

在Python中,捕捉异常可以使用tryexceptfinally,示例代码如下

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

1.3. 标准库

Python的标准库非常庞大,其组件范围也十分庞大

在不同的操作系统下也各不相同,所以这里着重了解以下日常常见的标准库

日常标准库全部文档可参考library - python.org

1.3.1. os

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常见的使用还包括

  • abspath: 返回文件或文件夹的绝对路径
  • basename: 返回文件或文件夹名(不含路径)
  • dirname: 返回文件或者文件夹的路径
  • exists: 文件/文件夹是否存在

更多用法可参考 os.path - 官方文档

os库除了读写文件外,日常使用中还包括获取环境变量以及大量日常与操作系统API相关的操作

1.3.2. sys

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)

1.3.3. shutil

shutil是相较于os库提供更多高阶的文件和文件集合操作方法

在Python中,shutil常用于文件的拷贝、移动、删除、解压缩等操作

以下面的例子说明shutil中几个常用方法copymovecopytreermtree

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

1.3.4. json

json是Python中用来处理Json数据最常用的库

Python中json库中常用方法包括了dumpsloads

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': '偷得浮生半日闲'}

1.3.5. time & datetime

timedatetime模块是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())

1.4. module与Package

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解释器会依顺序检查

  1. 当前工作目录
  2. PYTHONPATH
  3. 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

2. 进阶

2.1. Python环境管理

2.1.1. 多版本管理

在实际开发时,因为项目新旧关系,使用多个Python版本的需求非常常见

简单的处理办法是区分安装路径,设置不同的Path来区分不同的版本

实际上这样做不但麻烦,而且很容易出错,而Python有非常多的版本管理程序可供选择

最具代表性的便是Anacondaminicondapyenv

接下来以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版本的优势

  1. 快捷安装指定的Python版本
  2. 单独为每一个项目文件夹设置Python解释器的版本
  3. 无需记忆每一个项目对应的Python版本,进入项目目录自动对应Python版本

2.1.2. 使用虚拟环境

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

2.2. Python模块

2.2.1. 日志记录

大部分编程语言的日志库都是基础设施之一,因为日志记录是程序部署到生产环境正常运行极其重要的一环

日志记录对于程序来说,包括不限于以下3点作用

  • 运行状态记录
  • 方便重现Bug
  • 设置输出日志等级

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内置了几个对象

  • 日志对象Logger,用于控制日志输出
  • 日志处理对象Handler,将产生的日志输出到指定的位置
  • 过滤器对象Filter,提供粒度控制,用于控制过滤一些制定的输出内容
  • 格式化对象Formatter,搭配Handler对象可以实现不同的日志输出格式

2.2.2. 正则表达式

在Python中,正则表达式的使用非常常见,Python的标准库是完全支持正则表达式的

由于Python中采用反斜杠\来表达转义,所以在书写Python的正则表达式时,尽量使用r'string'的写法表示不进行Python字符串的转义

此文不涉及正则表达式的基础语法,若有兴趣请参考其他教程说明

Python对正则表示式的解析由import re来处理,下面介绍一些常见re库的方法

re.compile

正则表达式的预编译,可以用于匹配,如需多次反复使用建议提前使用此方法进行预编译提高性能

re.search

扫描字符串并返回匹配第一个相对应的对象,如匹配不到则返回None

import re

test_str = '(020)3386-2039'

r = re.search(r'\d{4}',test_str)
if not r:
    print('match fail')
if r:
    print(r.group()) # 3386

re.match

扫描字符串并从第一个字符开始匹配,如匹配失败则返回None

import re

test_str = '(020)3386-2039'

r = re.match(r'\d{4}',test_str)
if not r:
    print('match fail')
if r:
    print(r.group()) # match fail

re.fullmatch

如果整个字符串完全匹配规则则返回字符串,不匹配则返回None

import re

test_str = '(020)3386-2039'

r = re.fullmatch(r'\(\d+\)\d+-\d+',test_str)
if not r:
    print('match fail')
if r:
    print(r.group()) # (020)3386-2039

re.split

依据匹配规则进行切割,可指定切割次数,如果匹配失败,则返回完整字符串

import re

test_str = '(020)3386-2039'

r = re.split(r'z',test_str)
if not r:
    print('match fail')
if r:
    print(r) # ['', '020', '3386', '2039']

TODO...

2.2.3. 线程(Thread)

线程是操作系统进行运算调度的最小单位,也称之为轻量级进程

在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

2.2.4. 协程(Coroutine)

协程的内容较复杂,可参考以下这篇文章,写的非常简单易懂

协程与异步IO - 刘江的博客教程

多进程或者多线程效率高,却仍有缺点

  1. 多线程或多进程上下文切换导致的额外性能损耗
  2. 线程与进程的创建消耗资源较大
  3. 关键资源冲突的问题在不加锁的情况下难以解决

那么协程是什么?

协程本质上是一个线程执行多个任务,协程之间永远是数据安全的,且协程是用户级的,不由操作系统控制,所以也不需要”线程锁“

Python语言中存在GIL锁,因此Python进程中任何时刻都只有单条线程在执行

Python解释器在多线程这一点上,对IO任务进行优化工作

所以多线程执行IO任务时效率仍是正常的(相对地,CPU密集型任务采用多线程是不会有任何提升的)

而协程,则是提供给开发者去更好地解决IO堵塞问题的方法(即在单条线程中处理多个IO任务)

在Python3.X早期的版本中,实现协程的方法是yieldsend语法

在Python3.5的版本后,实现协程的方式asyncawait语法,同时提供了asyncio的IO堵塞方式

这里重点了解asyncawait语法以及asyncio

TODO...

2.3. 高阶语法

2.3.1. 列表推导式

列表推导式(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}

列表推导式语法非常强大,可以在实际开发中多多体验

2.3.2. 迭代器与生成器

迭代器是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在遍历之前并不会计算其中真正的值,这就是使用生成器的好处

生成器相对而言比较灵活,而且仅能遍历一次,限制性较大

2.4. 其他

2.4.1. GIL锁

CPython解释器采用一种叫做“GIL全局解释锁”的互斥锁,全称是Global Interpreter Lock,用于防止多线程并发执行机器码

关于GIL在这里不展开,其结论有2点

  • IO密集型任务,多线程与多进程的性能区别不大(GIL锁优化了IO堵塞的情况)
  • 计算密集型任务,多线程几乎没有提升(线程调度上下文切换可能会导致更慢),应采用多进程或协程处理

举例如下

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官网参考手册

2.4.2. CGI编程

3. 第三方库

3.1. requests

requests是Python中一个优雅的同步请求网络工具库

3.1.1. 安装

reqeusts非Python3自带库,使用pip安装如下

pip3 install requests

3.1.2. HTTP请求(GET/POST)

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}

3.1.3. Header设置

在发起模拟请求时,通常需要在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}

3.1.4. Cookies设置

除了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}

3.1.5. Form与Json数据的POST请求

下面的代码展示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}

3.1.6. Session请求

使用 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/>]>

[[replyMessage== null?"发表评论":"发表评论 @ " + replyMessage.m_author]]

account_circle
email
web_asset
textsms

评论列表([[messageResponse.total]])

还没有可以显示的留言...
[[messageItem.m_author]] [[messageItem.m_author]]
[[messageItem.create_time]]
[[getEnviron(messageItem.m_environ)]]
[[subMessage.m_author]] [[subMessage.m_author]] @ [[subMessage.parent_message.m_author]] [[subMessage.parent_message.m_author]]
[[subMessage.create_time]]
[[getEnviron(messageItem.m_environ)]]
目录