Лучший способ сделать декоратор для уникальных экземпляров класса Python - PullRequest
1 голос
/ 14 февраля 2020

Я работаю с кодом Python, который вызывает в C оболочки, но код C содержит очень ошибку (и у меня нет другой альтернативы) и вызывает segfault, когда Python объект, управляемый в C, выходит из области видимости, поэтому я должен сохранять ссылки на каждый созданный объект.

Есть ли хороший способ сделать эргономическую c "уникальную" оболочку, в которой каждый класс может только иметь один экземпляр для каждого набора аргументов конструктора, например,

@unique
class Test:
    cls_val = 0
    def __init__(self, val):
        self.val = val

a = Test(1)
b = Test(1)

assert a is b

c = Test(2)
d = Test(2)

assert c is not b and c is not a
assert c is d

Я создал этот декоратор, но он предотвращает использование любого класса с уникальным декором в качестве базового класса (создание экземпляра производного класса вызывает __new__ декоратора).

def unique(unique_cls):
    class Unique:
        instances = {}
        unique_class = unique_cls
        def __new__(cls, *args, **kwargs):
            if not Unique.instances.get(
                (
                     cls.unique_class,
                     f_args := frozenset(args),
                     f_kwargs := frozenset(kwargs),
                )                
            ):
                Unique.instances[
                     (Unique.unique_class, f_args, f_kwargs)
                ] = Unique.unique_class(*args, **kwargs)
            return Unique.instances[(Unique.unique_class, f_args, f_kwargs)]
        def __getattr__(self, name):
            # Overloaded to get class attributes working for decorated classes
            return object.__getattribute__(Unique.unique_class, name)
    return Unique

1 Ответ

5 голосов
/ 14 февраля 2020

Вы можете использовать functools.lru_cache для хранения всех экземпляров в кэше и не устанавливать ограничений на размер кэша. Когда вы вызываете конструктор, вы получаете новый экземпляр, и он будет храниться в кэше. Затем всякий раз, когда вы снова вызываете конструктор с теми же аргументами, вы получаете кэшированный экземпляр. Это также означает, что каждый объект всегда имеет ссылку из кэша.

from functools import lru_cache

@lru_cache(maxsize=None)
class Test:
    def __init__(self, val):
        self.val = val

Демонстрация:

>>> a = Test(1)
>>> a2 = Test(1)
>>> a is a2
True
>>> b = Test(2)
>>> b2 = Test(2)
>>> b is b2
True
>>> a is b
False

Если вам нужно иметь возможность подкласс Test (или действительно, сделать что-нибудь с Test, за исключением создания экземпляров), тогда вы можете переопределить __new__ и применить там декоратор. Это работает, потому что cls является аргументом для __new__, поэтому кэш будет различать guish между различными экземплярами по их классу и по __init__ аргументам.

class Test:
    @lru_cache(maxsize=None)
    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)
    def __init__(self, val):
        self.val = val

Демонстрация:

>>> Test(1) is Test(1)
True
>>> Test(1) is Test(2)
False
>>> class SubTest(Test): pass
... 
>>> Test(1) is SubTest(1)
False
>>> SubTest(1) is SubTest(1)
True
...