68

日常 Python 编程优雅之道

 5 years ago
source link: http://www.10tiao.com/html/263/201807/2652567988/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(点击上方蓝字,快速关注我们)


编译:linux中国 - MjSeven  英文:Nina Zakharenko

https://linux.cn/article-9856-1.html?utm_source=weixin


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

魔术方法

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

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

让我们创建一个 Money 类来示例:

  1. classMoney:

  2.    currency_rates = {

  3.        '$': 1,

  4.        '€': 0.88,

  5.    }

  6.    def __init__(self, symbol, amount):

  7.        self.symbol = symbol

  8.        self.amount = amount

  9.    def __repr__(self):

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

  11.    def convert(self, other):

  12.        """ Convert other amount to our currency """

  13.        new_amount = (

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

  15.            * self.currency_rates[self.symbol])

  16.        returnMoney(self.symbol, new_amount)

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

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

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

  2. >>> soda_cost

  3.    $5.25

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

  5. >>> pizza_cost

  6.    €7.99

我们可以使用魔术方法使得这个类的实例之间可以相互交互。假设我们希望能够将这个类的两个实例一起加在一起,即使它们是不同的货币。为了实现这一点,我们可以在 Money 类上实现 __add__ 这个魔术方法:

  1. classMoney:

  2.    # ... previously defined methods ...

  3.    def __add__(self, other):

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

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

  6.        returnMoney(self.symbol, new_amount)

现在我们可以以非常直观的方式使用这个类:

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

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

  3. >>> soda_cost + pizza_cost

  4.    $14.33

  5. >>> pizza_cost + soda_cost

  6.    €12.61

当我们将两个实例加在一起时,我们得到以第一个定义的货币符号所表示的结果。所有的转换都是在底层无缝完成的。如果我们想的话,我们也可以为减法实现 __sub__,为乘法实现 __mul__ 等等。阅读模拟数字类型[1]魔术方法指南[2]来获得更多信息。

我们学习到 __add__ 映射到内置运算符 +。其他魔术方法可以映射到像 [] 这样的符号。例如,在字典中通过索引或键来获得一项,其实是使用了 __getitem__ 方法:

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

  2. >>> d['two']

  3. 2

  4. >>> d.__getitem__('two')

  5. 2

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

  1. classAlphabet:

  2.    letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

  3.    def __len__(self):

  4.        return len(self.letters)

  5. >>> my_alphabet = Alphabet()

  6. >>> len(my_alphabet)

  7.    26

自定义迭代器

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

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

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

  2.          print(food + '. Yum!')

  3. Pizza. Yum!

  4. Fries. Yum!

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

◈ 要成为一个可迭代对象,一个类需要实现  __iter__() ◈  __iter__()  方法需要返回一个迭代器 ◈ 要成为一个迭代器,一个类需要实现  __next__() (或在 Python 2[3]中是  next() ),当没有更多的项要迭代时,必须抛出一个  StopIteration  异常。

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

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

  1. classServer:

  2.    services = [

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

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

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

  6.    ]

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

  1. classIterableServer:

  2.     def __init__(self):

  3.         self.current_pos = 0

  4.     def __next__(self):

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

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

  1. classIterableServer:

  2.     def __init__(self):

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

  4.     def __iter__(self):  # 我们可以在这里返回 self,因为实现了 __next__

  5.         returnself

  6.     def __next__(self):

  7.         whileself.current_pos < len(self.services):

  8.             service = self.services[self.current_pos]

  9.             self.current_pos += 1

  10.             if service['active']:

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

  12.         raiseStopIteration

  13.     next = __next__  # 可选的 Python2 兼容性

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

因为我们实现了 __next__() 方法,当它耗尽时,它会抛出 StopIteration。我们可以从 __iter__() 返回 self,因为 IterableServer 类遵循 iterable 协议。

现在我们可以遍历一个 IterableServer 实例,这将允许我们查看每个处于活动的服务,如下所示:

  1. >>> for protocol, port inIterableServer():

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

  3. service sshis running on port 22

  4. service http is running on port 21

太棒了,但我们可以做得更好!在这样类似的实例中,我们的迭代器不需要维护大量的状态,我们可以简化代码并使用 generator(生成器)[4] 来代替。

  1. classServer:

  2.     services = [

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

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

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

  6.     ]

  7.     def __iter__(self):

  8.         for service inself.services:

  9.             if service['active']:

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

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

这是因为一些内置的 Python 魔法。在 Python 关于 __iter__() 的文档[6]中我们可以看到,如果 __iter__() 是作为一个生成器实现的,它将自动返回一个迭代器对象,该对象提供 __iter__() 和 __next__() 方法。阅读这篇很棒的文章,深入了解迭代器,可迭代对象和生成器[7]

方法魔法

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

其中一个例子是别名功能。因为函数只是对象,所以我们可以将它们赋值给多个变量。例如:

  1. >>> def foo():

  2.        return'foo'

  3. >>> foo()

  4. 'foo'

  5. >>> bar = foo

  6. >>> bar()

  7. 'foo'

我们稍后会看到它的作用。

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

  1. >>> classDog:

  2.        sound = 'Bark'

  3.        def speak(self):

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

  5. >>> fido = Dog()

  6. >>> fido.sound

  7. 'Bark'

  8. >>> getattr(fido, 'sound')

  9. 'Bark'

  10. >>> fido.speak

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

  12. >>> getattr(fido, 'speak')

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

  14. >>> fido.speak()

  15. Bark! Bark!

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

  17. >>> speak_method()

  18. Bark! Bark!

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

  1. classOperations:

  2.     def say_hi(self, name):

  3.         print('Hello,', name)

  4.     def say_bye(self, name):

  5.         print ('Goodbye,', name)

  6.     defdefault(self, arg):

  7.         print ('This operation is not supported.')

  8. if __name__ == '__main__':

  9.     operations = Operations()

  10.     # 假设我们做了错误处理

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

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

  13.     func_to_call(argument)

脚本的输出是:

  1. $ python getattr.py

  2. > say_hi Nina

  3. Hello, Nina

  4. > blah blah

  5. This operation isnot supported.

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

  1. >>> from functools import partial

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

  3. >>> basetwo

  4. <functools.partial object at 0x1085a09f0>

  5. >>> basetwo('10010')

  6. 18

  7. # 这等同于

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

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

以下是我们如何为 GitHub API 和任何其他相关连接属性定义端点 URL 的简化版本。在这里查看完整代码[11]

  1. classGitHub(API):

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

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

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

  5.         self.setConnectionProperties(props)

然后,一旦配置了访问令牌[12],就可以开始使用 GitHub API[13]

  1. >>> gh = GitHub('token')

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

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

  4. >>> data

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

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

  1. class API:

  2.     # ... other methods ...

  3.     def __getattr__(self, key):

  4.         returnIncompleteRequest(self.client).__getattr__(key)

  5.     __getitem__ = __getattr__

在 API 类上的每次调用都会调用 IncompleteRequest 类[15]作为指定的 key

  1. classIncompleteRequest:

  2.     # ... other methods ...

  3.     def __getattr__(self, key):

  4.         if key inself.client.http_methods:

  5.             htmlMethod = getattr(self.client, key)

  6.             return partial(htmlMethod, url=self.url)

  7.         else:

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

  9.             returnself

  10.     __getitem__ = __getattr__

  11. classClient:

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

  13.     defget(self, url, headers={}, **params):

  14.         returnself.request('GET', url, None, headers)

如果最后一次调用不是 HTTP 方法(如 getpost 等),则返回带有附加路径的 IncompleteRequest。否则,它从Client 类[16]获取 HTTP 方法对应的正确函数,并返回 partial

如果我们给出一个不存在的路径会发生什么?

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

  2. >>> status

  3. ... 404

因为 __getattr__ 别名为 __getitem__

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

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

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

  4. >>> data

  5. .... # {....}

这真心是一些方法魔术!

了解更多

Python 提供了大量工具,使你的代码更优雅,更易于阅读和理解。挑战在于找到合适的工具来完成工作,但我希望本文为你的工具箱添加了一些新工具。而且,如果你想更进一步,你可以在我的博客 nnja.io[17] 上阅读有关装饰器、上下文管理器、上下文生成器和命名元组的内容。随着你成为一名更好的 Python 开发人员,我鼓励你到那里阅读一些设计良好的项目的源代码。Requests[18] 和 Flask[19] 是两个很好的起步的代码库。


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】
《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/


③ 最后请附上您的个人简介哈~

看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK