如何写出Pythonic风格的代码

如何写出Pythonic风格的代码

索引

>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[0]
4
>>> my_numbers[1]
5
>>> my_numbers[-1]
9
>>> my_numbers[-2]
3

切片

>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[0:2]
(4, 5)
>>> my_numbers[0:3:2]  # 步长为可选参数
(4, 3)
>>> my_numbers[0:-2]  # 只能从左往右
(4, 5)
>>> my_numbers[0:-1:2]
(4, 3)
>>> my_numbers[2:]  # 可以缺省起始和终止索引
(3, 9)
>>> my_numbers[:2]
(4, 5)
>>> my_numbers[:]  # 相当于浅拷贝
(4, 5, 3, 9)
>>> my_numbers[::]
(4, 5, 3, 9)
>>> interval = slice(0, 3, 2)
>>> my_numbers[interval]  # => my_numbers[0:3:2]
(4, 3)
>>> interval = slice(None, 3)
>>> my_numbers[interval] == my_numbers[:3]
True

创建自定义序列类型

class Items:
    def __init__(self, *values):
        self._values = list(values)
        
    def __len__(self):
        return len(self._values)
        
    def __getitem__(self, item):
        return self._values.__getitem__(item)

上面的例子封装了内置的list结构。另外一种是使用继承,通过拓展基类collections.UserList。

当你需要完完全全实现自己的序列结构时,请牢记两点:

  1. 按照范围切片时,结果应该是相同类型的实例序列。
  2. 在切片提供的范围内,请尊重Python使用的语义,排除最后的元素。

另外range也可以进行切片

>>> range(1, 100)[25:50]
range(26, 51)

上下文管理

上下文管理器是Python提供的独特功能,它十分有用,非常适合这样的模式:运行一些代码,需要前置和后置条件。例如处理文件:open->处理->close。

fd = open(filename)
try:
    process_file(fd)
finally:
    fd.close()

使用with

with open(filename) as fd:
    process_file(fd)

上下文管理器依赖两个魔法方法__enter__和__exit__,分别对应前置和后置处理逻辑。

通过类实现上下文管理器

def stop_database():
    print("systemctl stop postgresql.service")
    
def start_database():
    print("systemctl start postgresql.service")
    
def db_backup():
    print("pg_dump database")

class DBHandler:
    def __enter__(self):
        stop_database()
        return self  # "with xxx as obj" obj is self here.
        
    def __exit__(self, exc_type, ex_value, ex_traceback):
        start_database()
        
def main():
    with DBHandler():
        db_backup()

通过方法实现上下文管理器,需要使用contextlib.contextmanager装饰器

import contextlib

@contextlib.contextmanager
def db_handler():
    stop_database()
    yield  # "with xxx as obj" obj is "yield {obj}".
    start_database()

with db_handler():
    db_backup()

甚至连with也不用,需要使用contextlib.ContextDecorator基类

import contextlib

class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()

    def __exit__(self, ext_type, ex_value, ex_traceback):
        start_database()

@dbhandler_decorator()
def offline_backup():
    print("pg_dump database")

但是这种用法你没法使用__enter__函数的返回值

使用上下文管理器处理异常

import contextlib

with contextlib.suppress(DataConversionException):
    parse_data(input_json_or_dict)

如果引发DataConversionException,直接跳过。

属性

单下划线表示只会从内部访问(保护)的属性和方法。对象只应该公开那些外部调用者关心的属性和方法,也就是接口,一切不是严格属于接口的都应该使用单下划线。

双下划线并非定义私有变量的方式,访问双下划线会导致AttributeError,这是因为双下划线会导致重命名:

class Connector:
    def __init__(self):
        self.__timeout = 60  # ___
    
    def __run(self):
        print('Run')

conn = Connector()
print(conn.__timeout)  # raise AttributeError
print(conn._Connector__timeout)  # 60
print(conn.__run)  # raise AttributeError
print(conn._Connector__run)  # function

双下划线是非Pythonic方法。如果您需要将属性定义为私有的,使用单个下划线,并遵守Pythonic约定,即它是私有的属性。

使用@properties和@xxxx.setter装饰器

class User:
    def __init__(self, username):
        self.username = username
        self._email = None

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, new_email):
        self._email = new_email

迭代对象

python确定对象是否可迭代的,主要依据两点:

  1. 是否实现了__next__或__iter__迭代方法
  2. 是否是一个序列,实现了__len__和__getitem__

python会依照顺序尝试调用这些方法来迭代对象

class A:
    def __init__(self, count=0):
        self._count = count

    def __iter__(self):
        return self

    def __next__(self):
        if self._count >= 5:
            raise StopIteration
        self._count += 1
        return self._count

class B:
    def __iter__(self):
        return A(1)

a = list(A())
b = list(B())
print(a)  # [1, 2, 3, 4, 5]
print(b)  # [2, 3, 4, 5]

调用__iter__返回的值再调用__next__

或者__iter__直接返回生成器

from datetime import date, timedelta

class DateRangeContainerIterable:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date

    def __iter__(self):
        current_day = self.start_date
        while current_day < self.end_date:
            yield current_day
            current_day += timedelta(days=1)

d = DateRangeContainerIterable(date(2018, 1, 1), date(2018, 1, 5))
print(", ".join(map(str, d)))  # '2018-01-01, 2018-01-02, 2018-01-03, 2018-01-04'

包含对象

flag = element in container
# => flag = container.__contains__(element)

动态获取属性

python调用存在的属性会调用__getattribute__,当属性不存在时,会调用__getattr__。

可调用对象

当我们调用一个对象object(*args, *kwargs)时,实际上转换为了object.__call__(args, **kwargs)

from collections import defaultdict

class CallCount:
    def __init__(self):
        self._counts = defaultdict(int)

    def __call__(self, argument):
        self._counts[argument] += 1
        return self._counts[argument]

cc = CallCount()
print(cc(1))  # 1
print(cc(2))  # 1
print(cc(1))  # 2
print(cc(1))  # 3
print(cc(3))  # 1

魔法方法总结

[图片上传失败...(image-bd2aa1-1571013774916)]

Python注意事项

不要将引用类型的对象作为函数的默认参数,否则结果不可预期

def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}):
    name = user_metadata.pop("name")
    age = user_metadata.pop("age")
    return f"{name} ({age})"

print(wrong_user_display())  # 'John (30)
print(wrong_user_display({"name": "Jane", "age": 25}))  # John (25)
print(wrong_user_display())  # raise KeyError: 'name'

拓展内置数据类型

拓展内置数据类型的正确方式是使用collections模块

你可能感兴趣的:(如何写出Pythonic风格的代码)