2

I have written a class with an instance attribute, call it name. What is the best way of ensuring that all instances of the class will have unique names? Do I create under the class, a set and every time a new instance is created, the name gets added to the set in the definition of init? Since a set is a collection of unique elements, I can therefore validate against whether the new instance's name can be successfully added to the set.

EDIT: I wanted to be able to supply the name instead of assigning a UUID to it. So mementum's approach seems the most robust. jpkotta's is what I would have done.

Spinor8
  • 1,587
  • 4
  • 21
  • 48
  • 2
    This sounds like something better enforced at a different level, if at all. If you enforce it like this, you'll eventually find that "wait, I only want names to be unique across [certain data structures/logical divisions of the program]", or "wait, I deleted the old object with this name, why can't I reuse the name now?" – user2357112 Jan 15 '16 at 19:53
  • Are you going to be persisting these anywhere? – kylieCatt Jan 15 '16 at 21:02
  • Following your edit I added a combination of the `metaclass` and `descriptor` approaches which tries to use the best of both worlds. In my own projects my `metaclasses` do even remove parameters before they touch `__init__`, to alleviate the need for end users to cope with those parameters – mementum Jan 16 '16 at 09:07

3 Answers3

6

You can control instance creation with metaclasses (for example) and ensure that the name is unique. Let's assume that the __init__ method takes a parameter name which has no default value

class MyClass(object):

    def __init__(self, name, *args, **kwargs):
        self.name = name

Obviously, instances can have the same name with this. Let's use a metaclass (using compatible Python 2/3 syntax)

class MyMeta(type):
    _names = set()

    @classmethod
    def as_metaclass(meta, *bases):
        '''Create a base class with "this metaclass" as metaclass

        Meant to be used in the definition of classes for Py2/3 syntax equality

        Args:
          bases: a list of base classes to apply (object if none given)
        '''
        class metaclass(meta):
            def __new__(cls, name, this_bases, d):
                # subclass to ensure super works with our methods
                return meta(name, bases, d)
        return type.__new__(metaclass, str('tmpcls'), (), {})

    def __call__(cls, name, *args, **kwargs):
        if name in cls._names:
            raise AttributeError('Duplicate Name')

        cls._names.add(name)
        return type.__call__(cls, name, *args, **kwargs)


class MyClass(MyMeta.as_metaclass()):
    def __init__(self, name, *args, **kwargs):
        self.name = name


a = MyClass('hello')
print('a.name:', a.name)
b = MyClass('goodbye')
print('b.name:', b.name)

try:
    c = MyClass('hello')
except AttributeError:
    print('Duplicate Name caught')
else:
    print('c.name:', c.name)

Which outputs:

a.name: hello
b.name: goodbye
Duplicate Name caught

Using the metaclass technique you could even avoid having name as a parameter and the names could be generated automatically for each instance.

import itertools

class MyMeta(type):
    _counter = itertools.count()

    @classmethod
    def as_metaclass(meta, *bases):
        '''Create a base class with "this metaclass" as metaclass

        Meant to be used in the definition of classes for Py2/3 syntax equality

        Args:
          bases: a list of base classes to apply (object if none given)
        '''
        class metaclass(meta):
            def __new__(cls, name, this_bases, d):
                # subclass to ensure super works with our methods
                return meta(name, bases, d)
        return type.__new__(metaclass, str('tmpcls'), (), {})

    def __call__(cls, *args, **kwargs):
        obj = type.__call__(cls, *args, **kwargs)
        obj.name = '%s_%d' % (cls.__name__, next(cls._counter))
        return obj


class MyClass(MyMeta.as_metaclass()):
    pass


a = MyClass()
print('a.name:', a.name)

b = MyClass()
print('b.name:', b.name)

c = MyClass()
print('c.name:', c.name)

Output:

a.name: MyClass_0
b.name: MyClass_1
c.name: MyClass_2

To complete the question and answering the comment about preventing a.name = b.name (or any other name already in use) one can use a descriptor based approach

class DescName(object):

    def __init__(self):
        self.cache = {None: self}

    def __get__(self, obj, cls=None):
        return self.cache[obj]

    def __set__(self, obj, value):
        cls = obj.__class__

        if value in cls._names:
            raise AttributeError('EXISTING NAME %s' % value)

        try:
            cls._names.remove(self.cache[obj])
        except KeyError:  # 1st time name is used
            pass
        cls._names.add(value)
        self.cache[obj] = value


class MyClass(object):
    _names = set()

    name = DescName()

    def __init__(self, name, *args, **kwargs):
        self.name = name


a = MyClass('hello')
print('a.name:', a.name)
b = MyClass('goodbye')
print('b.name:', b.name)

try:
    c = MyClass('hello')
except AttributeError:
    print('Duplicate Name caught')
else:
    print('c.name:', c.name)

a.name = 'see you again'
print('a.name:', a.name)

try:
    a.name = b.name
except AttributeError:
    print('CANNOT SET a.name to b.name')
else:
    print('a.name %s = %s b.name' % (a.name, b.name))

With the expected output (names cannot be reused during __init__ or assignment)

a.name: hello
b.name: goodbye
Duplicate Name caught
a.name: see you again
CANNOT SET a.name to b.name

EDIT:

Since the OP favours this approach, a combined metaclass and descriptor approach which covers:

  • name class attribute as descriptoradded by the metaclass during class creation
  • name per instance initialization before the instance gets to __init__
  • name uniqueness also for assignment operations

  • storing the set and itertools.counter controlling name uniqueness inside the descriptor class which removes pollution from the class itself

import itertools


class MyMeta(type):
    class DescName(object):
        def __init__(self, cls):
            self.cache = {None: self, cls: set()}
            self.counter = {cls: itertools.count()}

        def __get__(self, obj, cls=None):
            return self.cache[obj]

        def __set__(self, obj, value):
            self.setname(obj, value)

        def setname(self, obj, name=None):
            cls = obj.__class__
            name = name or '%s_%d' % (cls.__name__, next(self.counter[cls]))

            s = self.cache[cls]
            if name in s:
                raise AttributeError('EXISTING NAME %s' % name)

            s.discard(self.cache.get(obj, None))
            s.add(name)
            self.cache[obj] = name

    def __new__(meta, name, bases, dct):
        cls = super(MyMeta, meta).__new__(meta, name, bases, dct)
        cls.name = meta.DescName(cls)  # add the name class attribute
        return cls

    @classmethod
    def as_metaclass(meta, *bases):
        class metaclass(meta):
            def __new__(cls, name, this_bases, d):
                # subclass to ensure super works with our methods
                return meta(name, bases, d)
        return type.__new__(metaclass, str('tmpcls'), (), {})

    def __call__(cls, *args, **kwargs):
        # Instead of relying on type we do the new and init calls
        obj = cls.__new__(cls, *args, **kwargs)
        cls.name.setname(obj)
        obj.__init__(*args, **kwargs)
        return obj


class MyClass(MyMeta.as_metaclass()):
    def __init__(self, *args, **kwargs):
        print('__init__ with name:', self.name)


a = MyClass()
b = MyClass()
c = MyClass()

a.name = 'my new name'
print('a.name:', a.name)

try:
    a.name = b.name
except AttributeError as e:
    print(e)
else:
    print('a.name %s == %s b.name' % (a.name, b.name))

Which outputs the expected:

__init__ with name: MyClass_0
__init__ with name: MyClass_1
__init__ with name: MyClass_2
a.name: my new name
EXISTING NAME MyClass_1
mementum
  • 3,153
  • 13
  • 20
  • TLDR - does it prevent `b.name = a.name`? – Aprillion Jan 15 '16 at 20:58
  • The OP wanted to control creation ... not modification. So no, it does not. You need to (or can) use a `descriptor` to control that – mementum Jan 15 '16 at 20:59
  • Added a `descriptor` based approach – mementum Jan 15 '16 at 21:20
  • the OP said `What is the best way of ensuring that all instances of the class will have unique names?`.. – Aprillion Jan 15 '16 at 21:23
  • Yes and he goes and elaborates about "instance creation". But the point was already taken and the answer extended. Better long than short and hopefully it helps the OP. He may even combine both techniques – mementum Jan 15 '16 at 21:25
  • nope, long is NOT better for all people, I am myself completely lost in this answer. and let's not confuse **problem statement** with **solution suggestion** – Aprillion Jan 15 '16 at 21:30
3
class Foo():
    _names = set()

    @property
    def name(self):
        return self._name

    def __init__(self, name):
        if name in Foo._names:
            raise NameError("Already used name '%s'." % name)

        self._name = name
        Foo._names.add(name)

To me, this is simpler than messing with metaclasses, etc. If you need to do this for several classes, metaclasses make more sense.

Making name a property without a write method makes assignment fail, effectively making name constant.

If you subclass Foo, it keeps the same set of names across all subclasses, which may or may not be what you want.

jpkotta
  • 9,237
  • 3
  • 29
  • 34
1
Python 2.7.10 (default, Oct 23 2015, 18:05:06) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.59.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from uuid import uuid4
>>> 
>>> class class_with_unique_name(object):
...     def __init__(self): 
...         self.name = str(uuid4())
...         
... 
>>> class_with_unique_name().name
'e2ce6b4e-8989-4390-b044-beb71d834385'
>>> class_with_unique_name().name
'ff277b1b-b149-47f6-9dc9-f9faecd18d11'
>>> class_with_unique_name().name
'63f70cbc-4f0a-4c8b-a35f-114bc5b8bc8d'
>>> class_with_unique_name().name
'a6f95523-ae43-4900-9366-022326474210'
>>> class_with_unique_name().name
'4e7c1200-bd45-427e-bcf0-e643b41f6347'
>>> class_with_unique_name().name
'58fa246e-4f99-49d4-9420-68234e24c921'
>>> class_with_unique_name().name
'7c86b351-fdb9-40c1-8021-b93c70e8e24d'
rbp
  • 1,850
  • 15
  • 28
  • While this would certainly ensure uniqueness it may not be what the OP wants. It sounds like they want to be able to declare `name` at instantiation. More clarification is needed. – kylieCatt Jan 15 '16 at 21:05
  • it solves this problem statement perfectly: "I have written a class with an instance attribute, call it name. What is the best way of ensuring that all instances of the class will have unique names?" – rbp Jan 15 '16 at 21:06
  • agree this is part of the the *best way* to do what OP said - `self.name` should be uneditable though.. no ambiguities in the requirements as stated. possibility of declaring custom `name`s is incompatible with ensuring the uniqueness since there is nothing said what should happen when attempting to declare the same name again or changing an existing name – Aprillion Jan 15 '16 at 21:24
  • @rbp Unless the way OP intends to use the class is `obj = MyClass(name=some_input_from_a_form)`. If the OP intends to use the class like so `obj = MyClass()` this approach would be great. Unless the OP was going to persist them in a DB then the best method would be to let the DB enforce uniqueness. We need further clarification before deciding the "best" way. – kylieCatt Jan 15 '16 at 21:37
  • @Aprillion it would be fairly easy to make `name` a property, with no setter. – rbp Jan 15 '16 at 21:39
  • so what's stopping you? – Aprillion Jan 15 '16 at 21:40
  • its not in the OP's problem statement. here are instructions: http://stackoverflow.com/questions/14594120/python-read-only-property – rbp Jan 15 '16 at 21:41