你可能不知道的Python 技巧小结


Posted in Python onJanuary 29, 2020

译者 | 豌豆花下猫

声明 :本文获得原作者授权翻译,转载请保留原文出处,请勿用于商业或非法用途。

有许许多多文章写了 Python 中的许多很酷的特性,例如变量解包、偏函数、枚举可迭代对象,但是关于 Python 还有很多要讨论的话题,因此在本文中,我将尝试展示一些我知道的和在使用的,但很少在其它文章提到过的特性。那就开始吧。

1、对输入的字符串“消毒”

对用户输入的内容“消毒”,这问题几乎适用于你编写的所有程序。通常将字符转换为小写或大写就足够了,有时你还可以使用正则表达式来完成工作,但是对于复杂的情况,还有更好的方法:

user_input = "This\nstring has\tsome whitespaces...\r\n"

character_map = {
 ord('\n') : ' ',
 ord('\t') : ' ',
 ord('\r') : None
}
user_input.translate(character_map) # This string has some whitespaces... "

在此示例中,你可以看到空格字符“ \n”和“ \t”被单个空格替换了,而“ \r”则被完全删除。这是一个简单的示例,但是我们可以更进一步,使用unicodedata 库及其 combining() 函数,来生成更大的重映射表(remapping table),并用它来删除字符串中所有的重音。

2、对迭代器切片

如果你尝试直接对迭代器切片,则会得到 TypeError ,提示说该对象不可取下标(not subscriptable),但是有一个简单的解决方案:

import itertools

s = itertools.islice(range(50), 10, 20) # <itertools.islice object at 0x7f70fab88138>
for val in s:
 ...

使用itertools.islice,我们可以创建一个 islice 对象,该对象是一个迭代器,可以生成我们所需的内容。但是这有个重要的提醒,即它会消耗掉切片前以及切片对象 islice 中的所有元素。

(译注:更多关于迭代器切片的内容,可阅读 Python进阶:迭代器与迭代器切片)

3、跳过可迭代对象的开始

有时候你必须处理某些文件,它们以可变数量的不需要的行(例如注释)为开头。 itertools 再次提供了简单的解决方案:

string_from_file = """
// Author: ...
// License: ...
//
// Date: ...

Actual content...
"""

import itertools

for line in itertools.dropwhile(lambda line:line.startswith("//"), string_from_file.split("\n")):
 print(line)

这段代码仅会打印在初始的注释部分之后的内容。如果我们只想丢弃迭代器的开头部分(在此例中是注释),并且不知道有多少内容,那么此方法很有用。

4、仅支持关键字参数(kwargs)的函数

当需要函数提供(强制)更清晰的参数时,创建仅支持关键字参数的函数,可能会挺有用:

def test(*, a, b):
 pass

test("value for a", "value for b") # TypeError: test() takes 0 positional arguments...
test(a="value", b="value 2") # Works...

如你所见,可以在关键字参数之前,放置单个 * 参数来轻松解决此问题。如果我们将位置参数放在 * 参数之前,则显然也可以有位置参数。

5、创建支持 with 语句的对象

我们都知道如何使用 with 语句,例如打开文件或者是获取锁,但是我们可以实现自己的么?是的,我们可以使用__enter__ 和__exit__ 方法来实现上下文管理器协议:

class Connection:
 def __init__(self):
 ...

 def __enter__(self):
 # Initialize connection...

 def __exit__(self, type, value, traceback):
 # Close connection...

with Connection() as c:
 # __enter__() executes
 ...
 # conn.__exit__() executes

这是在 Python 中实现上下文管理的最常见方法,但是还有一种更简单的方法:

from contextlib import contextmanager

@contextmanager
def tag(name):
 print(f"<{name}>")
 yield
 print(f"</{name}>")

with tag("h1"):
 print("This is Title.")

上面的代码段使用 contextmanager 装饰器实现了内容管理协议。tag 函数的第一部分(yield 之前)会在进入 with 语句时执行,然后执行 with 的代码块,最后会执行 tag 函数的剩余部分。

6、用__slots__节省内存

如果你曾经编写过一个程序,该程序创建了某个类的大量实例,那么你可能已经注意到你的程序突然就需要大量内存。那是因为 Python 使用字典来表示类实例的属性,这能使其速度变快,但内存不是很高效。通常这不是个问题,但是,如果你的程序遇到了问题,你可以尝试使用__slots__ :

class Person:
 __slots__ = ["first_name", "last_name", "phone"]
 def __init__(self, first_name, last_name, phone):
 self.first_name = first_name
 self.last_name = last_name
 self.phone = phone

这里发生的是,当我们定义__slots__属性时,Python 使用固定大小的小型数组,而不是字典,这大大减少了每个实例所需的内存。使用__slots__还有一些缺点——我们无法声明任何新的属性,并且只能使用在__slots__中的属性。同样,带有__slots__的类不能使用多重继承。

7、限制CPU和内存使用量

如果不是想优化程序内存或 CPU 使用率,而是想直接将其限制为某个固定数字,那么 Python 也有一个库能做到:

import signal
import resource
import os

# To Limit CPU time
def time_exceeded(signo, frame):
 print("CPU exceeded...")
 raise SystemExit(1)

def set_max_runtime(seconds):
 # Install the signal handler and set a resource limit
 soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
 resource.setrlimit(resource.RLIMIT_CPU, (seconds, hard))
 signal.signal(signal.SIGXCPU, time_exceeded)

# To limit memory usage
def set_max_memory(size):
 soft, hard = resource.getrlimit(resource.RLIMIT_AS)
 resource.setrlimit(resource.RLIMIT_AS, (size, hard))

在这里,我们可以看到两个选项,可设置最大 CPU 运行时间和内存使用上限。对于 CPU 限制,我们首先获取该特定资源(RLIMIT_CPU)的软限制和硬限制,然后通过参数指定的秒数和先前获取的硬限制来设置它。最后,如果超过 CPU 时间,我们将注册令系统退出的信号。至于内存,我们再次获取软限制和硬限制,并使用带有 size 参数的setrlimit 和获取的硬限制对其进行设置。

8、控制可以import的内容

某些语言具有非常明显的用于导出成员(变量、方法、接口)的机制,例如Golang,它仅导出以大写字母开头的成员。另一方面,在 Python 中,所有内容都会被导出,除非我们使用__all__ :

def foo():
 pass

def bar():
 pass

__all__ = ["bar"]

使用上面的代码段,我们可以限制from some_module import * 在使用时可以导入的内容。对于以上示例,通配导入时只会导入 bar。此外,我们可以将__all__ 设为空,令其无法导出任何东西,并且在使用通配符方式从此模块中导入时,将引发 AttributeError。

9、比较运算符的简便方法

为一个类实现所有比较运算符可能会很烦人,因为有很多的比较运算符——__lt__、__le__、__gt__ 或__ge__。但是,如果有更简单的方法呢?functools.total_ordering 可救场:

from functools import total_ordering

@total_ordering
class Number:
 def __init__(self, value):
 self.value = value

 def __lt__(self, other):
 return self.value < other.value

 def __eq__(self, other):
 return self.value == other.value

print(Number(20) > Number(3))
print(Number(1) < Number(5))
print(Number(15) >= Number(15))
print(Number(10) <= Number(2))

这到底如何起作用的?total_ordering 装饰器用于简化为我们的类实例实现排序的过程。只需要定义__lt__ 和__eq__,这是最低的要求,装饰器将映射剩余的操作——它为我们填补了空白。

( 译注: 原作者的文章分为两篇,为了方便读者们阅读,我特将它们整合在一起,以下便是第二篇的内容。)

10、使用slice函数命名切片

使用大量硬编码的索引值会很快搞乱维护性和可读性。一种做法是对所有索引值使用常量,但是我们可以做得更好:

# ID First Name Last Name
line_record = "2  John   Smith"

ID = slice(0, 8)
FIRST_NAME = slice(9, 21)
LAST_NAME = slice(22, 27)

name = f"{line_record[FIRST_NAME].strip()} {line_record[LAST_NAME].strip()}"
# name == "John Smith"

在此例中,我们可以避免神秘的索引,方法是先使用 slice 函数命名它们,然后再使用它们。你还可以通过 .start、.stop和 .stop 属性,来了解 slice 对象的更多信息。

11、在运行时提示用户输入密码

许多命令行工具或脚本需要用户名和密码才能操作。因此,如果你碰巧写了这样的程序,你可能会发现 getpass 模块很有用:

import getpass

user = getpass.getuser()
password = getpass.getpass()
# Do Stuff...

这个非常简单的包通过提取当前用户的登录名,可以提示用户输入密码。但是须注意,并非每个系统都支持隐藏密码。Python 会尝试警告你,因此切记在命令行中阅读警告信息。

12、查找单词/字符串的相近匹配

现在,关于 Python 标准库中一些晦涩难懂的特性。如果你发现自己需要使用Levenshtein distance 【2】之类的东西,来查找某些输入字符串的相似单词,那么 Python 的 difflib 会为你提供支持。

import difflib
difflib.get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'], n=2)
# returns ['apple', 'ape']

difflib.get_close_matches 会查找最佳的“足够好”的匹配。在这里,第一个参数与第二个参数匹配。我们还可以提供可选参数 n ,该参数指定要返回的最多匹配结果。另一个可选的关键字参数 cutoff (默认值为 0.6),可以设置字符串匹配得分的阈值。

13、使用IP地址

如果你必须使用 Python 做网络开发,你可能会发现 ipaddress 模块非常有用。一种场景是从 CIDR(无类别域间路由 Classless Inter-Domain Routing)生成一系列 IP 地址:

import ipaddress
net = ipaddress.ip_network('74.125.227.0/29') # Works for IPv6 too
# IPv4Network('74.125.227.0/29')

for addr in net:
 print(addr)

# 74.125.227.0
# 74.125.227.1
# 74.125.227.2
# 74.125.227.3
# ...

另一个不错的功能是检查 IP 地址的网络成员资格:

ip = ipaddress.ip_address("74.125.227.3")

ip in net
# True

ip = ipaddress.ip_address("74.125.227.12")
ip in net
# False

还有很多有趣的功能,在这里【3】可以找到,我不再赘述。但是请注意,ipaddress 模块和其它与网络相关的模块之间只有有限的互通性。例如,你不能将 IPv4Network 实例当成地址字符串——需要先使用 str 转换它们。

14、在Shell中调试程序崩溃

如果你是一个拒绝使用 IDE,并在 Vim 或 Emacs 中进行编码的人,那么你可能会遇到这样的情况:拥有在 IDE 中那样的调试器会很有用。

你知道吗?你有一个——只要用python3.8 -i 运行你的程序——一旦你的程序终止了, -i 会启动交互式 shell,在那你可以查看所有的变量和调用函数。整洁,但是使用实际的调试器(pdb )会如何呢?让我们用以下程序(script.py ):

def func():
 return 0 / 0

func()

并使用python3.8 -i script.py运行脚本:

# Script crashes...
Traceback (most recent call last):
  File "script.py", line 4, in <module>
    func()
  File "script.py", line 2, in func
    return 0 / 0
ZeroDivisionError: division by zero
>>> import pdb
>>> pdb.pm()  # Post-mortem debugger
> script.py(2)func()
-> return 0 / 0
(Pdb)

我们看到了崩溃的地方,现在让我们设置一个断点:

def func():
 breakpoint() # import pdb; pdb.set_trace()
 return 0 / 0

func()

现在再次运行它: 

  script.py(3)func()
-> return 0 / 0
(Pdb)  # we start here
(Pdb) step
ZeroDivisionError: division by zero
> script.py(3)func()
-> return 0 / 0
(Pdb)

大多数时候,打印语句和错误信息就足以进行调试,但是有时候,你需要四处摸索,以了解程序内部正在发生的事情。在这些情况下,你可以设置断点,然后程序执行时将在断点处停下,你可以检查程序,例如列出函数参数、表达式求值、列出变量、或如上所示仅作单步执行。

pdb 是功能齐全的 Python shell,理论上你可以执行任何东西,但是你还需要一些调试命令,可在此处【4】找到。

15、在一个类中定义多个构造函数

函数重载是编程语言(不含 Python)中非常常见的功能。即使你不能重载正常的函数,你仍然可以使用类方法重载构造函数:

import datetime

class Date:
 def __init__(self, year, month, day):
  self.year = year
  self.month = month
  self.day = day

 @classmethod
 def today(cls):
  t = datetime.datetime.now()
  return cls(t.year, t.month, t.day)

d = Date.today()
print(f"{d.day}/{d.month}/{d.year}")
# 14/9/2019

你可能倾向于将替代构造函数的所有逻辑放入__init__,并使用*args 、**kwargs 和一堆 if 语句,而不是使用类方法来解决。那可能行得通,但是却变得难以阅读和维护。

因此,我建议将很少的逻辑放入__init__,并在单独的方法/构造函数中执行所有操作。这样,对于类的维护者和用户而言,得到的都是干净的代码。

16、使用装饰器缓存函数调用

你是否曾经编写过一种函数,它执行昂贵的 I/O 操作或一些相当慢的递归,而且该函数可能会受益于对其结果进行缓存(存储)?如果你有,那么有简单的解决方案,即使用 functools 的lru_cache :

from functools import lru_cache
import requests

@lru_cache(maxsize=32)
def get_with_cache(url):
 try:
  r = requests.get(url)
  return r.text
 except:
  return "Not Found"


for url in ["https://google.com/",
   "https://martinheinz.dev/",
   "https://reddit.com/",
   "https://google.com/",
   "https://dev.to/martinheinz",
   "https://google.com/"]:
 get_with_cache(url)

print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)

在此例中,我们用了可缓存的 GET 请求(最多 32 个缓存结果)。你还可以看到,我们可以使用 cache_info 方法检查函数的缓存信息。装饰器还提供了 clear_cache 方法,用于使缓存结果无效。

我还想指出,此函数不应与具有副作用的函数一起使用,或与每次调用都创建可变对象的函数一起使用。

17、在可迭代对象中查找最频繁出现的元素

在列表中查找最常见的元素是非常常见的任务,你可以使用 for 循环和字典(map),但是这没必要,因为 collections 模块中有 Counter 类:

from collections import Counter

cheese = ["gouda", "brie", "feta", "cream cheese", "feta", "cheddar",
   "parmesan", "parmesan", "cheddar", "mozzarella", "cheddar", "gouda",
   "parmesan", "camembert", "emmental", "camembert", "parmesan"]

cheese_count = Counter(cheese)
print(cheese_count.most_common(3))
# Prints: [('parmesan', 4), ('cheddar', 3), ('gouda', 2)]

实际上,Counter 只是一个字典,将元素与出现次数映射起来,因此你可以将其用作普通字典:

print(cheese_count["mozzarella"])
# Prints: 1

cheese_count["mozzarella"] += 1

print(cheese_count["mozzarella"])
# Prints: 2

除此之外,你还可以使用 update(more_words) 方法轻松添加更多元素。Counter 的另一个很酷的特性是你可以使用数学运算(加法和减法)来组合和减去 Counter 的实例。

小结

在日常 Python 编程中,并非所有这些特性都是必不可少的和有用的,但是其中一些特性可能会时不时派上用场,并且它们也可能简化任务,而这本来可能很冗长且令人讨厌。

我还要指出的是,所有这些特性都是 Python 标准库的一部分,虽然在我看来,其中一些特性非常像是标准库中的非标准内容。因此,每当你要在 Python 中实现某些功能时,首先可在标准库查看,如果找不到,那你可能看得还不够仔细(如果它确实不存在,那么肯定在某些三方库中)。

如果你使用 Python,那么我认为在这里分享的大多数技巧几乎每天都会有用,因此我希望它们会派上用场。另外,如果你对这些 Python 技巧和骚操作有任何想法,或者如果你知道解决上述问题的更好方法,请告诉我!?

相关链接

[1] 原文地址: https://martinheinz.dev/blog/1

[2] Levenshtein distance: https://en.wikipedia.org/wiki/Levenshtein_distance

[3] 在这里: https://docs.python.org/3/howto/ipaddress.html

[4] 此处: https://docs.python.org/3/library/pdb.html%23debugger-commands#debugger-commands 

Python 相关文章推荐
python写的ARP攻击代码实例
Jun 04 Python
在Windows8上的搭建Python和Django环境
Jul 03 Python
Python的pycurl包用法简介
Nov 13 Python
对python读取zip压缩文件里面的csv数据实例详解
Feb 08 Python
pandas 时间格式转换的实现
Jul 06 Python
Python企业编码生成系统之主程序模块设计详解
Jul 26 Python
python实现对列表中的元素进行倒序打印
Nov 23 Python
tensorflow 初始化未初始化的变量实例
Feb 06 Python
Python xpath表达式如何实现数据处理
Jun 13 Python
Python如何读写二进制数组数据
Aug 01 Python
详解Scrapy Redis入门实战
Nov 18 Python
Python基础之变量的相关知识总结
Jun 23 Python
Python如何通过Flask-Mail发送电子邮件
Jan 29 #Python
Python原始套接字编程实例解析
Jan 29 #Python
Python内置类型性能分析过程实例
Jan 29 #Python
python add_argument()用法解析
Jan 29 #Python
python使用ctypes调用扩展模块的实例方法
Jan 28 #Python
Python 时间戳之获取整点凌晨时间戳的操作方法
Jan 28 #Python
使用Python制作新型冠状病毒实时疫情图
Jan 28 #Python
You might like
世界上第一台立体声收音机
2021/03/01 无线电
PHP与MySQL交互使用详解
2006/10/09 PHP
PHP 程序员也要学会使用“异常”
2009/06/16 PHP
PHP开发环境配置(MySQL数据库安装图文教程)
2010/04/28 PHP
利用PHP+JS实现搜索自动提示(实例)
2013/06/09 PHP
PHP简单遍历对象示例
2016/09/28 PHP
javascript 面向对象编程基础:封装
2009/08/21 Javascript
jQueryPad 实用的jQuery测试工具(支持IE,chrome,FF)
2010/05/22 Javascript
jQuery 源码分析笔记(5) jQuery.support
2011/06/19 Javascript
jquery 漂亮的删除确认和提交无刷新删除示例
2013/11/13 Javascript
js实现网页右上角滑出会自动消失大幅广告的方法
2015/02/27 Javascript
JavaScript实现页面5秒后自动跳转的方法
2015/04/16 Javascript
自定义刻度jQuery进度条及插件
2015/09/02 Javascript
Javascript控制div属性动态变化实例分析
2015/10/08 Javascript
图解JavaScript中的this关键字
2020/05/28 Javascript
Bootstrap模态框案例解析
2017/03/05 Javascript
JavaScript实现两个select下拉框选项左移右移
2017/03/09 Javascript
ng2学习笔记之bootstrap中的component使用教程
2017/03/09 Javascript
ES6新特性七:数组的扩充详解
2017/04/21 Javascript
关于express与koa的使用对比详解
2018/01/25 Javascript
JavaScript的数据类型转换原则(干货)
2018/03/15 Javascript
vue-cli 打包使用history模式的后端配置实例
2018/09/20 Javascript
使用ECharts实现状态区间图
2018/10/25 Javascript
图文详解vue框架安装步骤
2019/02/12 Javascript
Ajax请求时无法重定向的问题解决代码详解
2019/06/21 Javascript
vue3.0中使用postcss-pxtorem的具体方法
2019/11/20 Javascript
深入浅析ImageMagick命令执行漏洞
2016/10/11 Python
使用pyinstaller打包PyQt4程序遇到的问题及解决方法
2019/06/24 Python
python函数中将变量名转换成字符串实例
2020/05/11 Python
CSS3制作3D立方体loading特效
2020/11/09 HTML / CSS
HTML 5.1来了 9月份正式发布 更新内容预览
2016/04/26 HTML / CSS
洲际酒店集团英国官网:IHG英国
2019/07/10 全球购物
linux面试题参考答案(8)
2015/08/11 面试题
员工离职感谢信
2015/01/22 职场文书
spring cloud eureka 服务启动失败的原因分析及解决方法
2022/03/17 Java/Android
基于Python实现流星雨效果的绘制
2022/03/18 Python