Использует `is (CustomClass)` безопасно для обнаружения неинициализированных значений в Python - PullRequest
1 голос
/ 15 апреля 2019

Я создаю декоратор для кэширования значений дорогостоящих функций, которые могут генерировать исключения, и я хочу записать, достигли ли мы точки, где значение инициализировано или нет.На данный момент я просто инициализирую значение «нечетной» строкой new_val = "__THIS_IS_UNINITIALIZED__", но чувствует себя грязным .

Мне было интересно, если использовать is с пользовательским классом (который неничего не делать) будет безопасно.

Вот что у меня сейчас:

class Cache:
    _cached = {}
    @staticmethod
    def cache(prefix):
        def wrapper(wrapped):
            def inner(self, *args, **kwargs):
                new_val = "__THIS_IS_UNINITIALIZED__"
                key = Cache.make_key(*args, **kwargs)
                if key in Cache._cached:
                    print("cache hit")
                    return Cache._cached[key]
                print("cache miss")
                try:
                    # This can throw exceptions
                    new_val = wrapped(self, *args, **kwargs)
                    # Something below this can ALSO throw
                    # exceptions, but the value has been
                    # initialized at this point.
                except:
                    if new_val == "__THIS_IS_UNINITIALIZED__":
                        print("we never got to initialize :_( ")
                else:
                    Cache._cache[key] = new_val
            return inner
        return wrapper

И мне было интересно, смогу ли я использовать if is Class вместо if new_val == "__THIS_IS_UNINITIALIZED__"

Примерно так:

class Uninitialized:
    pass

class Cache:
    _cached = {}
    @staticmethod
    def cache(prefix):
        def wrapper(wrapped):
            def inner(self, *args, **kwargs):
                new_val = Uninitialized
                key = Cache.make_key(*args, **kwargs)
                if key in Cache._cached:
                    print("cache hit")
                    return Cache._cached[key]
                print("cache miss")
                try:
                    # This can throw exceptions
                    new_val = wrapped(self, *args, **kwargs)
                    # Something below this can ALSO throw
                    # exceptions, but the value has been
                    # initialized at this point.
                except:
                    if new_val is Uninitialized:
                        print("we never got to initialize :_( ")
                else:
                    Cache._cache[key] = new_val
            return inner
        return wrapper

    @staticmethod
    def make_key(*args, **kwargs):
        positional = ':'.join([str(s) for s in args])
        kw = ':'.join('%s=%s' % kf for kv in kwargs.items())
        return ':'.join([positional, kw])

class Test:
    def __init__(self):
        self.foo = 'hi'

    @Cache.cache('api')
    def test_cache(self, a, b):
        raise Exception("foo")

if __name__ == "__main__":
    t = Test()
    t.test_cache(1, 2)

Использование строки "__THIS_IS_UNINITIALIZED__" пока отлично работает (и будет работать в обозримом будущем).Честно говоря, это в основном для целей обучения.

Заранее спасибо.

Ответы [ 3 ]

2 голосов
/ 16 апреля 2019

Стандартная идиома заключается в том, чтобы по существу делать то, что вы делали, но создавать экземпляр object, а не определять новый класс для таких стражей.

 Uninitialized = object()

Так как ваше утверждение класса эквивалентно

Uninitialized = type("Uninitialized", (object,), {})

Единственная разница в том, что я создал object вместо type.


Обновление (через Объект Sentinel и его приложения? ): экземпляр пользовательского класса может предоставить более полезное представление, как показано на примере из модуля dataclasses :

>>> from dataclasses import MISSING
>>> repr(MISSING)
'<dataclasses._MISSING_TYPE object at 0x10baeaf98>'
>>> repr(object())
'<object object at 0x10b961200>'

Определение нового класса позволяет использовать имя класса в качестве короткого диагностического сообщения или описания цели дозорного.

2 голосов
/ 15 апреля 2019

Python на самом деле не имеет "неинициализированных переменных".Переменная не существует, пока вы не назначите ей значение.Если вы ссылаетесь на такую ​​переменную до того, как присвоите ей значение, вы получите

  1. a NameError, если ваш код ссылается на a, но a не назначен
  2. an AttributeError, если ваш код a.b, если a существует, но b не назначен
  3. UnboundLocalError, если ваш код знает о a, но пытается получить из него значениедо того, как он был назначен.

(Могут быть и другие случаи, которые я пропустил.)

Вы, кажется, пытаетесь изменить дизайн этого соглашения, и это, вероятно, не очень хорошая идея..

Если вы хотите проверить, что у вашего экземпляра класса a есть определенный атрибут b, вы можете сделать hasattr(a, "b").Или просто напишите try...except, что ловушка для AttributeError.

Но мне кажется, вы пытаетесь решить что-то, что является проблемой на другом языке.Ссылка на неинициализированную переменную в языках, где такие вещи существуют, может привести к segfault.Но выполнение соответствующей вещи в Python просто приводит к ошибке во время выполнения.Вам не нужно ловить это, потому что язык уже делает это.

1 голос
/ 16 апреля 2019

Я думаю, что есть два варианта вашего паттерна, которые можно считать улучшением.


Стражи

Это довольно распространенный шаблон, вы создаете что-то очень похожее на вашу уникальную строку, которая называется значением часового. Библиотека cpython также используется, например, в модуле классов данных . По сути, вы просто создаете и разделяете пустой объект, который описан как ничего не значащий, и используете его для создания экземпляров переменных, которые еще не имеют значащего значения.

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

constants.py

SENTINEL = object()
# some other constants maybe

code.py

from constants import SENTINEL

COSTLY_COMPUTE_RESULT = SENTINEL

def costly_compute():
    global COSTLY_COMPUTE_RESULT 
    if COSTLY_COMPUTE_RESULT is not SENTINEL:
        return COSTLY_COMPUTE_RESULT 
    COSTLY_COMPUTE_RESULT = ...  # assume this is some heavy lifting
    return costly_compute()

Это то, что я лично предлагаю сделать на случай, если вы захотите запустить такой механизм кэширования.


Хаки атрибутов функций

Поскольку функции являются объектами первого класса, они могут иметь атрибуты, как и любой другой объект. Так что-то вроде этого полностью допустимый код Python:

def costly_compute():
    try:
        return costly_compute.cache
    except AttributeError:
        pass
    costly_compute.cache = ...  # some heavy lifting again
    return costly_compute()

С стилистической точки зрения это довольно ужасно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...