平常 Python 编程优雅之道


平常 Python 编程优雅之道


编译自: https://opensource.com/article/18/4/elegant-solutions-everyday-python-problemshtml

做者: Nina Zakharenkopython

译者: MjSevengit

3 个可使你的 Python 代码更优雅、可读、直观和易于维护的工具。github

Python 提供了一组独特的工具和语言特性来使你的代码更加优雅、可读和直观。为正确的问题选择合适的工具,你的代码将更易于维护。在本文中,咱们将研究其中的三个工具:魔术方法、迭代器和生成器,以及方法魔术。web

魔术方法

魔术方法能够看做是 Python 的管道。它们被称为“底层”方法,用于某些内置的方法、符号和操做。你可能熟悉的常见魔术方法是 __init__(),当咱们想要初始化一个类的新实例时,它会被调用。shell

你可能已经看过其余常见的魔术方法,如 __str__ 和 __repr__。Python 中有一整套魔术方法,经过实现其中的一些方法,咱们能够修改一个对象的行为,甚至使其行为相似于内置数据类型,例如数字、列表或字典。编程

让咱们建立一个 Money 类来示例:api

class Money:ssh

currency_rates = {ide

'$': 1,

'€': 0.88,

}

def __init__(self, symbol, amount):

self.symbol = symbol

self.amount = amount

def __repr__(self):

return '%s%.2f' % (self.symbol, self.amount)

def convert(self, other):

""" Convert other amount to our currency """

new_amount = (

other.amount / self.currency_rates[other.symbol]

* self.currency_rates[self.symbol])

return Money(self.symbol, new_amount)

该类定义为给定的货币符号和汇率定义了一个货币汇率,指定了一个初始化器(也称为构造函数),并实现 __repr__,所以当咱们打印这个类时,咱们会看到一个友好的表示,例如 $2.00 ,这是一个带有货币符号和金额的 Money('$', 2.00) 实例。最重要的是,它定义了一种方法,容许你使用不一样的汇率在不一样的货币之间进行转换。

打开 Python shell,假设咱们已经定义了使用两种不一样货币的食品的成本,以下所示:

>>> soda_cost = Money('$', 5.25)

>>> soda_cost

$5.25

>>> pizza_cost = Money('€', 7.99)

>>> pizza_cost

€7.99

咱们可使用魔术方法使得这个类的实例之间能够相互交互。假设咱们但愿可以将这个类的两个实例一块儿加在一块儿,即便它们是不一样的货币。为了实现这一点,咱们能够在 Money 类上实现 __add__ 这个魔术方法:

class Money:

# ... previously defined methods ...

def __add__(self, other):

""" Add 2 Money instances using '+' """

new_amount = self.amount + self.convert(other).amount

return Money(self.symbol, new_amount)

如今咱们能够以很是直观的方式使用这个类:

>>> soda_cost = Money('$', 5.25)

>>> pizza_cost = Money('€', 7.99)

>>> soda_cost + pizza_cost

$14.33

>>> pizza_cost + soda_cost

€12.61

当咱们将两个实例加在一块儿时,咱们获得以第一个定义的货币符号所表示的结果。全部的转换都是在底层无缝完成的。若是咱们想的话,咱们也能够为减法实现 __sub__,为乘法实现 __mul__ 等等。阅读 模拟数字类型 或 魔术方法指南 来得到更多信息。

咱们学习到 __add__ 映射到内置运算符 +。其余魔术方法能够映射到像 [] 这样的符号。例如,在字典中经过索引或键来得到一项,实际上是使用了 __getitem__ 方法:

>>> d = {'one': 1, 'two': 2}

>>> d['two']

2

>>> d.__getitem__('two')

2

一些魔术方法甚至映射到内置函数,例如 __len__() 映射到 len()。

class Alphabet:

letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def __len__(self):

return len(self.letters)

>>> my_alphabet = Alphabet()

>>> len(my_alphabet)

26

自定义迭代器

对于新的和经验丰富的 Python 开发者来讲,自定义迭代器是一个很是强大的但使人迷惑的主题。

许多内置类型,例如列表、集合和字典,已经实现了容许它们在底层迭代的协议。这使咱们能够轻松地遍历它们。

>>> for food in ['Pizza', 'Fries']:

print(food + '. Yum!')

Pizza. Yum!

Fries. Yum!

咱们如何迭代咱们本身的自定义类?首先,让咱们来澄清一些术语。

  • 要成为一个可迭代对象,一个类须要实现 __iter__()

  • __iter__() 方法须要返回一个迭代器

  • 要成为一个迭代器,一个类须要实现 __next__()(或 在 Python 2 中是 next()),当没有更多的项要迭代时,必须抛出一个 StopIteration 异常。

呼!这听起来很复杂,可是一旦你记住了这些基本概念,你就能够在任什么时候候进行迭代。

咱们何时想使用自定义迭代器?让咱们想象一个场景,咱们有一个 Server 实例在不一样的端口上运行不一样的服务,如 http 和 ssh。其中一些服务处于 active 状态,而其余服务则处于 inactive 状态。

class Server:

services = [

{'active': False, 'protocol': 'ftp', 'port': 21},

{'active': True, 'protocol': 'ssh', 'port': 22},

{'active': True, 'protocol': 'http', 'port': 80},

]

当咱们遍历 Server 实例时,咱们只想遍历那些处于 active 的服务。让咱们建立一个 IterableServer 类:

class IterableServer:

def __init__(self):

self.current_pos = 0

def __next__(self):

pass # TODO: 实现并记得抛出 StopIteration

首先,咱们将当前位置初始化为 0。而后,咱们定义一个 __next__() 方法来返回下一项。咱们还将确保在没有更多项返回时抛出 StopIteration。到目前为止都很好!如今,让咱们实现这个 __next__() 方法。

class IterableServer:

def __init__(self):

self.current_pos = 0. # 咱们初始化当前位置为 0

def __iter__(self): # 咱们能够在这里返回 self,由于实现了 __next__

return self

def __next__(self):

while self.current_pos < len(self.services):

service = self.services[self.current_pos]

self.current_pos += 1

if service['active']:

return service['protocol'], service['port']

raise StopIteration

next = __next__ # 可选的 Python2 兼容性

咱们对列表中的服务进行遍历,而当前的位置小于服务的个数,但只有在服务处于活动状态时才返回。一旦咱们遍历完服务,就会抛出一个 StopIteration 异常。

由于咱们实现了 __next__() 方法,当它耗尽时,它会抛出 StopIteration。咱们能够从 __iter__() 返回 self,由于 IterableServer 类遵循 iterable 协议。

如今咱们能够遍历一个 IterableServer 实例,这将容许咱们查看每一个处于活动的服务,以下所示:

>>> for protocol, port in IterableServer():

print('service %s is running on port %d' % (protocol, port))

service ssh is running on port 22

service http is running on port 21

太棒了,但咱们能够作得更好!在这样相似的实例中,咱们的迭代器不须要维护大量的状态,咱们能够简化代码并使用 generator(生成器) 来代替。

class Server:

services = [

{'active': False, 'protocol': 'ftp', 'port': 21},

{'active': True, 'protocol': 'ssh', 'port': 22},

{'active': True, 'protocol': 'http', 'port': 21},

]

def __iter__(self):

for service in self.services:

if service['active']:

yield service['protocol'], service['port']

yield 关键字究竟是什么?在定义生成器函数时使用 yield。这有点像 return,虽然 return 在返回值后退出函数,但 yield 会暂停执行直到下次调用它。这容许你的生成器的功能在它恢复以前保持状态。查看 yield 的文档 以了解更多信息。使用生成器,咱们没必要经过记住咱们的位置来手动维护状态。生成器只知道两件事:它如今须要作什么以及计算下一个项目须要作什么。一旦咱们到达执行点,即 yield 再也不被调用,咱们就知道中止迭代。

这是由于一些内置的 Python 魔法。在 Python 关于 __iter__() 的文档 中咱们能够看到,若是 __iter__() 是做为一个生成器实现的,它将自动返回一个迭代器对象,该对象提供 __iter__() 和 __next__() 方法。阅读这篇很棒的文章,深刻了解 迭代器,可迭代对象和生成器 。

方法魔法

因为其独特的方面,Python 提供了一些有趣的方法魔法做为语言的一部分。

其中一个例子是别名功能。由于函数只是对象,因此咱们能够将它们赋值给多个变量。例如:

>>> def foo():

return 'foo'

>>> foo()

'foo'

>>> bar = foo

>>> bar()

'foo'

咱们稍后会看到它的做用。

Python 提供了一个方便的内置函数 称为 getattr() ,它接受 object, name, default 参数并在 object 上返回属性 name。这种编程方式容许咱们访问实例变量和方法。例如:

>>> class Dog:

sound = 'Bark'

def speak(self):

print(self.sound + '!', self.sound + '!')

>>> fido = Dog()

>>> fido.sound

'Bark'

>>> getattr(fido, 'sound')

'Bark'

>>> fido.speak

<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>

>>> getattr(fido, 'speak')

<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>

>>> fido.speak()

Bark! Bark!

>>> speak_method = getattr(fido, 'speak')

>>> speak_method()

Bark! Bark!

这是一个很酷的技巧,可是咱们如何在实际中使用 getattr 呢?让咱们看一个例子,咱们编写一个小型命令行工具来动态处理命令。

class Operations:

def say_hi(self, name):

print('Hello,', name)

def say_bye(self, name):

print ('Goodbye,', name)

def default(self, arg):

print ('This operation is not supported.')

if __name__ == '__main__':

operations = Operations()

# 假设咱们作了错误处理

command, argument = input('> ').split()

func_to_call = getattr(operations, command, operations.default)

func_to_call(argument)

脚本的输出是:

$ python getattr.py

> say_hi Nina

Hello, Nina

> blah blah

This operation is not supported.

接下来,咱们来看看 partial。例如,functool.partial(func, *args, **kwargs) 容许你返回一个新的 partial 对象 ,它的行为相似 func,参数是 args 和 kwargs。若是传入更多的 args,它们会被附加到 args。若是传入更多的 kwargs,它们会扩展并覆盖 kwargs。让咱们经过一个简短的例子来看看:

>>> from functools import partial

>>> basetwo = partial(int, base=2)

>>> basetwo

<functools.partial object at 0x1085a09f0>

>>> basetwo('10010')

18

# 这等同于

>>> int('10010', base=2)

让咱们看看在我喜欢的一个 名为 agithub 的库中的一些示例代码中,这个方法魔术是如何结合在一块儿的,这是一个(名字起得很 low 的) REST API 客户端,它具备透明的语法,容许你以最小的配置快速构建任何 REST API 原型(不只仅是 GitHub)。我发现这个项目颇有趣,由于它很是强大,但只有大约 400 行 Python 代码。你能够在大约 30 行配置代码中添加对任何 REST API 的支持。agithub 知道协议所需的一切(REST、HTTP、TCP),但它不考虑上游 API。让咱们深刻到它的实现中。

如下是咱们如何为 GitHub API 和任何其余相关链接属性定义端点 URL 的简化版本。在这里查看 完整代码 。

class GitHub(API):

def __init__(self, token=None, *args, **kwargs):

props = ConnectionProperties(api_url = kwargs.pop('api_url', 'api.github.com'))

self.setClient(Client(*args, **kwargs))

self.setConnectionProperties(props)

而后,一旦配置了 访问令牌 ,就能够开始使用 GitHub API 。

>>> gh = GitHub('token')

>>> status, data = gh.user.repos.get(visibility='public', sort='created')

>>> # ^ 映射到 GET /user/repos

>>> data

... ['tweeter', 'snipey', '...']

请注意,你要确保 URL 拼写正确,由于咱们没有验证 URL。若是 URL 不存在或出现了其余任何错误,将返回 API 抛出的错误。那么,这一切是如何运做的呢?让咱们找出答案。首先,咱们将查看一个 API 类 的简化示例:

class API:

# ... other methods ...

def __getattr__(self, key):

return IncompleteRequest(self.client).__getattr__(key)

__getitem__ = __getattr__

在 API 类上的每次调用都会调用 IncompleteRequest 类 做为指定的 key。

class IncompleteRequest:

# ... other methods ...

def __getattr__(self, key):

if key in self.client.http_methods:

htmlMethod = getattr(self.client, key)

return partial(htmlMethod, url=self.url)

else:

self.url += '/' + str(key)

return self

__getitem__ = __getattr__

class Client:

http_methods = ('get') # 还有 post, put, patch 等等。

def get(self, url, headers={}, **params):

return self.request('GET', url, None, headers)

若是最后一次调用不是 HTTP 方法(如 get、post 等),则返回带有附加路径的 IncompleteRequest。不然,它从 Client 类 获取 HTTP 方法对应的正确函数,并返回 partial。

若是咱们给出一个不存在的路径会发生什么?

>>> status, data = this.path.doesnt.exist.get()

>>> status

... 404

由于 __getattr__ 别名为 __getitem__:

>>> owner, repo = 'nnja', 'tweeter'

>>> status, data = gh.repos[owner][repo].pulls.get()

>>> # ^ Maps to GET /repos/nnja/tweeter/pulls

>>> data

.... # {....}

这真心是一些方法魔术!

相关文章
相关标签/搜索