reset password
Author Message
rabbott
Posts: 1649
Posted 20:38 Jan 25, 2019 |

Kevin Marlis asks this question.

This is from the code for Hearts in Hjelle's Type tutorial. (The full code is here.)

This is a method in 

class Deck(Sequence[Card]):

...

def __getitem__(self, key: Union[int, slice]) -> Union[Card, "Deck"]:
        if isinstance(key, int):
            return self.cards[key]
        elif isinstance(key, slice):
            cls = self.__class__
            return cls(self.cards[key])
        else:
            raise TypeError("Indices must be integers or slices")

 

When the key is an instance of slice, what does the line cls = self.__class__ accomplish? 

Last edited by rabbott at 20:40 Jan 25, 2019.
rabbott
Posts: 1649
Posted 20:48 Jan 25, 2019 |

The key is that a Slice is an object, like everything else in Python.

The line cls = self.__class__ makes cls the Deck class.

What is returned is a Deck instance whose cards are the specified slice of the cards from the object executing the __getitem__ method.

In other words, this retrieves a slice of the cards in the current Deck. The slice is returned as the cards in a newly created Deck object. 

Last edited by rabbott at 20:53 Jan 25, 2019.
kmarlis
Posts: 35
Posted 13:25 Jan 26, 2019 |

The return statement makes sense to me, the type annotations are really helpful with that. Is the cls of Deck initially Sequence, as far as typing is concerned? I think that is where my confusion is. 

jpatel77
Posts: 44
Posted 14:34 Jan 26, 2019 |

The class Deck inherits from Sequence of Cards. Therefore, Desk is a Sequence of Cards, with some added functionalities like play, deal, add_card (essentially everything that is defined in the class def of Deck) that are not available to the normal Sequence[Card] class/object. If you remove everything from the class def of Deck, it is just as good as the Sequence[Card]. And so, yes you can say Desk is a Sequence of Cards (all the time; not just initially) but not the other way around. That is how I understand it.

Last edited by jpatel77 at 14:34 Jan 26, 2019.
kmarlis
Posts: 35
Posted 14:49 Jan 26, 2019 |

Right, I understand that, but I'm still not sure why the cls = self.__class__ line is needed. Isn't cls already Deck without the need to call for self.__class__?

jpatel77
Posts: 44
Posted 15:01 Jan 26, 2019 |

Oh I see what you're saying. Actually it is a clever way to avoid hard-coded initialization. cls(self.cards[key]) actually creates a new object of Deck, equivalent to the statement Deck(self.cards[key]) , however, to avoid explicitly stating the class name Deck, he fetched it from self.__class___ . Because if Deck is renamed to something else, or some other class inherits from Deck and object of that class invokes __getitem__ then it should create an instance of appropriate class. I am pretty sure that is why he did this, however I would really want others to comment on that.

Last edited by jpatel77 at 15:01 Jan 26, 2019.
rabbott
Posts: 1649
Posted 15:05 Jan 26, 2019 |

cls is just a local variable.  It has no default value. Since we know we are in a Deck object, we could have skipped that line and used Deck in the next line instead of cls.

rabbott
Posts: 1649
Posted 15:07 Jan 26, 2019 |

Just read Jay's comment. Makes sense as a way to be extra cautious. 

kmarlis
Posts: 35
Posted 15:11 Jan 26, 2019 |

Ah okay, that makes sense. I couldn't find anything else explicitly declaring cls in that way before using cls() to create a new instance, and couldn't think of why it would be necessary, but that is pretty clever (and very forward thinking in a way that I wasn't expecting). Thanks for clarifying!

rabbott
Posts: 1649
Posted 16:06 Jan 26, 2019 |

This discussion is going on for quite a while, but ... we can make a teaching moment out of it.

 One can make a good case for the cls line.

 One of the basic principles of good programming style is known as DRY (Don't repeat yourself). Suppose you have some code that does a computation and you find that you need that same computation elsewhere in your program. You might be tempted to copy and paste the code. But that's bad style -- and marks whoever does it as a poor programmer. Instead of copying and pasting, extract the code and make, e.g., a function out of it. Then refer to that function from both places where you need that computation.

The reason not to copy and paste is so that if you ever need to change the code, you won't have to remember to change it everywhere you copied it.

Consider these two versions of the portion of the code in question.

if isinstance(key, slice):
   
cls = self.__class__
    return 
cls(self.cards[key])

if isinstance(key, slice):
    return Deck(self.cards[key])

The problem with the second version is that it results from copying and pasting Deck, the class name. That violates DRY.  A problem might arise if you decided to create a class called Hand for the hand a player holds. If you did that, you would have to change Deck to Hand in two places: the class definition and this bit of code. Forgetting to change it creates a bug. (Jay made a similar point above.)

Writng the code as Hjelle did respects DRY because it refers to the class name rather than copying it.

Since Hjelle is a strong coder and cares about the quality of his code, he may have decided that not violating DRY, even in a minor case like this, was worth the extra line.

One can also make a good case against the cls line.

The second version of the code is simpler and easier to understand than the original. One of the principles of writing Pythonic code is to write code that is simple and easy to understand. So the code that Hjelle wrote is not as Pythonic as it might be.

How should the conflict between these principles be resolved in this case? You tell me.

Last edited by rabbott at 20:21 Jan 26, 2019.
kmarlis
Posts: 35
Posted 13:13 Jan 27, 2019 |

I just realized what was tripping me up, and it is a pretty obvious mistake on my part.

__getitem__() is passed self, not cls as an argument. For some reason I kept seeing that first argument as cls. Whoops!