《Effective Python》笔记

本文介绍了如何进行有效的Python编程,以提高编码质量。
本文主要参考的书籍《Effective Python》 [美] Brett Slatkin。

用Pythonic方式思考

Python开发者用Pythonic这个形容词来描述那种符合特定风格的代码。

1 遵循PEP8风格指南

《Python Enhancement Proposal #8》又叫PEP8,是针对Python代码格式而编订的风格指南。下面列出几条常见的规则:

  • 空格

    • 使用space代替tab表示缩进
    • 每行字符数不应超过79
    • 文件中函数与类之间用2个空行,同一个类中,各方法之间用1个空行隔开
    • 在使用下标获取列表元素、调用函数或给关键字参数赋值时,不要在两旁添加空格
    • 为变量赋值时,赋值符号的左侧和右侧应各自写上一个空格
  • 命名

    • 函数、变量及属性应用小写字母来拼写,各单词之间以下划线相连
    • 受保护的实例属性,应以单个下划线开头;私有的实例属性,应以两个下划线开头
    • 类与异常,应以每个单词首字母均大写的形式命名
    • 模块级别的常量,应全部用大写字母拼写,各单词之间以下划线相连
  • 表达式和语句

    • 采用内联形式的否定词,如: 写if a is not b而不是if not a is b
    • 不要用检测长度的方法判断somelist是否空值,而是采用if not somelist写法判断
    • import语句应写文件开头,最好用绝对名称
    • import语句划分成三个部分,分别表示标准库模块、第三方模块以及自用模块,在每一部分中,各import语句应按模块的字母顺序来排列

2 了解bytes、str和unicode的区别

Python3有两种表示字符序列的类型:bytes和str。前者的实例包含原始的8位值,后者包含Unicode字符。
Python2两种表示字符序列的类型为:str和unicode。str的实例包含原始的8位值,unicode则包含Unicode字符。
把Unicode字符表示为二进制数据有多种方法,最常见的编码方式就是UTF-8。
编写Python程序时候,一定要把编码和解码操作放在界面最外围来做。
对于Python3和Python2,我们需要两种辅助函数方便开发者在UTF-8和Unicode两种情况之间转换。

  • Python3,接受str或bytes,返回str的方法:
def to_str(bytes_or_str):
    if isinstance(bytes_or_str,bytes):
        value = bytes_or_str.decode('utf-8')
    else:
        value = bytes_or_str
    return value    # Instance of str
  • Python3,接受str或bytes,返回bytes的方法:
def to_bytes(bytes_or_str):
    if isinstance(bytes_or_str,str):
        value = bytes_or_str.encode('utf-8')
    else:
        value = bytes_or_str
    return value    # Instance of bytes
  • Python2,接受str或unicode,返回unicode的方法:
def to_unicode(unicode_or_str):
    if isinstance(unicode_or_str,str):
        value = unicode_or_str.decode('utf-8')
    else:
        value = unicode_or_str
    return value    # Instance of unicode
  • Python2,接受str或unicode,返回str的方法:
def to_str(unicode_or_str):
    if isinstance(unicode_or_str,unicode):
        value = unicode_or_str.encode('utf-8')
    else:
        value = unicode_or_str
    return value    # Instance of str

3 合理运用try/except/else/finally

Python的异常处理可能要考虑四种不同的时机,这些时机可以用try/except/else/finally块来表述。

  • finally
    如果既要将异常向上传播,又要在异常发生时执行清理工作,那么可以使用try/finally结构。
    这种结构常见的用途是确保程序能够可靠的关闭文件句柄。
handle = open('file.txt')
try:
    data = handle.read()
finally:
    handle.close()
  • else
    try/except/else结构可以清晰地描述出哪些异常会由自己的代码来处理、哪些异常会传播到上一级。
    如果try没有发生异常,那么执行else块。
    例如,从字符串中加载JSON字典数据,然后返回键值。
def load_json_key(data,key):
    try:
        result_dic = json.joads(data)       # May raise ValueError
    except ValueError as e:
        raise KeyError from e
    else:
        return result_dic[key]      # May raise KeyError
  • try/except/else/finally完整版
UNDEFNED = object()

def divide_json(path):
    handle = open(path,'r+')
    try:
        data = handle.read()
        op = data.json.loads(data)
        value = (
            op['numerator']/
            op['denominator'])
    except ZeroDivisionError as e:
        raise UNDEFNED
    else:
        op['result'] = value
        result = json.dumps(op)
        handle.seek(0)
        handle.write(result)
        return value
    finally:
        handle.close()

函数

4 用异常表示特殊情况,而不是返回None

假设对于除法,当结果没有定义,一般会返回None,如下

def divide(a,b):
    try:
        return a/b
    except ZeroDivisionError:
        return None

然而,当采用下面方式调用时,结果为0,就会出现错误:

x,y = 0,5
result = divide(x,y)
if not result:
    print('Invalid inputs')

因此,不要返回None,而是把异常抛给上一级:

def divide(a,b):
    try:
        return a/b
    except ZeroDivisionError as e:
        raise ValueError("Invalid inputs") from e

5 在闭包中使用外围作用域变量

下面是一个闭包例子:

def sort_priority(values,group):
    def helper(x):
        if x in group:
            return (0,x)
        return (1,x)
    values.sort(key=helper)

如输入:

numbers = [8,3,1,2,5,4,7,6]
group = {2,3,5,7}
sort_priority(numbers,group)
print(numbers)
>>>
[2,3,5,7,1,4,6,8]

对这个函数进行改进,使其能够返回是否出现优先级较高的元素的标志。

def sort_priority(values,group):
    found = False
    def helper(x):
        if x in group:
            found = True
            return (0,x)
        return (1,x)
    values.sort(key=helper)
    return found

然而,found的值永远是False,这是因为内部函数helpera重新定义了一个found,相当于局部变量。
那么如何使用外部变量呢?
python3可以使用nonlocal关键字,即在helper内部首先定义nonlocal found,然后found = True
python2可以使用列表,即在外围定义found = [False],在内部使用found[0] = True

6 使用生成器

功能需求:查出字符串中每个单词的首字母,在整个字符串里的位置。
我们很容易可以写出下面的代码:

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index,letter in enumerate(text):
        if letter = ' ':
            result.append(index+1)
    return result

该代码的问题是过于繁琐,另外,如果当文本比较大时,需要全部读取完文本后,才能返回result。
因此,使用生成器会更好。

def index_words_iter(text):
    if text:
        yield 0
    for index,letter in enumerate(text):
        if letter == ' ':
            yield index+1 

调用方式如下:
result = list(index_words_iter(address))

7 使用数量可变的位置参数

令函数接受可选的位置参数(如*arg),能够使代码更清晰。
例如,定义log函数:

def log(mes,val):
    if not val:
        print(mes)
    else:
        val_str = ','.join(str(x) for x in val)
        print('%s: %s'%(mes,val_str))
log('Hi there',[1,2])
log('Hi there',[])
>>>
Hi there: 1,2
Hi there

当val为空时,也需要传入[],这种写法并不好。如何改善呢?
只需要在第二个参数前加一个*,即:

def log(mes,*val):
    ...
log('Hi there')
>>>
Hi there

变长参数需要注意两个问题:

  1. 只有能够确定输入的参数个数比较少时,才应该令函数接受*arg的变长参数。
  2. 在给函数增加新的变长参数时,需要修改原来调用函数的旧代码。

8 用关键字参数来表达可选的行为

Python支持按照位置传递参数。
举例:

def remainder(number,divisor):
    return number % divisor

下面这些调用都是等效的:

  • remainder(20,7)
  • remainder(20,divisor = 7)
  • remainder(number = 20, divisor = 7)
  • remainder(divisor = 7, number = 20)
    需要注意的是,位置参数必须出现在关键字参数之前:
remainder(number = 20, 7)
>>>
SyntaxError: non-keyword arg after keyword arg

每个参数只能指定一次:

remainder(20, number = 7)
>>>
TypeError: remainder() got multiple values for argument 'number'

关键字参数的3个好处:

  • 使阅读代码的人更容易理解其含义。
  • 可以在函数定义中提供默认值。(如: def remainder(number,divisor=1))
  • 可以提供一种扩充函数参数的有效方式,使得扩充之后的函数依然能与原有的那些调用代码相兼容。
    如(def remainder(number,divisor=1, isFloat=True),调用时应该写成:remainder(22,divisor=1, isFloat=True),而不是:remainder(22,1,True))。

关键点总结:

  • 关键字参数能够阐明每个参数的意图。
  • 可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式指定。

9 用None和文档字符串描述具有动态默认值的参数

当我们想用一种非静态的类型,来做关键字参数的默认值时,需要注意参数的实际默认值如果是可变类型,则应该用None作为形式上的默认值。举例:

def log(mes,when=datetime.now()):
    print('%s: %s'%(when,mes))

log('hello!')
sleep(0.1)
log('hello!')

>>>
2021-04-25 13:10:12.232123: hello!
2021-04-25 13:10:12.232123: hello!

两条消息的时间戳一样,这是因为datetime.now只执行了一次。
参数的默认值,会在每个模块加载进来的时候求出,而很多模块都是在程序启动时加载的。包含这段代码的模块一旦加载进来,参数的默认值就固定不变了。
如何实现正确的动态默认值呢?改为以下方式即可:

def log(mes,when=None):
    when = datetime.now() if when is None else when
    print('%s: %s'%(when,mes))

log('hello!')
sleep(0.1)
log('hello!')
>>>
2021-04-25 13:10:12.232123: hello!
2021-04-25 13:10:12.425353: hello!

另一个例子:

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default
foo = decode('bad data')
foo['a'] = 5
abr = decode('bad too')
abr['b'] = 1
print('foo: ',foo)
print('bar: ',bar)

>>>
foo: {'a':5, 'b':1}
bar: {'a':5, 'b':1}

这里的foo和bar都使用的是写在default参数默认值中的那个字典。
如何解决呢?

def decode(data, default=None):
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

10 用只能以关键字形式指定的参数来确保代码清晰

python3如何实现只能以关键字来指定的参数?

def safe_division(number, divisor, *, ignore_overflow=False, ignore_zero_division=False)

safe_division(1, 10**500, True, False)

>>>
TypeError: safe_division() takes 2 positional arguments but 4 were given

safe_division(1, 10**500, ignore_zero_division=True)        # OK

python2如何实现只能以关键字来指定的参数?

def safe_division(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_division = kwargs.pop('ignore_zero_division', False)
    if kwargs:
        raise TypeError('Unexpected **kwargs: %r'%kwargs)
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lingpy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值