Python 基础学习12:模块

在安装 Python 时,标准安装会包含一组称为标准库的模块,除外之外还可以手动安装其它的模块。
使用模块可以做到真正的开箱即用。

模块

在脚本中可以通过 import 将函数从外部模块导入到程序中。
模块是如何编写的?

模块就是程序

任何的 Python 程序都可以作为模块进行导入。
示例,创建一个名为 hello.py

#hello.py
print("Hello World!")

这个文件名 hello (不包括 .py 后缀)将称为模块的名称。

文件存放的位置很重要,这决定了 Python 解释器能够找到该文件,或者从哪儿去找该文件。
要告诉解释器从哪儿找到该文件,可以通过导入 sys 模块,然后添加路径至 sys.path 中:

import sys
sys.path.append('/home/imxcai/python')

导入模块后,将执行对应文件中的程序:

import hello
Hello World!

在当前目录中会创建一个 __pycache__的目录,里面存放的是以 .pyc 为后缀的文件,里面是包含已经处理好的导入文件,在不更改模块时能更高效的处理它,如果更改了模块,它会重新生成。
导入之后,如果再次导入,将不会打印输出,因为模块不是用来执行操作的,而是用于定义变量、函数、类等,鉴于定义只需做一次,所以导入多次和导入一次的效果是相同的。

如果要重复导入的话可以使用 importlib 模块:

>>> import importlib
>>> hello = importlib.reload(hello)
Hello World!

模块是用于下定义的

让模块值得被创建的原因在于它们像类一样,有自己的作用域,这意味着在模块中定义的类和函数以及对其赋值的变量都将称为模块的属性。

在模块中定义函数

编写一个 hello2.py 的文件,只包含一个函数:

def hello():
    print("Hello World")

随后导入该模块,并进行调用:

>>> import hello2
>>> hello2.hello()
Hello World

在模块中定义函数或变量主要是为了实现代码的重用。

在模块中添加测试代码

模块用于定义函数和类,通常会在其中添加一些测试代码来检查功能,如以下 hello3.py 所示:

def hello():
    print("Hello World")

hello()

但是一旦导入到程序中时,就会自动的调用 hello(),要解决此类问题,可以检查模块是作为程序运行还是作为模块被导入到另一个程序的。
可以使用变量 __name__

>>> import hello3
Hello World
>>> __name__
'__main__'
>>> hello3.__name__
'hello3'

所以在模块中可以包含一个条件测试,来有条件的执行测试方法:

def hello():
    print("Hello World")

if __name__ == '__main__':
    hello()

此时导入,就不会自动的调用 hello() 了:

>>> import hello3
>>> hello3.hello()
Hello World

让模块可用

让模块可用,最大的挑战是将模块放置在何处。

将模块放置在合适的位置

通过 sys.path 可以看到 Python 解释器默认从哪些路径中寻找模块:

>>> import sys, pprint
>>> pprint.pprint(sys.path)
['',
 '/usr/lib64/python312.zip',
 '/usr/lib64/python3.12',
 '/usr/lib64/python3.12/lib-dynload',
 '/usr/lib64/python3.12/site-packages',
 '/usr/lib/python3.12/site-packages']

但是上述目录存在一些局限性:

  • 不希望默认的目录中存放自己写的文件
  • 没有权限

告诉解释器从哪儿找

一种是添加 sys.path ,但是这种方法不常用,标准做法是将模块所在的目录包含在环境变量 PYTHONPATH 中。

模块是单个单个的文件,为了组织模块,可以将其编组为包,包其实就是一种模块,但它可以包含其它模块(通常是为同一目标编写的模块)。
模块存储在 .py 文件中,而包是一个目录。
要让 Python 视目录为包,目录中必须包含文件 __init__.py 。如果像普通模块导入的话,该文件中的内容就是包的内容。
要将模块加入包中,只需将模块文件放在包目录中即可。还可以创建包嵌套包这种结构。
示例:

$ tree drawing/
drawing/
├── colors.py
├── __init__.py
└── shapes.py

随后可以通过以下方式导入包中的内容:

>>> import drawing #导入 drawing 包,可以使用 __init__.py 中的内容
>>> import drawing.colors #可以使用 colors 模块中的内容,用的是全名
>>> from drawing import shapes #可以使用简化名称 shapes 来使用该模块中内容

探索模块

探索模块是一项重要的技能。

模块中包含的内容

想要了解一个模块中包含哪些函数、类或变量?

使用 dir 函数

函数 dir 可以列出对象的所有属性:

>>> import copy
>>> dir(copy)
['Error', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_copy_dispatch', '_copy_immutable', '_deepcopy_atomic', '_deepcopy_dict', '_deepcopy_dispatch', '_deepcopy_list', '_deepcopy_method', '_deepcopy_tuple', '_keep_alive', '_reconstruct', 'copy', 'deepcopy', 'dispatch_table', 'error']

以下划线开头的属性是不提供外部使用的,使用列表推导进行过滤:

>>> [n for n in dir(copy) if not n.startswith('_')]
['Error', 'copy', 'deepcopy', 'dispatch_table', 'error']

使用变量 __all__

在模块中可能会有 __all__ 变量,它是一个列表,旨在定义模块的公有接口,并且告诉解释器如果执行 import copy.* 时,将导入的函数有哪些。

>>> copy.__all__
['Error', 'copy', 'deepcopy']

当需要使用非 __all__ 中的函数时,必须显式的导入 from copy import xxx
如果不设置 __all__,则会在以 import * 方式导入时,导入所有不以下划线开头的全局名称。

使用 help 获取帮助

可以使用 help 函数来获取模块中特定函数的信息:

>>> help(copy.copy)
Help on function copy in module copy:

copy(x)
    Shallow copy operation on arbitrary Python objects.

    See the module's __doc__ string for more info.

事实上,上述的帮助信息来自于 copy.copy 函数中的 __doc__ 字符串,所以也可以直接打印:

>>> print(copy.copy.__doc__)
Shallow copy operation on arbitrary Python objects.

    See the module's __doc__ string for more info.

文档

查看模块本身比查看模块文档快很多,当然查看模块的描述,搞清楚相关参数是什么,还是很有必要查看文档的。

>>> print(range.__doc__)
range(stop) -> range object
range(start, stop[, step]) -> range object

Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement).

使用源代码

想找到模块的源代码文件可以查看模块的 __file__ 属性:

>>> copy.__file__
'/usr/lib64/python3.12/copy.py'

标准库:一些深受欢迎的模块

安装完 Python 后,就可以使用大量的模块。

sys

sys 能帮助访问与 Python 解释器相关的变量和函数:

  • argv 命令行参数,包括脚本名
  • exit([arg]) 退出当前程序,可通过可选参数指定返回值或错误消息
  • modules 一个字典,将模块名映射到加载的模块
  • path 一个列表,包含要在其中查找模块的目录的名称
  • platform 一个平台标识符,如sunos5或win32
  • stdin 标准输入流——一个类似于文件的对象
  • stdout 标准输出流——一个类似于文件的对象
  • stderr 标准错误流——一个类似于文件的对象

os

os 模块能够帮助访问多个操作系统的服务。
重要的函数与变量:

  • environ 包含环境变量的映射
  • system(command) 在子shell中执行操作系统命令
  • sep 路径中使用的分隔符
  • pathsep 分隔不同路径的分隔符
  • linesep 行分隔符(’\n’、’\r’或’\r\n’)
  • urandom(n) 返回n个字节的强加密随机数据

如果想借助 os.system 来打开系统中的浏览器的话,可以有更优雅的方式,使用 webbrowser 模块:

>>> import webbrowser
>>> webbrowser.open('https://www.imxcai.com')
True

fileinput

fileinput 模块能够轻松的迭代一系列文本文件中的内容。
重要的函数:

  • input([files[, inplace[, backup]]]) 帮助迭代多个输入流中的行
  • filename() 返回当前文件的名称
  • lineno() 返回(累计的)当前行号
  • filelineno() 返回在当前文件中的行号
  • isfirstline() 检查当前行是否是文件中的第一行
  • isstdin() 检查最后一行是否来自sys.stdin
  • nextfile()关闭当前文件并移到下一个文件
  • close()关闭序列

为一个 Python 脚本中添加行号的示例:

import fileinput
for line in fileinput.input(inplace=True):
    line = line.rstrip()
    num = fileinput.lineno()
    print('{:<50} # {:2d}'.format(line, num))

集合、堆和双端队列

除了常用的字典和列表外,Python 还有其它的数据结构。

集合

集合是由内置类 set 实现的,可以直接通过这个类创建集合。

>>> set(range(10))
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

可以用花括号显式的创建,但不能通过花括号来创建空集合,这将会创建一个字典。

>>> type({})
<class 'dict'>
>>> type({1,2,3})
<class 'set'>

集合主要用于成员资格的检查,所以会自动忽略重复的元素:

>>> {1,2,2,3,1}
{1, 2, 3}

并且集合中元素的顺序也是不确定的。

>>> {'b', 'a', 'd', 'c'}
{'b', 'c', 'a', 'd'}

对集合可以进行交集和并集的计算。
要计算两个集合的并集,可以对其中一个集合调用 union 方法,或使用欧冠按位运算符 |

>>> a = {1,2,3}
>>> b = {2,3,4}
>>> a.union(b)
{1, 2, 3, 4}
>>> a | b
{1, 2, 3, 4}

除此之外对集合常用的操作如下:

>>> a & b
{2, 3}
>>> c = a & b
>>> c.issubset(a)
True
>>> c <= a
True
>>> a.intersection(b)
{2, 3}
>>> a.difference(b)
{1}
>>> a - b
{1}
>>> a.symmetric_difference(b)
{1, 4}
>>> a ^ b
{1, 4}

集合不能直接通过 add 方法添加集合中的集合,需要借助 frozenset

>>> a.add(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'

>>> a.add(frozenset(b))
>>> a
{1, 2, 3, frozenset({2, 3, 4})}

堆(heap) 是一种优先队列,能够以任意顺序添加对象,并随时找出(并删除)最小的元素,相比列表方法 min 效率高很多。

Python 没有独立的堆类型,只有一些堆操作函数的模块,模块名为 heapq ,包含6个函数:

  • heappush(heap, x) 将x压入堆中
  • heappop(heap) 从堆中弹出最小的元素
  • heapify(heap) 让列表具备堆特征
  • heapreplace(heap, x) 弹出最小的元素,并将x压入堆中
  • nlargest(n, iter) 返回iter中n个最大的元素
  • nsmallest(n, iter) 返回iter中n个最小的元素

示例:

>>> from heapq import *
>>> from random import shuffle
>>> data = list(range(10))
>>> shuffle(data)
>>> data
[5, 6, 1, 2, 0, 8, 3, 7, 9, 4]
>>> heap = []
>>> for n in data:
...     heappush(heap, n)
... 
>>> heap
[0, 1, 3, 6, 2, 8, 5, 7, 9, 4]
>>> heappush(heap, 0.5)
>>> heap
[0, 0.5, 3, 6, 1, 8, 5, 7, 9, 4, 2]
>>> heappop(heap)
0
>>> heappop(heap)
0.5
>>> heappop(heap)
1

heapreplace 要比依次执行 heappopheappush 效率更高:

>>> heap
[2, 4, 3, 6, 9, 8, 5, 7]
>>> heapreplace(heap, 1)
2
>>> heap
[1, 4, 3, 6, 9, 8, 5, 7]

双端队列

在需要按添加元素的顺序进行删除时,双端队列很有用。
示例:

>>> from collections import deque
>>> q = deque(range(5))
>>> q.append(5)
>>> q.appendleft(6)
>>> q
deque([6, 0, 1, 2, 3, 4, 5])
>>> q.pop()
5
>>> q.popleft()
6

time

模块 time 包含用于获取当前时间、操作系统时间和日期、从字符串中读取日期、将日期格式化为字符串的函数。

时间可以用元组表示,如 (2008, 1, 21, 12, 2, 56, 0, 21, 0) 后面三个元素分别代表 星期、儒略日(一年的第几天)、夏令时。

time 模块中重要的函数:

  • asctime([tuple]) 将时间元组转换为字符串
  • localtime([secs]) 将秒数转换为表示当地时间的日期元组
  • mktime(tuple) 将时间元组转换为当地时间
  • sleep(secs) 休眠(什么都不做)secs秒
  • strptime(string[, format]) 将字符串转换为时间元组
  • time() 当前时间(从新纪元开始后的秒数,以UTC为准)

random

模块 random 包含生成伪随机数的函数。
模块中的重要函数:

  • random() 返回一个0~1(含)的随机实数
  • getrandbits(n) 以长整数方式返回n个随机的二进制位
  • uniform(a, b) 返回一个a~b(含)的随机实数
  • randrange([start], stop, [step]) 从range(start, stop, step)中随机地选择一个数
  • choice(seq) 从序列seq中随机地选择一个元素
  • shuffle(seq[, random]) 就地打乱序列seq
  • sample(seq, n) 从序列seq中随机地选择n个值不同的元素

从文本文件中随机选择一行的示例:

import random, fileinput

fortunes = list(fileinput.input())
print(random.choice(fortunes))

随机发牌示例:

from random import shuffle
values = list(range(1,11)) + 'Jack Queen King'.split()
suits = 'diamonds clubs hearts spades'.split()
deck = ['{} of {}'.format(v, s) for v in values for s in suits ]
shuffle(deck)

while deck:
    ignore = input(deck.pop())

shelve 和 json

可以使用 shelve 模块将对象持久化存储到文件中。
一个使用 shelve 模块的简单数据库示例:

import sys, shelve

def store_person(db):
    """let custom input data and store to shelf obj.

    Args:
        db (_type_): _description_
    """
    pid = input('Enter unique ID number: ')
    person = {}
    person['name'] = input('Enter name: ')
    person['age'] = input('Enter age: ')
    person['phone'] = input('Enter phone number: ')
    db[pid] = person

def lookup_person(db):
    """input ID and required field, then get data from shelf obj.

    Args:
        db (_type_): _description_
    """
    pid = input('Enter ID number: ')
    field = input('What would you like to know?(name, age, phone) ')
    field = field.strip().lower()

    print(field.capitalize() + ':', db[pid][field])

def print_help():
    help_str = """
    The available command are:
    store: Stores information about a person
    lookup: Looks up a person from ID number
    ?: Print this message
    quit: Quit program
    """
    print(help_str)

def enter_command():
    cmd = input('Enter command(? for help): ')
    cmd = cmd.strip().lower()
    return cmd

# def main():
#     database = shelve.open('database.dat')
#     while True:
#         try:
#             cmd = enter_command()
#             if cmd == 'store':
#                 store_person(database)
#             elif cmd == 'lookup':
#                 lookup_person(database)
#             elif cmd == '?':
#                 print_help()
#             elif cmd == 'quit':
#                 return
#         except KeyError:
#                 print('invaild id or field, pls try again')

#     database.close()   

def main():
    database = shelve.open('database.dat')
    try:
        while True:
            cmd = enter_command()
            if cmd == 'store':
                store_person(database)
            elif cmd == 'lookup':
                lookup_person(database)
            elif cmd == '?':
                print_help()
            elif cmd == 'quit':
                return
    except KeyError:
        print('input id or field wrong, pls try again')
    finally:
        database.close()


if __name__ == '__main__': main()

re

模块 re 提供了对正则表达式的支持。

正则表达式

正则表达式是可匹配文本片段的模式。
最简单的正则表达式为普通字符串,与它自己匹配。

通配符
正则表达式可与多个字符串匹配,可使用特殊字符来创建这种正则表达式。
. 与除换行符外的其它字符都匹配。

对特殊字符进行转义
要让特殊字符的行为与普通字符一样,可以对其进行转义,在前面加一个 \

字符集
用方括号将一个子串括起,称之为字符集,这样的字符集与其包含的字符都匹配,如 [pj]ythonpythonjpython 匹配。
[a-zA-Z0-9] 与大小写字母和数字都匹配, 字符集只能匹配一个字符。
要指定排除字符,在字符集中添加一个 ^ ,如 [^abc] 与除a 、b、c 外的其它字符匹配。

二选一和子模式
管道符 | 是可以表示二选一的特殊字符,如 Python|Perl 这个例子也可以写成 P(ython|erl) 。单个字符也称为子模式。

可选模式和重复模式
在子模式后面加上问号,可将其指定为可选的,即可出现一次或不出现,除外还有几个运算符用于表示子模式可重复多次:

  • (pattern)* : pattern 可重复0、1或多次
  • (pattern)+:pattern 可重复1或多次
  • (pattern){m,n}:pattern 可重复 m~n 次

字符串的开头和结尾
可使用脱字符 ^ 来指定字符串的开头与模式匹配,$ 值指定字符串末尾匹配。

re 模块

re 模块包含多个使用正则表达式的函数:

  • compile(pattern[, flags]) 根据包含正则表达式的字符串创建模式对象
  • search(pattern, string[, flags]) 在字符串中查找模式
  • match(pattern, string[, flags]) 在字符串开头匹配模式
  • split(pattern, string[, maxsplit=0]) 根据模式来分割字符串
  • findall(pattern, string) 返回一个列表,其中包含字符串中所有与模式匹配的子串
  • sub(pat, repl, string[, count=0]) 将字符串中与模式pat匹配的子串都替换为repl
  • escape(string) 对字符串中所有的正则表达式特殊字符都进行转义

re.search 在给定的字符串中查找到第一个与指定正则表达式匹配的字串,如果找到了返回 True ,反之返回 False ,示例:

>>> re.search('.ython', 'Python Cython')
<re.Match object; span=(0, 6), match='Python'>

re.split 根据与模式匹配的子串来分隔字符串:

>>> some_text = 'alpha, beta,,,,gamma  delta'
>>> re.split('[, ]+', some_text)
['alpha', 'beta', 'gamma', 'delta']

re.findall 返回一个列表,包含所有与给定模式匹配的子串:

>>> re.findall('.ython', 'Python Cython')
['Python', 'Cython']

re.sub 从左往右将与模式匹配的字串替换为指定内容:

>>> re.sub('{name}', 'imxcai', 'hello {name}!, welcome {name}')
'hello imxcai!, welcome imxcai'

匹配对象和编组

编组就是放在圆括号内的子模式,根据左边的括号数编号的,其中0指的是整个模式。
re 匹配对象的一些重要方法:

  • group([group1, ...]) 获取与给定子模式(编组)匹配的子串
  • start([group]) 返回与给定编组匹配的子串的起始位置
  • end([group])返回与给定编组匹配的子串的终止位置(与切片一样,不包含终止位置)
  • span([group]) 返回与给定编组匹配的子串的起始和终止位置

编组示例:

>>> m = re.match(r'www\.(.*)\..{3}', 'www.python.org')
>>> m.group(1)
'python'
>>> m.start(1)
4
>>> m.end(1)
10
>>> m.span(1)
(4, 10)

替换中的组号和函数

使用 re.sub 替换字符串:

>>> emphasis_pattern = r'\*([^\*]+)\*'
>>> re.sub(emphasis_pattern, r'<em>\1</em>', 'Hello, *world*!')
'Hello, <em>world</em>!'

查找邮件发送人名字和收件人邮箱地址的示例:

import re, fileinput

pat = re.compile('From: (.*) <.*?>$')
mail_addr = re.compile(r'[a-z\-\.]+@[a-z\-\.]+', re.IGNORECASE)
addresses = set()

for line in fileinput.input():
    m = pat.match(line)
    if m: print(m.group(1))
    for address in mail_addr.findall(line):
        addresses.add(address)

for address in sorted(addresses):
    print(address)

模板系统示例:

import fileinput, re

field_pat = re.compile(r'\[(.+?)\]')
scope = {}

def replacement(match):
    code = match.group(1)
    try:
        return str(eval(code, scope))
    except SyntaxError:
        exec(code, scope)
        print('scope :', scope['name'])

        return ''

lines = []
for line in fileinput.input():
    lines.append(line)

text = ''.join(lines)

print(text)

print(field_pat.sub(replacement, text))

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部