Python 进阶技巧

本文介绍了 Python 中一些较为高级但实际很常用的技巧,包括上下文管理器、动态导入、猴子补丁、鸭子类型。

上下文管理器

在任何一门编程语言中,文件的输入输出、数据库的连接断开、互斥锁的获得释放等,都是很常见的资源管理操作。但资源都是有限的,在写程序时,我们必须保证这些资源在使用过后得到释放,不然就容易造成资源泄露,轻则使得系统处理缓慢,重则会使系统崩溃。

为了解决这个问题,不同的编程语言都引入了不同的机制。而在 Python 中,对应的解决方式便是 上下文管理器(context manager),它可以确保用过的资源得到迅速释放,有效提高了程序的安全性。上下文管理器通常和 with 语句 一起使用,可以在极大程度上简化代码,例如:

1
2
3
4
5
6
7
8
9
with open('test.txt', 'w') as f:
f.write('hello world')

# 上面的代码等同于
f = open('test.txt', 'w')
try:
f.write('hello')
finally:
f.close()

示例:基于类的上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class FileManager(object):
def __init__(self, name, mode):
print('call __init__ method')

self.name = name
self.mode = mode
self.file = None

def __enter__(self):
# 返回需要被管理的资源
print('call __enter__ method')

self.file = open(self.name, self.mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
# 一些释放、清理资源的操作
print('call __exit__ method')

if self.file:
self.file.close()


with FileManager('test.txt', 'w') as f:
print('ready to write to file')
f.write('hello world')

动态导入

__import__() 内置函数用于动态加载类和函数,如果一个模块经常变化就可以使用 __import__() 来动态载入,问题是只会导入最高级的包或模块。

importlib 库的目的有两个:第一个目的是在 Python 源代码中提供 import 语句的实现(扩展了 __import__() 函数),第二个目的是使用户更容易导入他们的自定义模块。

  • importlib.import_module(name, package=None)
    • 导入一个模块。
    • 参数 name 指定了以绝对或相对导入方式导入什么模块(比如 pkg.mod..mod)。
    • 如果参数 name 使用相对导入方式,那么必须指定参数 packages 为要导入的包名,这个包名作为解析这个包名的锚点(比如 import_module(..mod, pkg.subpkg) 将会导入 pkg.mod)。
  • importlib.reload(module)
    • 重新加载之前导入的模块。
    • 参数 module 必须是一个之前已经成功导入过的模块对象。

示例:动态加载插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from importlib import import_module, reload


def loading(plugin_name):
try:
plugin = import_module(f'.{plugin_name}', package='test')
# plugin = reload(plugin) # reload module
poc = getattr(plugin, 'poc')
poc()
except (ModuleNotFoundError, AttributeError) as e:
print(e)


loading('payload') # 动态导入test目录下的payload.py文件

猴子补丁

猴子补丁(Monkey Patch),也叫做 热补丁、程序运行过程中的补丁,允许在运行期间动态修改一个类或模块。

如果是自己写的代码,Monkey Patch 就毫无意义了,直接改源码就可以,Monkey Patch 的主要用途在于源码不宜直接修改。比如你要修复一个第三方 module 的 bug 或者进行特定的修改、扩展,通常来说有下面几种做法:

  1. 直接把源 package/module,复制一份到当前项目中,再改源码。但不推荐,因为会导致当前的项目代码管理上的混乱。
  2. 向原作者提 pull request,以修正 bug 或者其他。应该如此,但未必能联系到对方,联系到了对方未必修改,修改了未必很快能用上。

这个时候 Monkey Patch 的价值就出来了,不用改原始的 module 源码,就能达到自己期望的效果。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# monkey_patch.py
import requests

base_get = requests.get # 保留原始函数


def monkey_get(a):
print('this is a test') # 增加自己的逻辑
return base_get(a) # 调用原始函数


requests.get = monkey_get # 覆盖原始函数

---------
# test.py
import requests


def send():
print(requests.get('http://www.baidu.com'))

---------
# run.py
import monkey_patch # 不导入monkey_patch,则执行原module的逻辑
from test import send

send()

鸭子类型

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型(Duck Typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由“当前方法和属性的集合”决定。鸭子类型在动态语言中常常使用,是用来实现多态的一种方式

示例:使用 duck typing 实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Eagle:
def fly(self):
print(f'{self.__class__.__name__} flying.')


class Pigeon:
def fly(self):
print(f'{self.__class__.__name__} flying.')


class Duck:
def fly(self):
print(f'{self.__class__.__name__} can not fly.')


for bird in [Eagle, Pigeon, Duck]:
bird().fly()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!