日常 Python 编程优雅之道
Python

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


![](/data/attachment/album/201807/22/144349jn000yv7w5m7nk9v.jpg)


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


### 魔术方法


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


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


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



```
class Money:

    currency_rates = {
        '$': 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__` 等等。阅读[模拟数字类型](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types)或[魔术方法指南](https://rszalski.github.io/magicmethods/)来获得更多信息。


我们学习到 `__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](https://docs.python.org/2/library/stdtypes.html#iterator.next)中是 `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（生成器）](https://docs.python.org/3/library/stdtypes.html#generator-types) 来代替。



```
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 的文档](https://docs.python.org/3/reference/expressions.html#yieldexpr)以了解更多信息。使用生成器，我们不必通过记住我们的位置来手动维护状态。生成器只知道两件事：它现在需要做什么以及计算下一个项目需要做什么。一旦我们到达执行点，即 `yield` 不再被调用，我们就知道停止迭代。


这是因为一些内置的 Python 魔法。在 [Python 关于 `__iter__()` 的文档](https://docs.python.org/3/reference/datamodel.html#object.__iter__)中我们可以看到，如果 `__iter__()` 是作为一个生成器实现的，它将自动返回一个迭代器对象，该对象提供 `__iter__()` 和 `__next__()` 方法。阅读这篇很棒的文章，深入了解[迭代器，可迭代对象和生成器](http://nvie.com/posts/iterators-vs-generators/)。


### 方法魔法


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


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



```
>>> def foo():
       return 'foo'
>>> foo()
'foo'
>>> bar = foo
>>> bar()
'foo'

```

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


Python 提供了一个方便的内置函数[称为 `getattr()`](https://docs.python.org/3/library/functions.html#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 对象](https://docs.python.org/3/library/functools.html#functools.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`](https://github.com/mozilla/agithub) 的库中的一些示例代码中，这个方法魔术是如何结合在一起的，这是一个（名字起得很 low 的） REST API 客户端，它具有透明的语法，允许你以最小的配置快速构建任何 REST API 原型（不仅仅是 GitHub）。我发现这个项目很有趣，因为它非常强大，但只有大约 400 行 Python 代码。你可以在大约 30 行配置代码中添加对任何 REST API 的支持。`agithub` 知道协议所需的一切（`REST`、`HTTP`、`TCP`），但它不考虑上游 API。让我们深入到它的实现中。


以下是我们如何为 GitHub API 和任何其他相关连接属性定义端点 URL 的简化版本。在这里查看[完整代码](https://github.com/mozilla/agithub/blob/master/agithub/GitHub.py)。



```
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)

```

然后，一旦配置了[访问令牌](https://github.com/settings/tokens)，就可以开始使用 [GitHub API](https://developer.github.com/v3/repos/#list-your-repositories)。



```
>>> gh = GitHub('token')
>>> status, data = gh.user.repos.get(visibility='public', sort='created')
>>> # ^ 映射到 GET /user/repos
>>> data
... ['tweeter', 'snipey', '...']

```

请注意，你要确保 URL 拼写正确，因为我们没有验证 URL。如果 URL 不存在或出现了其他任何错误，将返回 API 抛出的错误。那么，这一切是如何运作的呢？让我们找出答案。首先，我们将查看一个 [`API` 类](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L30-L58)的简化示例：



```
class API:
    # ... other methods ...
    def __getattr__(self, key):
        return IncompleteRequest(self.client).__getattr__(key)
    __getitem__ = __getattr__

```

在 `API` 类上的每次调用都会调用 [`IncompleteRequest` 类](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L60-L100)作为指定的 `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` 类](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L102-L231)获取 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
.... # {....}

```

这真心是一些方法魔术！


### 了解更多


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




---


via: <https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems>


作者：[Nina Zakharenko](https://opensource.com/users/nnja) 选题：[lujun9972](https://github.com/lujun9972) 译者：[MjSeven](https://github.com/MjSeven) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
Python offers a unique set of tools and language features that help make your code more elegant, readable, and intuitive. By selecting the right tool for the right problem, your code will be easier to maintain. In this article, we'll examine three of those tools: magic methods, iterators and generators, and method magic.

## Magic methods


Magic methods can be considered the plumbing of Python. They're the methods that are called "under the hood" for certain built-in methods, symbols, and operations. A common magic method you may be familiar with is `__init__()`

, which is called when we want to initialize a new instance of a class.

You may have seen other common magic methods, like `__str__`

and `__repr__`

. There is a whole world of magic methods, and by implementing a few of them, we can greatly modify the behavior of an object or even make it behave like a built-in datatype, such as a number, list, or dictionary.

Let's take this `Money`

class for example:

```
``````
class Money:
currency_rates = {
'$': 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)
```


The class defines a currency rate for a given symbol and exchange rate, specifies an initializer (also known as a constructor), and implements `__repr__`

, so when we print out the class, we see a nice representation such as `$2.00`

for an instance `Money('$', 2.00)`

with the currency symbol and amount. Most importantly, it defines a method that allows you to convert between different currencies with different exchange rates.

Using a Python shell, let's say we've defined the costs for two food items in different currencies, like so:

```
``````
>>> soda_cost = Money('$', 5.25)
>>> soda_cost
$5.25
>>> pizza_cost = Money('€', 7.99)
>>> pizza_cost
€7.99
```


We could use magic methods to help instances of this class interact with each other. Let's say we wanted to be able to add two instances of this class together, even if they were in different currencies. To make that a reality, we could implement the `__add__`

magic method on our `Money`

class:

```
``````
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)
```


Now we can use this class in a very intuitive way:

```
``````
>>> soda_cost = Money('$', 5.25)
>>> pizza_cost = Money('€', 7.99)
>>> soda_cost + pizza_cost
$14.33
>>> pizza_cost + soda_cost
€12.61
```


When we add two instances together, we get a result in the first defined currency. All the conversion is done seamlessly under the hood. If we wanted to, we could also implement `__sub__`

for subtraction, `__mul__`

for multiplication, and many more. Read about [emulating numeric types](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types), or read this [guide to magic methods](https://rszalski.github.io/magicmethods/) for others.

We learned that `__add__`

maps to the built-in operator `+`

. Other magic methods can map to symbols like `[]`

. For example, to access an item by index or key (in the case of a dictionary), use the `__getitem__`

method:

```
``````
>>> d = {'one': 1, 'two': 2}
>>> d['two']
2
>>> d.__getitem__('two')
2
```


Some magic methods even map to built-in functions, such as `__len__()`

, which maps to `len()`

.

```
``````
class Alphabet:
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def __len__(self):
return len(self.letters)
>>> my_alphabet = Alphabet()
>>> len(my_alphabet)
26
```


## Custom iterators

Custom iterators are an incredibly powerful but unfortunately confusing topic to new and seasoned Pythonistas alike.

Many built-in types, such as lists, sets, and dictionaries, already implement the protocol that allows them to be iterated over under the hood. This allows us to easily loop over them.

```
``````
>>> for food in ['Pizza', 'Fries']:
print(food + '. Yum!')
Pizza. Yum!
Fries. Yum!
```



How can we iterate over our own custom classes? First, let's clear up some terminology.

- To be
*iterable*, a class needs to*implement*`__iter__()`

- The
`__iter__()`

method needs to*return*an*iterator* - To be an
*iterator*, a class needs to*implement*`__next__()`

(or`next()`

[in Python 2](https://docs.python.org/2/library/stdtypes.html#iterator.next)), which*must*raise a`StopIteration`

exception when there are no more items to iterate over.

Whew! It sounds complicated, but once you remember these fundamental concepts, you'll be able to iterate in your sleep.

When might we want to use a custom iterator? Let's imagine a scenario where we have a `Server`

instance running different services such as `http`

and `ssh`

on different ports. Some of these services have an `active`

state while others are `inactive`

.

```
``````
class Server:
services = [
{'active': False, 'protocol': 'ftp', 'port': 21},
{'active': True, 'protocol': 'ssh', 'port': 22},
{'active': True, 'protocol': 'http', 'port': 80},
]
```


When we loop over our `Server`

instance, we only want to loop over `active`

services. Let's create a new class, an `IterableServer`

:

```
``````
class IterableServer:
def __init__(self):
self.current_pos = 0
def __next__(self):
pass # TODO: Implement and remember to raise StopIteration
```


First, we initialize our current position to `0`

. Then, we define a `__next__()`

method, which will return the next item. We'll also ensure that we raise `StopIteration`

when there are no more items to return. So far so good! Now, let's implement this `__next__()`

method.

```
``````
class IterableServer:
def __init__(self):
self.current_pos = 0. # we initialize our current position to zero
def __iter__(self): # we can return self here, because __next__ is implemented
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__ # optional python2 compatibility
```


We keep looping over the services in our list while our current position is less than the length of the services but only returning if the service is active. Once we run out of services to iterate over, we raise a `StopIteration`

exception.

Because we implement a `__next__()`

method that raises `StopIteration`

when it is exhausted, we can return `self`

from `__iter__()`

because the `IterableServer`

class adheres to the `iterable`

protocol.

Now we can loop over an instance of `IterableServer`

, which will allow us to look at each active service, like so:

```
``````
>>> 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
```


That's pretty great, but we can do better! In an instance like this, where our iterator doesn't need to maintain a lot of state, we can simplify our code and use a [generator](https://docs.python.org/3/library/stdtypes.html#generator-types) instead.

```
``````
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']
```


What exactly is the `yield`

keyword? Yield is used when defining a generator function. It's sort of like a `return`

. While a `return`

exits the function after returning the value, `yield`

suspends execution until the next time it's called. This allows your generator function to maintain state until it resumes. Check out [yield's documentation](https://docs.python.org/3/reference/expressions.html#yieldexpr) to learn more. With a generator, we don't have to manually maintain state by remembering our position. A generator knows only two things: what it needs to do right now and what it needs to do to calculate the next item. Once we reach a point of execution where `yield`

isn't called again, we know to stop iterating.

This works because of some built-in Python magic. In the [Python documentation for __iter__()](https://docs.python.org/3/reference/datamodel.html#object.__iter__) we can see that if

`__iter__()`

is implemented as a generator, it will automatically return an iterator object that supplies the `__iter__()`

and `__next__()`

methods. Read this great article for a deeper dive of [iterators, iterables, and generators](http://nvie.com/posts/iterators-vs-generators/).

## Method magic

Due to its unique aspects, Python provides some interesting method magic as part of the language.

One example of this is aliasing functions. Since functions are just objects, we can assign them to multiple variables. For example:

```
``````
>>> def foo():
return 'foo'
>>> foo()
'foo'
>>> bar = foo
>>> bar()
'foo'
```


We'll see later on how this can be useful.

Python provides a handy built-in, [called getattr()](https://docs.python.org/3/library/functions.html#getattr), that takes the

`object, name, default`

parameters and returns the attribute `name`

on `object`

. This programmatically allows us to access instance variables and methods. For example:```
``````
>>> 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!
```


Cool trick, but how could we practically use `getattr`

? Let's look at an example that allows us to write a tiny command-line tool to dynamically process commands.

```
``````
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()
# let's assume we do error handling
command, argument = input('> ').split()
func_to_call = getattr(operations, command, operations.default)
func_to_call(argument)
```


The output of our script is:

```
``````
$ python getattr.py
> say_hi Nina
Hello, Nina
> blah blah
This operation is not supported.
```


Next, we'll look at `partial`

. For example, ** functool.partial(func, *args, **kwargs)** allows you to return a new

[that behaves like](https://docs.python.org/3/library/functools.html#functools.partial)

*partial object*`func`

called with `args`

and `kwargs`

. If more `args`

are passed in, they're appended to `args`

. If more `kwargs`

are passed in, they extend and override `kwargs`

. Let's see it in action with a brief example:```
``````
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo
<functools.partial object at 0x1085a09f0>
>>> basetwo('10010')
18
# This is the same as
>>> int('10010', base=2)
```


Let's see how this method magic ties together in some sample code from a library I enjoy using [called ](https://github.com/mozilla/agithub)

, which is a (poorly named) REST API client with transparent syntax that allows you to rapidly prototype any REST API (not just GitHub) with minimal configuration. I find this project interesting because it's incredibly powerful yet only about 400 lines of Python. You can add support for any REST API in about 30 lines of configuration code. [agithub](https://github.com/mozilla/agithub)`agithub`

knows everything it needs to about protocol (`REST`

, `HTTP`

, `TCP`

), but it assumes nothing about the upstream API. Let's dive into the implementation.

Here's a simplified version of how we'd define an endpoint URL for the GitHub API and any other relevant connection properties. View the [full code](https://github.com/mozilla/agithub/blob/master/agithub/GitHub.py) instead.

```
``````
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)
```


Then, once your [access token](https://github.com/settings/tokens) is configured, you can start using the [GitHub API](https://developer.github.com/v3/repos/#list-your-repositories).

```
``````
>>> gh = GitHub('token')
>>> status, data = gh.user.repos.get(visibility='public', sort='created')
>>> # ^ Maps to GET /user/repos
>>> data
... ['tweeter', 'snipey', '...']
```


Note that it's up to you to spell things correctly. There's no validation of the URL. If the URL doesn't exist or anything else goes wrong, the error thrown by the API will be returned. So, how does this all work? Let's figure it out. First, we'll check out a simplified example of the [ API class](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L30-L58):

```
``````
class API:
# ... other methods ...
def __getattr__(self, key):
return IncompleteRequest(self.client).__getattr__(key)
__getitem__ = __getattr__
```


Each call on the `API`

class ferries the call to the [ IncompleteRequest class](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L60-L100) for the specified

`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') # ... and post, put, patch, etc.
def get(self, url, headers={}, **params):
return self.request('GET', url, None, headers)
```


If the last call is not an HTTP method (like 'get', 'post', etc.), it returns an `IncompleteRequest`

with an appended path. Otherwise, it gets the right function for the specified HTTP method from the [ Client class](https://github.com/mozilla/agithub/blob/dbf7014e2504333c58a39153aa11bbbdd080f6ac/agithub/base.py#L102-L231) and returns a

`partial`

.What happens if we give a non-existent path?

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


And because `__getitem__`

is aliased to `__getattr__`

:

```
``````
>>> owner, repo = 'nnja', 'tweeter'
>>> status, data = gh.repos[owner][repo].pulls.get()
>>> # ^ Maps to GET /repos/nnja/tweeter/pulls
>>> data
.... # {....}
```


Now that's some serious method magic!

## Learn more

Python provides plenty of tools that allow you to make your code more elegant and easier to read and understand. The challenge is finding the right tool for the job, but I hope this article added some new ones to your toolbox. And, if you'd like to take this a step further, you can read about decorators, context managers, context generators, and `NamedTuple`

s on my blog [nnja.io](http://nnja.io). As you become a better Python developer, I encourage you to get out there and read some source code for well-architected projects. [Requests](https://github.com/requests/requests) and [Flask](https://github.com/pallets/flask) are two great codebases to start with.

To learn more about these topics, as well as decorators, context managers, context decorators, and NamedTuples, attend Nina Zakharenko 's talk, [Elegant Solutions for Everyday Python Problems](https://us.pycon.org/2018/schedule/presentation/164/), at [PyCon Cleveland 2018](https://us.pycon.org/2018/).

## 2 Comments