编译自:https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems
作者:NinaZakharenko
译者:MjSeven
3 tools to make your Python code more elegant, readable, intuitive, and easy to maintain.
3个可以使你的Python代码更优雅、可读、直观和易于维护的工具。
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.
Python提供了一组独特的工具和语言特性来使你的代码更加优雅、可读和直观。为正确的问题选择合适的工具,你的代码将更易于维护。在本文中,我们将研究其中的三个工具:魔术方法、迭代器和生成器,以及方法魔术。
1 Magic methods
1 魔术方法
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.
魔术方法可以看作是Python的管道。它们被称为“底层”方法,用于某些内置的方法、符号和操作。你可能熟悉的常见魔术方法是__init__(),当我们想要初始化一个类的新实例时,它会被调用。
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.
你可能已经看过其他常见的魔术方法,如__str__和__repr__。Python中有一整套魔术方法,通过实现其中的一些方法,我们可以修改一个对象的行为,甚至使其行为类似于内置数据类型,例如数字、列表或字典。
Let's take this Money class for example:
让我们创建一个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)
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.
该类定义为给定的货币符号和汇率定义了一个货币汇率,指定了一个初始化器(也称为构造函数),并实现__repr__,因此当我们打印这个类时,我们会看到一个友好的表示,例如$2.00,这是一个带有货币符号和金额的Money('$',2.00)实例。最重要的是,它定义了一种方法,允许你使用不同的汇率在不同的货币之间进行转换。
Using a Python shell, let's say we've defined the costs for two food items in different currencies, like so:
打开Pythonshell,假设我们已经定义了使用两种不同货币的食品的成本,如下所示:
>>> 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:
我们可以使用魔术方法使得这个类的实例之间可以相互交互。假设我们希望能够将这个类的两个实例一起加在一起,即使它们是不同的货币。为了实现这一点,我们可以在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)
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, or read this guide to magic methods for others.
当我们将两个实例加在一起时,我们得到以第一个定义的货币符号所表示的结果。所有的转换都是在底层无缝完成的。如果我们想的话,我们也可以为减法实现__sub__,为乘法实现__mul__等等。阅读模拟数字类型或魔术方法指南来获得更多信息。
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:
我们学习到__add__映射到内置运算符+。其他魔术方法可以映射到像[]这样的符号。例如,在字典中通过索引或键来获得一项,其实是使用了__getitem__方法:
>>> 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().
一些魔术方法甚至映射到内置函数,例如__len__()映射到len()。
class Alphabet: letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' def __len__(self): return len(self.letters) >>> my_alphabet = Alphabet() >>> len(my_alphabet) 26
2 Custom iterators
2 自定义迭代器
Custom iterators are an incredibly powerful but unfortunately confusing topic to new and seasoned Pythonistas alike.
对于新的和经验丰富的Python开发者来说,自定义迭代器是一个非常强大的但令人迷惑的主题。
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__()
要成为一个可迭代对象,一个类需要实现__iter__()
The __iter__() method needs to return an iterator
__iter__()方法需要返回一个迭代器
To be an iterator, a class needs to implement __next__() (or next() in Python 2), which must raise a StopIteration exception when there are no more items to iterate over.
要成为一个迭代器,一个类需要实现__next__()(或在Python2中是next()),当没有更多的项要迭代时,必须抛出一个StopIteration异常。
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.
我们什么时候想使用自定义迭代器?让我们想象一个场景,我们有一个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}, ]
When we loop over our Server instance, we only want to loop over active services. Let's create a new class, an IterableServer:
当我们遍历Server实例时,我们只想遍历那些处于active的服务。让我们创建一个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.
首先,我们将当前位置初始化为0。然后,我们定义一个__next__()方法来返回下一项。我们还将确保在没有更多项返回时抛出StopIteration。到目前为止都很好!现在,让我们实现这个__next__()方法。
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.
我们对列表中的服务进行遍历,而当前的位置小于服务的个数,但只有在服务处于活动状态时才返回。一旦我们遍历完服务,就会抛出一个StopIteration异常。
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.
因为我们实现了__next__()方法,当它耗尽时,它会抛出StopIteration。我们可以从__iter__()返回self,因为IterableServer类遵循iterable协议。
Now we can loop over an instance of IterableServer, which will allow us to look at each active service, like so:
现在我们可以遍历一个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
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 generatorinstead.
太棒了,但我们可以做得更好!在这样类似的实例中,我们的迭代器不需要维护大量的状态,我们可以简化代码并使用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']
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, yieldsuspends execution until the next time it's called. This allows your generator function to maintain state until it resumes. Check out yield's documentation 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.
yield关键字到底是什么?在定义生成器函数时使用yield。这有点像return,虽然return在返回值后退出函数,但yield会暂停执行直到下次调用它。这允许你的生成器的功能在它恢复之前保持状态。查看yield的文档以了解更多信息。使用生成器,我们不必通过记住我们的位置来手动维护状态。当yield不再被调用,我们就知道停止迭代。
This works because of some built-in Python magic. In the Python documentation for __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.
这是因为一些内置的Python魔法。在Python关于__iter__()的文档中我们可以看到,如果__iter__()是作为一个生成器实现的,它将自动返回一个迭代器对象,该对象提供__iter__()和__next__()方法。阅读这篇很棒的文章,深入了解迭代器,可迭代对象和生成器。
3 Method magic
3 方法魔法
Due to its unique aspects, Python provides some interesting method magic as part of the language.
由于其独特的方面,Python提供了一些有趣的方法魔法作为语言的一部分。
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(), that takes the object, name, defaultparameters and returns the attribute name on object. This programmatically allows us to access instance variables and methods. For example:
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> >>> getattr(fido, 'speak') > >>> 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.
这是一个很酷的技巧,但是我们如何在实际中使用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() # 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 partial object that behaves like 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:
接下来,我们来看看partial。例如,functool.partial(func,*args,**kwargs)允许你返回一个新的partial对象,它的行为类似func,参数是args和kwargs。如果传入更多的args,它们会被附加到args。如果传入更多的kwargs,它们会扩展并覆盖kwargs。让我们通过一个简短的例子来看看:
>>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo>>> 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 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 knows everything it needs to about protocol (REST, HTTP, TCP), but it assumes nothing about the upstream API. Let's dive into the implementation.
让我们看看在我喜欢的一个名为agithub的库中的一些示例代码中,这个方法魔术是如何结合在一起的,这是一个(名字起得很low的)RESTAPI客户端,它具有透明的语法,允许你以最小的配置快速构建任何RESTAPI原型(不仅仅是GitHub)。我发现这个项目很有趣,因为它非常强大,但只有大约400行Python代码。你可以在大约30行配置代码中添加对任何RESTAPI的支持。agithub知道协议所需的一切(REST、HTTP、TCP),但它不考虑上游API。让我们深入到它的实现中。
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 instead.
以下是我们如何为GitHubAPI和任何其他相关连接属性定义端点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)
Then, once your access token is configured, you can start using the GitHub API.
>>> 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:
请注意,你要确保URL拼写正确,因为我们没有验证URL。如果URL不存在或出现了其他任何错误,将返回API抛出的错误。那么,这一切是如何运作的呢?让我们找出答案。首先,我们将查看一个API类的简化示例:
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 for the specified key.
在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') # ... 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 and returns a partial .
如果最后一次调用不是HTTP方法(如get、post等),则返回带有附加路径的IncompleteRequest。否则,它从Client类获取HTTP方法对应的正确函数,并返回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!
这真心是一些方法魔术!
4 Learn more
4 了解更多
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 NamedTuples on my blog 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. Requestsand Flask are two great codebases to start with.
Python提供了大量工具,使你的代码更优雅,更易于阅读和理解。挑战在于找到合适的工具来完成工作,但我希望本文为你的工具箱添加了一些新工具。而且,如果你想更进一步,你可以在我的博客nnja.io上阅读有关装饰器、上下文管理器、上下文生成器和命名元组的内容。随着你成为一名更好的Python开发人员,我鼓励你到那里阅读一些设计良好的项目的源代码。Requests和Flask是两个很好的起步的代码库。
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, at PyCon Cleveland 2018.
via:https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems
About the author
作者:NinaZakharenko选题:lujun9972译者:MjSeven校对:wxy
Nina Zakharenko - Nina Zakharenko is a cloud developer advocate at Microsoft, focusing on Python. Before joining Microsoft, she was a software engineer with a decade of experience who honed her technical chops writing software for companies like Reddit, meetup, and HBO. In her spare time, she enjoys snowboarding, hiking, and riding her bike in her home base in Portland, OR.
本文由LCTT原创编译,Linux中国荣誉推出