3

Not sure if this is feasible or not. The implementation/example below is dummy, FYI.

I have a Python class, Person. Each person has a public first name and a public last name attribute with corresponding private attributes - I'm using a descriptor pattern to manage access to the underlying private attributes.

I am using the descriptor to count the number of times the attribute is accessed as well as obtain the underlying result.

class AttributeAccessCounter:
    def __init__(self):
        self._access_count = 0

    def __get__(self, instance, owner):
        self._access_count += 1
        return getattr(instance, self.attrib_name)

    def __set_name__(self, obj, name):
        self.attrib_name = f'_{name}'

    @property
    def counter(self):
        return self._access_count


class Person:
    first = AttributeAccessCounter()
    last = AttributeAccessCounter()

    def __init__(self, first, last):
        self._first = first
        self._last = last

From an instance of the class Person, how can I access the _access_count or property counter?

john = Person('John','Smith')
print(john.first) # 'John'
print(john.first.counter) # AttributeError: 'str' object has no attribute 'counter'
2
  • 1
    As far as I can tell, there is no way to retrieve that access count - the descriptor protocol turns every possible access into something else. (And note that the access count is per-class, rather than per-instance - it's not clear which of the two you intended.) If you added if instance is None: return self._access_count as the first line of __get__(), you could retrieve the count via Person.first. Commented Jun 18, 2024 at 13:31
  • 2
    You're setting _access_count on the descriptor itself. That means the count is global, not per-instance - it tracks the number of times the attribute has been accessed across all instances of Person. Is this what you really wanted? Commented Jun 18, 2024 at 13:41

3 Answers 3

3

Currently you don't differentiate when the descriptor is accessed through the instance or through the class itself. property does this for example. It gives you the descriptor object when you access it through the class.

You can do the same:

    def __get__(self, instance, owner):
        if instance is None:
            return self
        self._access_count += 1
        return getattr(instance, self.attrib_name)

Now the counter property of the descriptor class can be accessed.

However there is another problem pointed out by @user2357112 in the comment. You store this count on descriptor and it's shared between different instances. You can't really tell first attribute of which instance of Person is accessed n times.

To solve that, if you still want to store it in the descriptor object, one way is to use a dictionary and call e method for getting the count.

Here is the complete code:

from collections import defaultdict


class AttributeAccessCounter:
    def __init__(self):
        self._access_counts = defaultdict(int)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        self._access_counts[instance] += 1
        return getattr(instance, self.attrib_name)

    def __set_name__(self, obj, name):
        self.attrib_name = f"_{name}"

    def count(self, obj):
        return self._access_counts[obj]


class Person:
    first = AttributeAccessCounter()
    last = AttributeAccessCounter()

    def __init__(self, first, last):
        self._first = first
        self._last = last


john = Person("John", "Smith")
foo = Person("foo", "bar")

print(Person.first.count(john))  # 0
print(john.first)                # 'John'
print(Person.first.count(john))  # 1
print(Person.first.count(foo))   # 0
Sign up to request clarification or add additional context in comments.

2 Comments

My initial approach to per-instance counting was much like yours, but the downside is that the counter defaultdict may end up holding references to instances of the underlying class indefinitely, and without a way to clear this it will leak memory, hence I opted for the more straightforward approach. That said, this approach can be made work in a manner that avoids potential memory leakage if weakref is used.
@metatoaster Yes that was good point. weakref.WeakKeyDictionary can save us.
2

The direct access via john.first.counter cannot be done, as the descriptor __get__ will return the str with how it's done. There is a way around this, by modifying the method as per this answer.

    def __get__(self, instance, owner):
        if instance is None:
            return self
        self._access_count += 1
        return getattr(instance, self.attrib_name)

The value of the counter may be retrieved by type(john).first.counter as per the question, or alternatively Person.first.counter. Note that the counter for the attribute access is shared across all instances of the underlying object in this implementation, as the counter is stored on the descriptor object itself:

john = Person('John','Smith')
mary = Person('Mary','Jane')
print(john.first)
print(mary.first)
print(type(john).first.counter)
print(type(mary).first.counter)

The above will now output the following:

John
Mary
2
2

If you want individual counter per instance, the relevant counter will need to be stored onto the instance also, perhaps something like this if both are desired:

class AttributeAccessCounter:
    def __init__(self):
        self._access_count = 0

    def __get__(self, instance, owner):
        if instance is None:
            return self
        self._access_count += 1
        setattr(
            instance,
            f'{self.attrib_name}_counter',
            getattr(instance, f'{self.attrib_name}_counter', 0) + 1,
        )
        return getattr(instance, self.attrib_name)

    def __set_name__(self, obj, name):
        self.attrib_name = f'_{name}'

    @property
    def counter(self):
        return self._access_count

    def count(self, instance):
        """
        This counts the number of times the attribute has been
        accessed for the particular instance.
        """
        return getattr(instance, f'{self.attrib_name}_counter', 0)

With this new test program:

john = Person('John','Smith')
mary = Person('Mary','Jane')
print(john.first)
print(mary.first)
print(mary.first)
print(Person.first.counter)
print(Person.first.count(john))
print(Person.first.count(mary))
print(Person.last.count(mary))

Generates the following output:

John
Mary
Mary
3
1
2
0

Comments

0

As you may see here:

https://docs.python.org/3/howto/descriptor.html#customized-names

An option could be to use


print(vars(Person)['first']._access_count)

which will give you the underlying object without triggering the __get__

notice we call vars on the object's class

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.