pythonic_使用Python的特殊方法使代码更“ Pythonic”

pythonic

by Marco Massenzio

由Marco Massenzio

使用Python的特殊方法使代码更“ Pythonic” (Make your code more “pythonic” using Python’s special methods)

In his excellent Fluent Python book, Luciano Ramalho talks about Python’s “data model.” He gives some excellent examples of how the language achieves internal consistency through the judicious use of a well-defined API. In particular, he discusses how Python’s “special methods” enable the construction of elegant solutions, which are concise and highly readable.

Luciano Ramalho在其出色的Fluent Python书中谈到了Python的“数据模型”。 他提供了一些出色的示例,说明了该语言如何通过谨慎使用定义良好的API来实现内部一致性。 特别是,他讨论了Python的“ 特殊方法 ”如何实现简洁,可读性强的优雅解决方案的构建。

And while you can find countless examples online of how to implement the iterative special methods (__iter__() and friends), here I wanted to present an example of how to use two of the lesser known special methods: __del__() and __call__().

尽管您可以在网上找到无数示例,以了解如何实现迭代特殊方法( __iter __()和朋友),但在此我想提供一个示例,说明如何使用两个鲜为人知的特殊方法: __del __()__call __()

For those familiar with C++, these implement two very familiar patterns: the destructor and the function object (aka, operator()).

对于熟悉C ++的人来说,它们实现了两种非常熟悉的模式: 析构函数函数对象 (aka, operator() )。

实施自毁密钥 (Implement a self-destructing key)

Let’s say that we want to design an encryption key which will be in turn encrypted with a master key — and whose “plaintext” value will only be used “in flight” to encrypt and decrypt our data — but will otherwise only be stored encrypted.

假设我们要设计一个加密密钥 ,然后再用一个主密钥对其进行加密,并且其“纯文本”值仅在“传输中”用于加密和解密我们的数据,而在其他情况下则仅以加密方式存储。

There are many reasons why you may want to do this, but the most common is when the data to be encrypted is very large and time-consuming to encrypt. Should the master key be compromised, we could revoke it then re-encrypt the encryption keys with a new master key — all without incurring the time penalty associated with decrypting and re-encrypting possibly several terabytes worth of data.

您可能有很多理由要这样做,但是最常见的是要加密的数据非常大且加密很费时的情况。 如果主密钥遭到破坏,我们可以将其撤销,然后使用新的主密钥对加密密钥进行重新加密-所有这些都不会造成与解密和重新加密可能价值几TB的数据有关的时间损失。

In fact, re-encrypting the encryption keys may be so computationally inexpensive that this could be done on a regular basis, rotating the master key at frequent intervals (perhaps weekly) to decrease the attack surface.

实际上,重新加密加密密钥可能在计算上非常便宜,以至于可以定期进行,以频繁的间隔(可能每周一次)旋转主密钥以减少攻击面。

If we use OpenSSL command-line tools to do all the encryption and decryption tasks, we need to temporarily store the encryption key as “plaintext” in a file, which we will securely destroy using the shred Linux tool.

如果我们使用OpenSSL命令行工具执行所有加密和解密任务,则需要将加密密钥作为“纯文本”临时存储在文件中,我们将使用粉碎的Linux工具将其安全地销毁。

Note that we use the term “plaintext” here to signify that the contents are decrypted, not to mean plain text format. The key is still binary data, but if intercepted by an attacker, would not be protected with encryption.

请注意,此处我们使用术语“纯文本”表示内容已解密,而不是纯文本格式。 密钥仍然是二进制数据,但是如果被攻击者截获,则不会受到加密的保护。

However, just implementing the call to the shredding utility as the last step in our encryption algorithm would not be sufficient to ensure that this is executed under all possible code path executions. There may be errors, raised exceptions, a gracefully termination (Control-c), or an abrupt SIGKILL of the program.

但是,仅将对切碎实用程序的调用作为我们加密算法的最后一步来实现,不足以确保在所有可能的代码路径执行下执行该调用。 可能存在错误,引发异常,正常终止(Control-c)或程序的突然SIGKILL。

Guarding against all possibilities is not only tiresome, but also error-prone. Instead we can let the Python interpreter can do the hard work for us, and ensure that certain actions are always undertaken when the object is garbage collected.

防范所有可能性不仅繁琐,而且容易出错。 相反,我们可以让Python解释器为我们完成艰苦的工作,并确保在垃圾回收对象时始终执行某些操作。

Note that the technique shown here will not work for the SIGKILL case (aka kill -9), for which you’d need to employ a more advanced technique (signal handlers).

请注意,此处显示的技术不适用于SIGKILL情况(也称为kill -9),为此您需要采用更高级的技术(信号处理程序)。

The idea is to create a class which implements the __del__() special method, which is guaranteed to be always invoked when the there are no further references to the object, and it is being garbage-collected. The exact timing of that happening is implementation dependent, but in common Python interpreters, it seems to be almost instantaneous.

这个想法是创建一个实现__del __()特殊方法的类,该类保证在没有对象的进一步引用且被垃圾回收时始终调用该方法。 发生这种情况的确切时间取决于实现,但是在常见的Python解释器中,这几乎是瞬时的。

Here’s what happens on a MacOS laptop, running El Capitan and Python 2.7:

这是在运行El Capitan和Python 2.7的MacOS笔记本电脑上发生的情况:

$ pythonPython 2.7.10 (default, Oct 23 2015, 19:19:21)[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin>>> class Foo():...     def __del__(self):...         print("I'm gone, goodbye!")...>>> foo = Foo()>>> bar = foo>>> foo = None>>> bar = 99I'm gone, goodbye!>>> another = Foo()>>> ^DI'm gone, goodbye!$

As you can see, the “destructor” method will be invoked either when there are no longer references to it (foo) or when the interpreter exits (bar).

如您所见,当不再有引用( foo )或解释器退出( bar )时,将调用“析构函数”方法。

The following code fragment shows how we ended up implementing our “self-encrypting” key. I called it SelfDestructKey because the real feature is that it destructs the plaintext version of the encryption key upon exit.

以下代码片段显示了我们最终如何实现“自加密”密钥。 我之所以称其为SelfDestructKey,是因为其真正的功能是在退出时会破坏加密密钥的纯文本版本。

class SelfDestructKey(object):    """A self-destructing key: it will shred its contents when it gets deleted.        This key also encrypts itself with the given key before writing itself out to a file.    """     def __init__(self, encrypted_key, keypair):        """Creates an encryption key, using the given keypair to encrypt/decrypt it.         The plaintext version of this key is kept in a temporary file that will be securely        destroyed upon this object becoming garbage collected.         :param encrypted_key the encrypted version of this key is kept in this file: if it            does not exist, it will be created when this key is saved        :param keypair a tuple containing the (private, public) key pair that will be used to            decrypt and encrypt (respectively) this key.        :type keypair collections.namedtuple (Keypair)        """        self._plaintext = mkstemp()[1]        self.encrypted = encrypted_key        self.key_pair = keypair        if not os.path.exists(encrypted_key):            openssl('rand', '32', '-out', self._plaintext)        else:            with open(self._plaintext, 'w') as self_decrypted:                openssl('rsautl', '-decrypt', '-inkey', keypair.private, _in=encrypted_key,                        _out=self_decrypted)     def __str__(self):        return self._plaintext     def __del__(self):        try:            if not os.path.exists(self.encrypted):                self._save()            shred(self._plaintext)        except ErrorReturnCode as rcode:            raise RuntimeError(                "Either we could not save encrypted or not shred the plaintext passphrase "                "in file {plain} to file {enc}.  You will have to securely delete the plaintext "                "version using something like `shred -uz {plain}".format(                    plain=self._plaintext, enc=self.encrypted))     def _save(self):        """ Encrypts the contents of the key and writes it out to disk.         :param dest: the full path of the file that will hold the encrypted contents of this key.        :param key: the name of the file that holds an encryption key (the PUBLIC part of a key pair).        :return: None        """        if not os.path.exists(self.key_pair.public):            raise RuntimeError("Encryption key file '%s' not found" % self.key_pair.public)        with open(self._plaintext, 'rb') as selfkey:            openssl('rsautl', '-encrypt', '-pubin', '-inkey', self.key_pair.public,                    _in=selfkey, _out=self.encrypted)

Also, note how I have implemented the __str__() method, so that I can get the name of the file containing the plaintext key by just invoking:

另外,请注意我是如何实现__str __()方法的,以便仅通过调用即可获取包含纯文本键的文件的名称:

passphrase = SelfDestructKey(secret_file, keypair=keys) encryptor = FileEncryptor(    secret_keyfile=str(passphrase),    plain_file=plaintext,    dest_dir=enc_cfg.out)

Note that this is a simplified version of the code. The full code is available at the filecrypt Github repository, and it has been more fully explained in this blog entry.

请注意,这是代码的简化版本。 完整代码可在filecrypt Github存储库中找到,并且在此Blog条目中已进行了更全面的说明。

We could have just as easily implemented the __str__() method to return the actual contents of the encryption key.

我们可以很容易地实现__str __()方法以返回加密密钥的实际内容。

Be that as it may, if you look in the code that uses the encryption key, at no point we need to invoke the _save() method or directly invoke the shred utility. This will all be taken care of by the interpreter when either the passphrase goes out of scope, or the script terminates (normally or abnormally).

即便如此,如果您查看使用加密密钥的代码,则我们绝不需要调用_save()方法或直接调用shred实用程序。 当密码短语超出范围或脚本终止(正常或异常)时,解释器将解决所有这些问题。

使用Callable对象实现命令模式 (Implement the Command Pattern with a Callable object)

Python has the concept called a callable, which is essentially “something that can be invoked as if it were a function.” This follows the Duck Typing approach: “if it looks like a duck, and quacks like a duck, then it is a duck.” Well in the case of callable, “if it looks like a function, and can be called like a function, then it is a function.”

Python有一个称为可调用的概念它实质上是“可以被当作函数调用的东西”。 这遵循鸭子输入法:“如果它看起来像鸭子,而嘎嘎像鸭子,那么它就是鸭子。” 在callable的情况下,“如果它看起来像一个函数,并且可以像一个函数一样被调用,那么它就是一个函数。”

To make a class object behave as a callable, all we need to do is to define a __call__() method and then implement it as any other “ordinary” class method.

为了使一个类对象表现为可调用的,我们要做的就是定义一个__call __()方法,然后将其实现为任何其他“普通”类方法。

Say that we want to implement a “command runner” script that (similarly to, for example, git) can take a set of sub-commands and execute them. One approach could be to use the Command Pattern in our CommandRunner class:

假设我们要实现一个“命令运行程序”脚本(类似于git),它可以采用一组子命令并执行它们。 一种方法是在CommandRunner类中使用Command Pattern

class CommandRunner(object):    """Implements the Command pattern, with the help of the       __call__() special method."""     def __init__(self, config):        """Initiailize the Runner with the configuration            from parsing the command line.            :param config the command-line arguments, as parsed                 by ``argparse``           :type config Namespace        """        self._config = config     def __call__(self):        method = self._config.cmd        if hasattr(self, method):            callable_meth = self.__getattribute__(method)            if callable_meth:                callable_meth()        else:            raise RuntimeError('Unexpected command "{}"; not found'.format(method))     def run(self):        # Do something with the files        pass     def build(self):        # Call an external method that takes a list of files        build(self._config.files)     def diff(self):        """Will compute the diff between the two files passed in"""        if self._config.files and len(self._config.files) == 2:            file_a, file_b = tuple(self._config.files)            diff_files(file_a, file_b)        else:            raise RuntimeError("Not enough arguments for diff: "                               "2 expected, {} found".format(                len(self._config.files) if self._config.files                                         else 'none'))     def diff_all(self):        # This will take a variable number of files and         # will diff them all        pass

Here’s the config initialization argument is a Namespace object as returned by the argparse library:

这是config初始化参数,是argparse库返回的命名空间对象:

def parse_command():    """ Parse command line arguments and returns a config object
:return: the configured options    :rtype: Namespace or None    """    parser = argparse.ArgumentParser()     # Removed the `help` argument for better readability;    # always include that to help your user, when they invoke your     # script with the `--help` flag.    parser.add_argument('--host', default='localhost')    parser.add_argument('-p', '--port', type=int, default=8080,)    parser.add_argument('--workdir', default=default_wkdir)     parser.add_argument('cmd', default='run', choices=[        'run', 'build', 'diff', 'diff_all'])    parser.add_argument('files', nargs=argparse.REMAINDER")    return parser.parse_args()

To invoke this script we would use something like:

要调用此脚本,我们将使用类似以下内容的代码:

$ ./main.py run my_file.py

or:

要么:

$ ./main.py diff file_1.md another_file.md

It’s worth pointing out how we also protect against errors using another special method (__getattribute__()) and the hasattr() method that is part of the above-mentioned Python’s data model:

值得指出的是,我们如何使用另一个特殊方法( __getattribute __() )和上述Python 数据模型中hasattr()方法来防止错误:

if hasattr(self, method):    callable_meth = self.__getattribute__(method)

Note that we could have used the __getattr__() special method to define the behavior of the class when attempting to access non-existing attributes, but in this case it was probably easier to do that at the point of call.

请注意,当尝试访问不存在的属性时,我们可以使用__getattr __()特殊方法来定义类的行为,但是在这种情况下,在调用时这样做可能会更容易。

Given the fact that we are telling argparse to limit the possible value to the choices when parsing the cmd argument, we are guaranteed that we will never get an “unknown” command. However, the CommandRunner class does not need to know this, and it can be used in other instances where we do not have such a guarantee. Not to mention that we are only one typo away from some very puzzling bug, if we didn’t do our homework in __call__().

考虑到我们在解析cmd参数时告诉argparse将可能的值限制在选择范围内的事实,因此可以保证我们永远不会得到“未知”命令。 但是, CommandRunner类不需要知道这一点,并且可以在没有此类保证的其他实例中使用它。 更不用说如果我们没有在__call __()中完成功课,那么我们与一些非常令人困惑的错误只有一个错字。

To make all this work, then we only need to implement a trivial __main__ snippet:

要使所有这些工作正常进行,那么我们只需要实现一个简单的__main__片段即可:

if __name__ == '__main__':     cfg = parse_command()     try: runner = CommandRunner(cfg)         runner() # Looks like a function, let's use it like one.     except Exception as ex:         logging.error("Could not execute command `{}`: {}".format(            cfg.cmd, ex))         exit(1)

Note how we invoke the runner as if it were a method. This will in turn execute the __call__() method and run the desired command.

注意我们如何像运行方法一样调用运行程序。 这将依次执行__call __()方法并运行所需的命令。

We truly hope everyone agrees that this is a way more pleasant code to look at than monstrosities such as:

我们确实希望每个人都同意,这是一种比令人讨厌的代码更令人愉悦的代码,例如:

# DON'T DO THIS AT HOME# Please avoid castle-of-ifs, they are just plain ugly.if cfg.cmd == "build":    # do something to buildelif cfg.cmd == "run":    # do something to runelif cfg.cmd == "diff":    # do something to diffelif cfg.cmd == "diff_all":    # do something to diff_allelse:    print("Unknown command", cfg.cmd)

Learning about Python’s “special methods” will make your code easier to read and re-use in different situations. It will also make your code more “pythonic” and immediately recognizable to other fellow pythonistas, thus making your intent clearer to understand and reason about.

了解Python的“特殊方法”将使您的代码在不同情况下更易于阅读和重用。 这也将使您的代码更具“ Python风格”,并立即为其他pythonista同伴所识别,从而使您的意图更加清晰易懂。

Originally published at codetrips.com on July 22, 2016.

最初于2016年7月22日发布在codetrips.com

翻译自: https://www.freecodecamp.org/news/make-your-code-more-pythonic-using-pythons-special-methods-b348f915852e/

pythonic

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值