-2

I would like to write a custom list class in Python 3 like in this question How would I create a custom list class in python?, but unlike that question I would like to implement __get__ and __set__ methods. Although my class is similar to the list, but there are some magic operations hidden behind these methods. And so I would like to work with this variable like with list, like in main of my program (see below). I would like to know, how to move __get__ and __set__ methods (fget and fset respectively) from Foo class to MyList class to have only one class.

My current solution (also, I added output for each operation for clarity):

class MyList:

    def __init__(self, data=[]):
        print('MyList.__init__')
        self._mylist = data 

    def __getitem__(self, key):
        print('MyList.__getitem__')
        return self._mylist[key]

    def __setitem__(self, key, item):
        print('MyList.__setitem__')
        self._mylist[key] = item

    def __str__(self):
        print('MyList.__str__')
        return str(self._mylist)


class Foo:

    def __init__(self, mylist=[]):
        self._mylist = MyList(mylist)

    def fget(self):
        print('Foo.fget')
        return self._mylist

    def fset(self, data):
        print('Foo.fset')
        self._mylist = MyList(data)

    mylist = property(fget, fset, None, 'MyList property')


if __name__ == '__main__':
    foo = Foo([1, 2, 3])
    # >>> MyList.__init__
    print(foo.mylist)
    # >>> Foo.fget
    # >>> MyList.__str__
    # >>> [1, 2, 3]
    foo.mylist = [1, 2, 3, 4]
    # >>> Foo.fset
    # >>> MyList.__init__
    print(foo.mylist)
    # >>> Foo.fget
    # >>> MyList.__str__
    # >>> [1, 2, 3, 4]
    foo.mylist[0] = 0
    # >>> Foo.fget
    # >>> MyList.__setitem__
    print(foo.mylist[0])
    # >>> Foo.fget
    # >>> MyList.__getitem__
    # >>> 0

Thank you in advance for any help.


How to move __get__ and __set__ methods (fget and fset respectively) from Foo class to MyList class to have only one class?


UPD:

Thanks a lot to @Blckknght! I tried to understand his answer and it works very well for me! It's exactly what I needed. As a result, I get the following code:

class MyList:
    def __init__(self, value=None):
        self.name = None
        if value is None:
            self.value = []
        else:
            self.value = value

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

    def __set__(self, instance, value):
        setattr(instance, self.name, MyList(value))

    def __getitem__(self, key):
        return self.value[key]

    def __setitem__(self, key, value):
        self.value[key] = value

    def append(self, value):
        self.value.append(value)

    def __str__(self):
        return str(self.value)


class Foo:
    my_list = MyList()

    def __init__(self):
        self.my_list = [1, 2, 3]
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list = [4, 5, 6, 7, 8]
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list[0] = 10
        print(type(self.my_list))       # <class '__main__.MyList'>
        self.my_list.append(7)
        print(type(self.my_list))       # <class '__main__.MyList'>
        print(self.my_list)             # [10, 5, 6, 7, 8, 7]

foo = Foo()

I don't know, that's Pythonic way or not, but it works as I expected.

Community
  • 1
  • 1
Roman Alexeev
  • 339
  • 3
  • 16
  • 1
    "but unlike that question I would like to implement `__get__` and `__set__` methods" - why would you do that? Are you sure you understand what `__get__` and `__set__` are? – user2357112 Apr 03 '17 at 02:34
  • 1
    Instead of asking about how to use `__get__` and `__set__`, can you explain what *behavior* you want to achieve? Why are you using `__get__` and `__set__` in your existing example? What do you want your list class to be able to do that it won't do if you just remove `Foo` and work directly with a `MyList` instance? – BrenBarn Apr 03 '17 at 02:36
  • What is your question? – wwii Apr 03 '17 at 02:38
  • @BrenBarn If I would removed `Foo` class, then I couldn't use assignment operator `foo.mylist = [1, 2, 3, 4]`, because in this case `foo.mylist` will become simple `list`. And I need to hide some other methods behind this operation. For example, I work with remote devices and when I invoke `foo.mylist = [...]` I would like, that program write this to these devices, but not my local variable. – Roman Alexeev Apr 03 '17 at 02:48
  • @wwii How to move `__get__` and `__set__` methods (`fget` and `fset` respectively) from `Foo` class to `MyList` class to have only one class? – Roman Alexeev Apr 03 '17 at 02:49
  • 2
    @RomanAlexeev: What are you even imagining that would do? If you think you'd be able to do something like `x = MyList([1]); x = [2]` and have `x` be a `MyList` after that, no. Python doesn't work that way. You can't override the assignment operator like you can in something like C++. – user2357112 Apr 03 '17 at 02:54
  • @user2357112 I work with remote devices and when I invoke `foo.mylist = [...]` I would like, that program write list `[...]` to these devices, but not my local variable. These operations would be in these class. But I would like to work with that variable as if it's local variable. I have other similar variables, but they are `int` and there I could use decorator `@property` and `@x.setter`, But in case of `list` I need to implement not only `__set__` and `__get__`, but `__getitem__` and `__setitem__` too. – Roman Alexeev Apr 03 '17 at 02:55
  • 1
    What you want is still not clear. You say you want to "have only one class", but you also say you want to do `foo.mylist = ...`. You obviously can't do `foo.mylist = ...` without having `foo` be something different from whatever `foo.mylist` is. So, if you had "only one class", how would you want to use it? – BrenBarn Apr 03 '17 at 03:00
  • @BrenBarn Sorry, it's typo. `mylist = [...]` instead of `foo.mylist = [...]`, where my `mylist = MyList()`. – Roman Alexeev Apr 03 '17 at 03:06
  • @BrenBarn I would like exactly that @user2357112 wrote: `x = MyList([1]); x = [2]` and have `x` be a `MyList` after that. – Roman Alexeev Apr 03 '17 at 03:08
  • 1
    @RomanAlexeev You can't do that. The `__get__` and `__set__` methods override the assignment of the class/instance attribute, not as a variable in the local or global namespace. – Pedro Werneck Apr 03 '17 at 03:11
  • Thanks to everyone. I tried to implement it, but if it's impossible, as @user2357112 said, then I won't use it. It's helpful too. – Roman Alexeev Apr 03 '17 at 03:12
  • @Pedro Werneck Thank you too. I probably misunderstood these attributes. – Roman Alexeev Apr 03 '17 at 03:17

2 Answers2

2

In a comment, you explained what you actually want:

x = MyList([1])
x = [2]
# and have x be a MyList after that.

That is not possible. In Python, plain assignment to a bare name (e.g., x = ..., in contrast to x.blah = ... or x[0] = ...) is an operation on the name only, not the value, so there is no way for any object to hook into the name-binding process. An assignment like x = [2] works the same way no matter what the value of x is (and indeed works the same way regardless of whether x already has a value or whether this is the first value being assigned to x).

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
1

While you can make your MyList class follow the descriptor protocol (which is what the __get__ and __set__ methods are for), you probably don't want to. That's because, to be useful, a descriptor must be placed as an attribute of a class, not as an attribute of an instance. The properties in your Foo class creating separate instances of MyList for each instance. That wouldn't work if the list was defined on the Foo class directly.

That's not to say that custom descriptors can't be useful. The property you're using in your Foo class is a descriptor. If you wanted to, you could write your own MyListAttr descriptor that does the same thing.

class MyListAttr(object):
    def __init__(self):
        self.name = None

    def __set_name__(self, owner, name):     # this is used in Pyton 3.6+
        self.name = "_" + name

    def find_name(self, cls):  # this is used on earlier versions that don't support set_name
        for name in dir(cls):
            if getattr(cls, name) is self:
                self.name = "_" + name
                return
        raise TypeError()

    def __get__(self, obj, owner):
        if obj is None:
           return self
        if self.name is None:
            self.find_name(owner)
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if self.name is None:
            self.find_name(type(obj))
        setattr(obj, self.name, MyList(value))

class Foo(object):
    mylist = MyListAttr() # create the descriptor as a class variable

    def __init__(self, data=None):
        if data is None:
            data = []

        self.mylist = data    # this invokes the __set__ method of the descriptor!

The MyListAttr class is more complicated than it otherwise might be because I try to have the descriptor object find its own name. That's not easy to figure out in older versions of Python. Starting with Python 3.6, it's much easier (because the __set_name__ method will be called on the descriptor when it is assigned as a class variable). A lot of the code in the class could be removed if you only needed to support Python 3.6 and later (you wouldn't need find_name or any of the code that calls it in __get__ and __set__).

It might not seem worth writing a long descriptor class like MyListAttr to do what you were able to do with less code using a property. That's probably correct if you only have one place you want to use the descriptor. But if you may have many classes (or many attributes within a single class) where you want the same special behavior, you will benefit from packing the behavior into a descriptor rather than writing a lot of very similar property getter and setter methods.

You might not have noticed, but I also made a change to the Foo class that is not directly related to the descriptor use. The change is to the default value for data. Using a mutable object like a list as a default argument is usually a very bad idea, as that same object will be shared by all calls to the function without an argument (so all Foo instances not initialized with data would share the same list). It's better to use a sentinel value (like None) and replace the sentinel with what you really want (a new empty list in this case). You probably should fix this issue in your MyList.__init__ method too.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Many thanks for your answer! It really helped me. I don't fully understand this magic, but I tried to implement it for my class, using Python 3.6, and it works as I needed. I updated my question in accordance with your answer. – Roman Alexeev Apr 03 '17 at 22:55
  • Um, there's a bunch of weird stuff in your implementation. You're using the `name` attribute both for the descriptor variable name, and for the actual data. That doesn't make any sense. My design is for the descriptor to be a different type than the list, since you don't use them in the same places. – Blckknght Apr 03 '17 at 23:08
  • It seems weird for me too. But I couldn't implement it in other way. In your code `setattr(obj, self.name, MyList(value))` (`MyList` -> `MyListAttr`, I think, it's typo) you pass `value`, but you couldn't, because `__init__` has only one argument `self`, as I understand. It raised error: `TypeError: __init__() takes 1 positional argument but 2 were given`. Could you explain, how to pass argument with value in the right way? – Roman Alexeev Apr 04 '17 at 00:02
  • Thank you for your remarks! It seems I understood my mistakes, that you wrote about. I edited my code in accordance with your remarks. I will be very appreciate, if you look my edited code and say, is it right way or not. – Roman Alexeev Apr 04 '17 at 00:51
  • I think the misunderstanding is that I intend `MyListAttr` to be a separate class from `MyList` (I didn't show that in my code because it doesn't need any changes from what you had in your original code). You don't want the same class to be used as the container (with `__getitem__` and `__setitem__` methods) and the descriptor (with `__get__` and `__set__`). Having the `MyListAttr` create a `MyList` (which you thought was a typo) was a deliberate thing! – Blckknght Apr 04 '17 at 03:33