Рубрики
Без рубрики

Сделайте свой код более «Pythonic», используя специальные методы Python

Автор оригинала: FreeCodeCapm Team.

Марко Массензио

В его превосходном Свободный Python Книга, Лучано Рамальхо рассказывает о «модели данных Python». Он дает несколько превосходных примеров того, как язык достигает внутренней последовательности благодаря разумным использованию четко определенного API. В частности, он обсуждает, как «Особые методы» « Special Methods » позволяют построить элегантные решения, которые являются краткими и очень читаемыми.

И хотя вы можете найти бесчисленные примеры в Интернете, как реализовать итеративный Специальные методы ( ____er __ () и друзья), здесь я хотел представить пример того, как использовать два из менее известных специальных методов: __del __ () и __call __ () Отказ

Для тех, кто знаком с C ++, они реализуют два очень знакомых образца: деструктор и Функциональный объект (aka, Оператор () ).

Реализовать самоуничтожающий ключ

Скажем, что мы хотим разработать Ключ шифрования который будет в свою очередь, зашифрован с Мастер ключ – И чей «открытый текст» стоимость будет использоваться только «в полете» для шифрования и расшифровки наших данных – но в противном случае будет храниться только зашифрованные.

Есть много причин, по которым вы можете захотеть сделать это, но наиболее распространенным является, когда данные зашифрованы, – это очень большая и трудоустройство для шифрования. Должен ли Мастер ключ Будьте скомпрометированы, мы могли бы отозвать его, затем повторно зашифровать ключи шифрования с новым главным ключом – все не понесло наказание на время, связанное с расшифровкой и повторной шифрованием, возможно, несколько данных Terabytes.

Фактически, повторное шифрование ключей шифрования может быть так вычислительно недорого, что это может быть сделано на регулярной основе, вращая Мастер ключ При частых интервалах (возможно, еженедельно) уменьшить поверхность атаки.

Если мы используем Openssl Инструменты командной строки для выполнения всех задач шифрования и дешифрования, нам нужно временно хранить ключ шифрования как «открытый текст» в файле, который мы будем надежно уничтожить с помощью инструмента Shred Linux.

Обратите внимание, что мы используем термин «открытый текст», чтобы обозначить, что содержимое дешифровано, не означает простого текстового формата. Ключ все еще двоичные данные, но если он перехвачен злоумышленником, будет не быть защищенным шифрованием.

Тем не менее, просто реализация вызова на утилиту для измельчения, поскольку последний шаг в нашем алгоритме шифрования не будет достаточным, чтобы это было выполнено в соответствии с Все Возможные кодовые пути выполнения. Могут быть ошибки, поднятые исключения, изящно прекращаются (Control-C) или резкий сигкилл программы.

Охрана во всех возможностях – не только утомительный, но и без ошибок. Вместо этого мы можем позволить интерпретатору Python может сделать тяжелую работу для нас, и убедиться, что определенные действия всегда Предпринятый, когда объект собирается мусор.

Обратите внимание, что методика, показанная здесь, не будет работать для корпуса SIGKILL (kill kill -9), для которых вам нужно использовать более продвинутую технику (обработчики сигналов).

Идея состоит в том, чтобы создать класс, который реализует __del __ () Специальный метод, который гарантированно будет всегда Вызов, когда то нет дальнейших ссылок на объект, и он собирается мусор. Точное время этого происходит зависит от реализации, но в общепринятых переводчиках Python, кажется, почти мгновенно.

Вот что происходит на ноутбуке MacOS, работает El Capitan и Python 2.7:

$ 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!$

Как видите, метод «Destructor» будет вызван либо когда больше нет ссылок на него ( foo ) или когда переводчик выходит ( бар ).

Следующий фрагмент кода показывает, как мы закончили реализовать наш «самозащитный» ключ. Я назвал это Самодеструктурный Потому что реальная особенность в том, что это уничтожает Версия открытия ключей шифрования при выходе.

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)

Кроме того, обратите внимание, как я реализовал __str __ () Метод, так что я могу получить имя файла, содержащего клавишу открытого текста, просто вызывающим:

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

Обратите внимание, что это упрощенная версия кода. Полный код доступен в FileCrypt Репозиторий GitHub, и это было более полно объяснено в этом Вход в блоге Отказ

Мы могли бы просто так легко реализовать __str __ () Метод для возврата фактического содержимого ключа шифрования.

Будь то, насколько это возможно, если вы посмотрите в код, который использует ключ шифрования, в коем случае мы должны вызывать _save () Метод или напрямую вызывают утилиту кровя. Все это будет позаботиться о переводчике, когда либо пароль выходит из-за объема, либо скрипт завершается (нормально или ненормально).

Реализуйте командный шаблон с вызывающим объектом

Python имеет концепцию под названием Callable, который по сути «то, что можно вызвать как если бы это была функция». Это следует за Утка печатает Подход: «Если это выглядит как утка, и кверь, как утка, то это утка». Ну в случае Callable , «Если это выглядит как функция, и его можно назвать как функция, то это это функция».

Сделать объект класса ведут себя как Callable, Все, что нам нужно сделать, это определить __call __ () Метод, а затем реализовать его как любой другой «обычный» метод класса.

Скажите, что мы хотим реализовать сценарий «командного бегуна», который (аналогично, например, Git) может принимать набор подковых команд и выполнять их. Один подход может быть использовать Узор команды В нашем классе CommandRunner:

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

Вот аргумент инициализации конфигурации – это объект пространства имен, возвращаемый 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()

Чтобы вызвать этот сценарий, мы бы использовали что-то вроде:

$ ./main.py run my_file.py

или же:

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

Стоит отметить, как мы также защищаем от ошибок, используя другой специальный метод ( __getattribute __ () ) и Hasattr () Метод, который является частью вышеупомянутого Python’s Модель данных :

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

Обратите внимание, что мы могли бы использовать __getattr __ () Специальный метод для определения поведения класса при попытке доступа к несуществующим атрибутам, но в этом случае, вероятно, было легче сделать это в точке вызова.

Учитывая тот факт, что мы говорим argparse Ограничить возможное значение для Выбор При разборе CMD Аргумент, нам гарантировано, что мы никогда не получим команду «Неизвестные». Тем не менее, CommandRunner Класс не должен знать это, и его можно использовать в других случаях, когда у нас нет такой гарантии. Не говоря уже о том, что мы только один опечаток от какой-то очень головоломки, если мы не сделали нашу домашнюю работу в __call __ () Отказ

Чтобы сделать всю эту работу, то нам нужно только реализовать тривиальную __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)

Обратите внимание, как мы вызываем бегун так, как если бы это был метод. Это будет в свою очередь, выполнить __call __ () метод и запустить нужную команду.

Мы действительно надеемся, что все согласны с тем, что это более приятный код, чтобы посмотреть, чем такие монструзды, как:

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

Узнав о «специальных методах Python», облегчит ваш код для чтения и повторного использования в разных ситуациях. Это также сделает ваш код более «Pythonic» и сразу узнаваю другим товарищам Pythonistas, что делает ваш намеренный более четкими для понимания и разума.

Первоначально опубликовано codetrips.com 22 июля 2016 года.