2

Let us have an example class Foo in Python:

class Foo:
    bar = 'bar'
    def access_bar(self):
        return self.bar  

Can I, for example print a warning, when accessing Foo().bar directly, but at the same time not print this warning when calling Foo().access_bar(), which accesses that attribute from within the class?

I tried to implement __getattribute__ method, but with no luck with differentiating these cases.

I know it's quite a strange question, but please don't answer me like 'You should not need this'.

Josef Kvita
  • 156
  • 1
  • 13
  • `Foo().access_bar()` is not the same as `Foo.access_bar()` – Adelin Feb 20 '19 at 09:03
  • 1
    You appear to have a motivation for asking your question. It could be helpful if you explain why this distinction is necessary (even if it's just curiosity!) – holdenweb Feb 20 '19 at 09:04
  • Do you really want a *class* attribute as opposed to an instance attribute (`self.bar = 'bar'`)? Are you confident you know the difference? – Alex Hall Feb 20 '19 at 09:09

4 Answers4

2

you could make bar a property which allows to control the access without showing the method call to the outside, and make your attribute private:

class Foo:
    __bar = 'bar'
    @property
    def bar(self):
        print("direct access")
        return Foo.__bar
    def access_bar(self):
        return self.__bar

f = Foo()

print("warn",f.bar)
print("OK",f.access_bar())

prints:

direct access
warn bar
OK bar
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • I know this way. But I access the ` __bar` a lot from the inside and I didn't like the underscores (and PyCharm warnings about using them). So I try to implement a way that I don't have to use underscores inside the class, but the user get's warned when he tries to access that attribute from the outside. – Josef Kvita Feb 20 '19 at 09:20
  • 1
    @JosefKvita keep in mind that [there are no private class variables in python](https://stackoverflow.com/questions/1641219/does-python-have-private-variables-in-classes). No matter how much you try to hide the internal variable that holds the value, someone can find a way to access it directly. If this is an attempt to deter other engineers from accessing sensitive fields, maybe try having a conversation with them instead :) – darksky Feb 20 '19 at 09:24
  • I know that. I just wanna print a warning, not make it not accessible. :) – Josef Kvita Feb 20 '19 at 09:32
  • @JosefKvita why are you accessing `__bar` a lot from the inside? Shouldn't the only direct access be from `access_bar`? If not, my answer won't help you, it doesn't differentiate between inside and outside the class. – Alex Hall Feb 20 '19 at 09:39
2

Here is the 'real' answer to your question, which you probably shouldn't do:

import inspect


class Foo:
    bar = 'bar'

    def access_bar(self):
        return self.bar

    def __getattribute__(self, item):
        if item == 'bar':
            code = inspect.currentframe().f_back.f_code
            if not (start_lineno <= code.co_firstlineno <= end_lineno
                    and code.co_filename == __file__):
                print('Warning: accessing bar directly')
        return super().__getattribute__(item)


lines, start_lineno = inspect.getsourcelines(Foo)
end_lineno = start_lineno + len(lines) - 1

print(1, Foo().bar)
print(2, Foo().access_bar())

If you do this it's important that there's only one class named Foo in the file, otherwise inspect.getsourcelines(Foo) may not give the right result.

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
  • 1
    `print(Foo.bar)` does not print a warning. This only works on instances of the class `Foo`. You will need a [metaclass](https://stackoverflow.com/questions/9484634/override-class-attribute-getter) if you want this to work also for class attributes. – darksky Feb 20 '19 at 09:29
  • I think this is exactly what I needed, thanks a lot! I'll try to play with it. I know there had to be something, because everything can be done in Python! :-) – Josef Kvita Feb 20 '19 at 09:33
  • good call, I didn't even dare tell the OP to use inspect module for this. "which you probably shouldn't do" gives you my vote :) – Jean-François Fabre Feb 20 '19 at 09:35
  • Now it checks if the attribute was called from outside the access_bar() method, what if I wanted to check for outside the Foo class (bar accessed in any method inside the class wouldn't print the warning)? – Josef Kvita Feb 20 '19 at 09:42
  • How about instead of using `inspect` to find the origin of the call to `__getattribute__`, [modifying it slightly](https://stackoverflow.com/a/54783680/1362294) so that it learns to print warning? I have no idea how good an idea that is. – darksky Feb 20 '19 at 10:08
1

I suggest storing the value in a protected (one leading underscore) or private (two underscores) attribute, and making bar a property that can be accessed safely, the equivalent of access_bar in your question. That's how this sort of thing is typically done in Python.

class Foo:
    _bar = 'bar'

    @property
    def bar(self):
        # do extra things here
        return self._bar

A user can still write foo._bar or foo._Foo__bar (for a private attribute) to get the attribute externally without any warning, but if they are aware of the conventions surrounding leading underscores they will probably feel somewhat uncomfortable doing so and be aware of the risks.

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
1

Here's another attempt at improving Alex's answer by adding a metaclass so that it also works for class attributes, and doing away with inspect module, and instead add a warning flag to the __getattribute__ function itself.

class FooType(type):
    def __getattribute__(self, item):
        if item == "bar":
            print("Warning: accessing bar directly from class")
        return item.__getattribute__(self, item)

class Foo(object, metaclass=FooType):
    bar = 'bar'

    def access_bar(self):
        return self.__getattribute__('bar', warn=False)

    def __getattribute__(self, item, warn=True):
        if item == 'bar' and warn:
            print('Warning: accessing bar directly from instance')
        return super().__getattribute__(item)


print(Foo.bar)
#Warning: accessing bar directly from class
#bar

print(Foo().bar)
#Warning: accessing bar directly from instance
#bar

print(Foo().access_bar())
#bar
darksky
  • 1,955
  • 16
  • 28
  • The bad thing on this is that I have to access the attribute everywhere inside class (like on 100 places), with a `self.__getattribute__('bar', warn=False)`, not just `self.bar`, or am I wrong? That's surely worse than `self._bar`. – Josef Kvita Feb 20 '19 at 10:53
  • That's right. I think this solution protects you from using `instance.bar` everywhere, including inside the class. It encourages you to replace `self.bar` with `self.access_bar()`. – darksky Feb 20 '19 at 10:54