Python重构 - 第1部分

有时候回头看自己之前写的代码,觉得真的是不堪入目啊,哈哈哈,各种if嵌套,for循环一层又一层,代码过于冗长,代码看起来既不美观,又没啥可读性,甚至还影响运行速度。

所以对于编写简洁、易于理解、可读性好的程序还是很有必要的,接下来总结一些常见的重构方法,希望借此自己总结一下经验,也顺便给大家分享一下!

一、遇到多个if条件判断

太多的嵌套会使代码难以理解,在Python中尤其如此,因为Python中没有括号来帮助描述不同的嵌套级别。

阅读深层嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力减少嵌套,并且if可以同时使用两个条件and

  • 重构前
if a:
  if not b:
    return True 
  • 重构后
if a and not b:
  return True 

二、两个if条件中均需执行某程序

我们应该一直在寻找删除重复代码的方法。这也是一种提升代码很好的方法。

有时在条件的两个分支上都重复执行代码。这意味着代码将始终执行。可以将重复的行吊出条件行,并替换为单行。

  • 重构前
if a>5:
  ret = 5 * a
  result = f"SUM: {ret}"
else:
  ret = 10 * a
  result = f"SUM: {ret}"

这里就是删除重复项来达到重构的目的

  • 重构后
if a>5:
  ret = 5 * a
else:
  ret = 10 * a
result = f"SUM: {ret}"

三、将内部循环中的yield替换为yield from

如果还不了解yield的话,可以查阅《python中yield的用法详解——最简单,最清晰的解释》,个人觉得这个博主讲的很好,通俗易懂!
经常忽略的一个小窍门是 Python 的yield关键字有对应的为collections准备的yield from。因此无需使用 for 循环遍历集合。这使代码变短,并删除 for 中的额外变量。而且消除 for 循环后,yield from使程序运行效率提高约 15%。

  • 重构前
def get_content(entry):
    for block in entry.get_blocks():
        yield block
  • 重构后
def get_content(entry):
    yield from entry.get_blocks()

四、使用any函数代替for循环

一种常见的模式是,我们需要查找某个条件是否满足集合中一个或所有项目的条件。这可以通过如下的for循环来完成:

  • 重构前
found = False
for thing in things:
    if thing == other_thing:
        found = True
        break

更简洁的方式,清楚地表明了代码的意图,就是使用Python any()和all()内置函数。

  • 重构后
found = any(thing == other_thing for thing in things)

当至少有一个元素计算为 True 时,any() 将返回 True ,只有当所有元素都计算为 True 时,all() 将返回 True。

五、用[]替换list()

创建列表的最简洁方法是使用[]

x = []
x = ['first', 'second']

这样做有额外的优点:是一个很好的改进程序性能的方法。
以下是更改之前和之后的时间对比:

[root@docker]# python3 -m timeit "x = []"
10000000 loops, best of 3: 0.0234 usec per loop
[root@docker]# python3 -m timeit "x = list()"
10000000 loops, best of 3: 0.125 usec per loop

同样的原因和性能表现,使用{}替代dict()。

六、将重复执行的语句移出for/while循环

提升的另一种类型是将不变语句退出循环。如果一条语句只是设置了一些要在循环中使用的变量,则它不需要在其中。循环本质上很复杂,因此在编写循环时,您应该想到使循环更短,更容易理解。
在此示例中,city变量在循环内分配,但只能读取,不能更改。

  • 重构前
for building in buildings:
    city = 'London'
    addresses.append(building.street_address, city)
  • 重构后
city = 'London'
for building in buildings:
    addresses.append(building.street_address, city)

这也提高了性能-每次循环运行时,都会执行循环中的任何语句。由于只需要执行一次,因此浪费在这些多次执行上的时间被浪费了。如果语句涉及对数据库的调用或其他耗时的任务,则可以节省大量资金。

七、将for循环转换为列表/字典/集合理解

  • 重构前
cubes = []
for i in range(20):
    cubes.append(i ** 3)

我们创建列表,然后用值迭代地填充它-在这里,我们创建一个多维数据集列表,该列表以cubes变量结尾。
在Python中,我们可以访问列表推导。这些可以在一行上做相同的事情,消除了声明一个空列表然后附加到它的麻烦:

  • 重构后
cubes = [i ** 3 for i in range(20)]

我们已经将三行代码变成了一行,这是绝对的胜利-这意味着在阅读方法时来回滚动的次数更少,并有助于使事情易于管理。

将代码压缩到一行可能会使它更难以阅读,但是对于理解而言,并非如此。您需要的所有元素都会很好地呈现出来,一旦您习惯了语法,它实际上比for循环版本更具可读性。

另一点是,该分配现在更多地是原子操作-我们在声明什么cubes,而不是在说明如何构建它。这使代码看起来更像是一种叙述,因为向前看,我们将更关注的是什么,cubes而不是其构造的细节。

最终,理解通常比循环构建集合更快,这是考虑性能的另一个因素。

如果您需要更深入的理解,请在这里查看我的帖子。它准确地介绍了它们的工作方式,如何使用它们进行过滤以及字典和设置理解。

八、用+=优化自加

  • 重构前
a = a + 1
  • 重构后
a += 1

类似的还有-=,*=,/=和**=等用法!

九、仅使用一次的内联变量

我们在人们的代码中经常看到的东西是分配给结果变量,然后立即返回它。

  • 重构前
def state_attributes(self):
    """Return the state attributes."""
    state_attr = {
        'a': self.code_format,
        'b': self.changed_by,
    }
    return state_attr
  • 重构后
def state_attributes(self):
    """Return the state attributes."""
    return {
        'a': self.code_format,
        'b': self.changed_by,
    }

这样可以缩短代码并删除不必要的变量,从而减轻了读取函数的负担。

如果将中间变量用作参数或条件,则中间变量会很有用,并且该名称可以充当对变量表示的注释。在从函数返回它的情况下,函数名称可以告诉您结果是什么-在上面的示例中,它是状态属性,并且该state_attr名称未提供任何其他信息。

十、将if语句替换为if表达式

经常要根据某个程序状态将变量设置为两个不同值之一。

  • 重构前
if condition:
    x = 1
else:
    x = 2

可以使用Python的条件表达式语法(其三元运算符的版本)写在一行上:

  • 重构后
x = 1 if condition else 2

这绝对是更简洁的方法,但这是更具争议性的重构之一(连同列表理解)。一些编码人员不喜欢这些表达式,并且发现将它们解析起来要比完全写出来稍微难一些。

我们的观点是,只要条件表达式短并且适合一行,它就是一种改进。仅x定义了一条语句,而不是必须读取两条语句以及if-else行。与理解示例类似,当我们扫描代码时,我们通常不需要知道如何x分配的详细信息,而只需查看其正在分配并继续进行即可。

十一、生成器使用

一个技巧是类似的功能any,all并sum允许您传递生成器而不是集合。这意味着不要这样做:

  • 重构前
hat_found = any([is_hat(item) for item in wardrobe])

也可以这样写:

hat_found = any(is_hat(item) for item in wardrobe)

这样就去除了一对括号,使意图更加清晰。如果找到帽子,它也将立即返回,而不必构建整个列表。这种惰性评估可以提高性能。

请注意,实际上我们实际上是将生成器传递给any()如此严格的代码,如下所示:

hat_found = any((is_hat(item) for item in wardrobe))

但是Python允许您省略这对括号。

接受生成器的标准库函数是:

'all', 'any', 'enumerate', 'frozenset', 'list', 'max', 'min', 'set', 'sum', 'tuple'

十二、将条件简化为return语句

要查看的最后一个重构是我们到达方法结尾并想要返回True或的位置False。这样做的常见方法是这样的:

  • 重构前
def function():
    if isinstance(a, b) or issubclass(b, a):
        return True
    return False
  • 重构后
def function():
    return isinstance(a, b) or issubclass(b, a)

仅当表达式的计算结果为布尔值时,才可以这样做。在此示例中:

def any_hats():
    hats = [item for item in wardrobe if is_hat(item)]
    if hats or self.wearing_hat():
        return True
    return False

我们无法进行这种精确的重构,因为现在我们可以返回hats列表,而不是True 或者 False。为了确保我们返回的是布尔值,我们可以将返回值包装在对的调用中bool():

def any_hats():
    hats = [item for item in wardrobe if is_hat(item)]
    return bool(hats or self.wearing_hat())

十三、添加保护条款

深层嵌套的函数可能很难理解。在阅读它们时,您必须记住每个嵌套级别所适用的条件。鉴于没有括号来帮助定义条件块,这在Python中甚至更加困难。减少嵌套的一种简单方法是将条件转换为保护子句。

作为一个例子,让我们看一下这个函数:

  • 重构前
def should_i_wear_this_hat(self, hat):
    if isinstance(hat, Hat):
        current_fashion = FASHION_API.get_fashion(FASHION_TYPE.HAT)
        weather_outside = self.look_out_of_window()
        is_stylish = self.evaluate_style(hat, current_fashion)
        if weather_outside.is_raining:
            print("Damn.")
            return True
        else:
            print("Great.")
            return is_stylish
    else:
        return False

考虑到嵌套的两层,这很难解析。当我到达 else底部时,我需要if在顶部和顶部测试之间来回滑动一下,然后 才能了解发生了什么。此if 条件用于检查是否有边缘情况,其中传入的不是hat。由于我们只是False在这里返回,因此是引入保护子句的理想场所:

  • 重构后
def should_i_wear_this_hat(self, hat):
    if not isinstance(hat, Hat):
        return False

    current_fashion = get_fashion()
    weather_outside = self.look_out_of_window()
    is_stylish = self.evaluate_style(hat, current_fashion)
    if weather_outside.is_raining:
        print("Damn.")
        return True
    else:
        print("Great.")
        return is_stylish

我们通过反转条件并立即返回来添加保护子句。现在,在函数开始时会处理边缘情况,而我不必再为此担心。

减少了嵌套级别,该函数的其余部分现在更易于阅读。是否添加保护子句有时是主观的。对于真正简短的功能,可能不值得这样做。在功能更长或更复杂的地方,它通常是有用的工具。

十四、交换if / else以除去空的if主体

我们有时看到的一种模式是一种条件,其中主体不发生任何事情,所有动作都在else子句中。

  • 重构前
if location == OUTSIDE:
    pass
else:
    take_off_hat()

在这种情况下,我们可以通过交换主体和else周围区域来使代码更短,更简洁。我们必须确保反转条件,然后else子句中的逻辑移入主体。

  • 重构后
if location != OUTSIDE:
   take_off_hat()
else:
   pass

然后,我们有一个else不执行任何操作的子句,因此可以将其删除。

if location != OUTSIDE:
    take_off_hat()

十五、合并追加到列表

当声明一个列表并用值填充它时,一种自然而然的方法是将其声明为空然后追加到列表中。

  • 重构前
hats_i_own = []
hats_i_own.append('panama')
hats_i_own.append('baseball_cap')
hats_i_own.append('bowler')
  • 重构后
hats_i_own = ['panama', 'baseball_cap', 'bowler']

十六、将变量更接近其用法

局部变量的范围应始终尽可能严格地定义。
这意味着:

  • 您无需通过不需要的函数部分将变量保留在工作内存中。这减少了理解代码的认知负担。
  • 如果代码位于声明和一起使用变量的一致块中,则可以更轻松地将函数分开,这可以导致方法更短,更易于理解。
  • 如果将变量声明为离用法不远,它们可能会陷入困境。如果稍后更改或删除了使用它们的代码,则可以闲置一些未使用的变量,从而不必要地使代码复杂化。
    让我们再看一下前面的例子:
  • 重构前
def should_i_wear_this_hat(self, hat):
    if not isinstance(hat, Hat):
        return False

    current_fashion = get_fashion()
    weather_outside = self.look_out_of_window()
    is_stylish = self.evaluate_style(hat, current_fashion)
    if weather_outside.is_raining:
        print("Damn.")
        return True
    else:
        print("Great.")
        return is_stylish

味着我们也可以移动current_fashion仅在此处使用的 变量。您确实需要检查这些变量稍后是否在函数中不使用,如果函数简短易懂,这会更容易。

将分配移动到current_fashion还可避免在下雨天调用函数,如果调用成本很高,则可能会导致性能提高。

  • 重构后
def should_i_wear_this_hat(self, hat):
    if not isinstance(hat, Hat):
        return False

    weather_outside = self.look_out_of_window()
    if weather_outside.is_raining:
        print("Damn.")
        return True
    else:
        print("Great.")
        current_fashion = get_fashion()
        is_stylish = self.evaluate_style(hat, current_fashion)
        return is_stylish

实际上,我们可以再进一步一步并内联is_stylish变量。这说明了小型重构通常可以彼此建立并导致进一步的改进。

def should_i_wear_this_hat(self, hat):
    if not isinstance(hat, Hat):
        return False

    weather_outside = self.look_out_of_window()
    if weather_outside.is_raining:
        print("Damn.")
        return True
    else:
        print("Great.")
        current_fashion = get_fashion()
        return self.evaluate_style(hat, current_fashion)

十七、使用items() 直接解压缩字典值

遍历字典时,一个很好的提示是,您可以用来items()同时访问键和值。这使您可以转换此:

  • 重构前
hats_by_colour = {'blue': ['panama', 'baseball_cap']}
for hat_colour in hats_by_colour:
    hats = hats_by_colour[hat_colour]
    if hat_colour in self.favourite_colours:
        think_about_wearing(hats)
  • 重构后
hats_by_colour = {'blue': ['panama', 'baseball_cap']}
for hat_colour, hats in hats_by_colour.items():
    if hat_colour in self.favourite_colours:
        think_about_wearing(hats)

这节省了我们以前分配给的行hats,将其合并到for循环中。现在,该代码可以更自然地读取,并且无需重复操作。

十八、简化序列比较

我们经常做的事情是在尝试对列表或序列进行操作之前检查列表或序列是否包含元素。

  • 重构前
if len(list_of_hats) > 0:
    hat_to_wear = choose_hat(list_of_hats)

使用Python的方法是仅使用Python列表和序列求值True是否具有元素的事实,False否则使用。

这意味着我们可以更简单地将上面的代码编写为:

  • 重构后
if list_of_hats:
    hat_to_wear = choose_hat(list_of_hats)

这样做是惯例,请参阅Python的PEP8样式指南。一旦您习惯了这种方式,它确实使代码更易于阅读,并且变得更加混乱

十九、合并有条件的重复块

我们应该始终在寻找删除重复代码的机会。这样做的一个好地方是if…elif 链中有多个相同的块。

  • 重构前
def process_payment(payment):
    if payment.currency == 'USD':
        process_standard_payment(payment)
    elif payment.currency == 'EUR':
        process_standard_payment(payment)
    else:
        process_international_payment(payment)
  • 重构后
def process_payment(payment):
    if payment.currency == 'USD' or payment.currency == 'EUR':
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

现在,如果需要更改process_standard_payment(payment)线,我们可以在一个地方而不是两个地方进行。如果这些块涉及多行,这将变得更加重要。

二十、用in运算符替换同一变量的多个比较

同样是上个优化后的程序

  • 重构前
def process_payment(payment):
    if payment.currency == 'USD' or payment.currency == 'EUR':
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

通过使用in运算符并将要比较的值移动到集合中,我们可以简化操作。

  • 重构后
def process_payment(payment):
    if payment.currency in ['USD', 'EUR']:
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

这避免了一点重复,现在可以一目了然地理解和理解条件了。

二十一、使用或简化表达式

在这里,我们发现自己设置了一个值,如果它的值为True,则使用默认值。

可以写成如下形式:

  • 重构前
if input_currency:
    currency = input_currency
else:
    currency = DEFAULT_CURRENCY

或使用if表达式:

  • 重构方案1
currency = input_currency if input_currency else DEFAULT_CURRENCY

两者都可以简化为以下内容,这更易于阅读并且避免了的重复input_currency。

  • 重构方案2
currency = input_currency or DEFAULT_CURRENCY

之所以有效,是因为首先评估了左侧。如果计算结果为True则将currency设置为此,并且不会评估右侧。如果求值到False右侧,将求值currency并将其设置为DEFAULT_CURRENCY。

二十二、用直接引用替换for循环中的索引

在Python中常用于循环的一种模式是用于range(len(list))生成一系列可以迭代的数字。

  • 重构前
for i in range(len(currencies)):
    print(currencies[i])

如果i仅使用索引进行列表访问,则可以通过直接遍历列表来改进此代码,如以下示例所示。

  • 重构后
for currency in currencies:
    print(currency)

这段代码更容易理解,而且也很混乱。特别是能够使用有意义的名称来currency大大提高可读性。

二十三、删除不必要的keys()调用

有时,当遍历字典时,您只需要使用字典键即可。

  • 重构前
for currency in currencies.keys():
    process(currency)

在这种情况下,keys()不需要进行调用,因为遍历字典时的默认行为是遍历键。

  • 重构后
for currency in currencies:
    process(currency)

现在,该代码稍微更简洁,更易于阅读,并且避免调用函数会带来(小的)性能改进。

二十四、将手动循环计数器替换为要枚举的调用

遍历列表时,有时需要访问循环计数器,该计数器将使您知道要使用的元素的索引。像这样:

  • 重构前
i = 0
for currency in currencies:
    print(i, currency)
    i += 1

但是,有一个内置的Python函数,enumerate可让您直接生成索引

  • 重构后
for i, currency in enumerate(currencies):
    print(i, currency)

enumerate函数还有一个特点是还可以自己指定索引起始位置:

for i, currency in enumerate(currencies, 2):
    print(i, currency)

阅读此内容时,我们不必担心i变量的记账方式,让我们专注于真正重要的代码。

参考链接:
https://sourcery.ai/blog/

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值