Created
November 7, 2023 22:47
-
-
Save wd5gnr/daa5d2d344319b6ef17206f537f15b08 to your computer and use it in GitHub Desktop.
Issue with Python Class Variables
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is what's wrong with class variables in Python | |
# and yes, properties work (don't work) the same way | |
# This is a stupid but clean example where class A | |
# has a class variable x. It also has direct subclasses | |
# B and C | |
# Then there is a subclass of C named D | |
# None of them should have an x variable, but they all will get one | |
class A: | |
x=0 # our class variable | |
@classmethod | |
def printx(cls,msg): | |
print(msg,cls.x) | |
class B(A): | |
@classmethod | |
def testit(cls): | |
cls.x=1 | |
cls.printx('From cls') # prints 1 | |
A.printx('From B') # prints 100 | |
super().printx('From Super') # prints 1 | |
A.x=100 | |
A.printx('Case A') # prints 100 | |
B.testit() | |
A.printx('Case A1') # prints 100 | |
# So, ok you say, just make sure to call the base class... hmmm... | |
class C(A): | |
@classmethod | |
def testit(cls): | |
A.x=2 | |
cls.printx("From Ccls") # prints 3 | |
A.printx('From A') # prints 2 | |
super().printx('From Super') # prints 3 (?) | |
C.x=3 | |
A.x=200 | |
A.printx('Case A') # prints 200 | |
C.printx('Case C') # prints 3 | |
C.testit() | |
A.printx('Case A1') # print 2 | |
C.printx('case C1') # prints 3 | |
# Ok so super() is broken for class variables. S'ok, right? But now you | |
# must know where something lives which violates encapsulation because... | |
class D(C): | |
@classmethod | |
def testit(cls): | |
C.x=2 # why not? It is our baseclass -- how do I know this is in A? Why do I even know about A? | |
cls.printx("From Dcls") | |
C.printx("from C") | |
super().printx('From Super') | |
A.printx("from A") | |
A.x=999 | |
B.x=500 | |
C.x=133 | |
D.x=2000 | |
A.printx('Case A') # prints 999 | |
D.testit() | |
A.printx('A final') # 999 | |
B.printx('B final') # 500 | |
C.printx('C final') # 2 | |
D.printx('D final') # 2000 | |
# so from class D that derives from class C I must "know" that class variable x really comes from A. | |
# and I better hope that no future change puts in C or in a new base class for A. | |
# Here's an oddity: | |
class E(A): | |
@classmethod | |
def testit(cls): | |
print(cls.x,A.x) # 1234 1234 so right now cls.x is A.x | |
cls.x=3 # this creates E.x = 3 | |
print(cls.x,A.x) # 3 1234 so now cls.x != A.x | |
A.x=1234 | |
E.testit() | |
# Why is this bad behavior? | |
# Well, of cousre, someone will say it is not and that all other OO languages are wrong | |
# However, taking the opposite side, I would say that class variables are things | |
# that belong to the class as a whole. For example, say we have a bunch of People. | |
# Some are Writers and some are Engineers and some are Lawyers | |
# After I create a bunch of people I'd like to know how many of them there are. | |
class People: | |
count=0 # no people yet | |
@classmethod | |
def get_population(cls): | |
return cls.count | |
def __init__(self): | |
People.count+=1 # class variable | |
al=People() | |
mike=People() | |
lisa=People() | |
print("We have",People.get_population(),"generic people") | |
class Writer(People): | |
def __init__(self): | |
super().__init__() | |
self.job="Writer" | |
def write(): | |
print("I'm blocked") | |
class Engineer(People): | |
def __init__(self): | |
super().__init__() | |
self.job="Engineer" | |
def design(): | |
print("I'm busy") | |
class Lawyer(People): | |
def __init__(self): | |
super().__init__() | |
self.job="Lawyer" | |
def litigate(): | |
print("Please deposit $800") | |
# So... | |
julie=Engineer() | |
mary=Lawyer() | |
bob=Writer() | |
steve=Engineer() | |
print("We have",People.get_population()," people") | |
# This works because super().__init__() calls into People and it expressly uses People.count | |
# But suppose we have (yes, contrived): | |
class JoinedTwins(People): | |
def __init__(self): | |
super().__init__() | |
self.job="Twins" | |
# self.count+=1 # nope Now we have carl_n_earl.count=1, People.count=8 | |
# self.__class__.count+=1 # nope Now we have JoinedTwins.count=1, People.count=8 | |
# just for fun and assume we don't have multiple inheritence.. ugly | |
# for xcont in self.__class__.__bases__: | |
# if hasattr(xcont,'count'): | |
# xcont.count+=1 | |
# or does JointedTwins.count exist? | |
# It does. So even though we know it is there and for now | |
# reading from it gets People count, the moment we assign it, it belongs to us | |
print("Has attribute count?",hasattr(self,'count')) | |
People.count+=1 # Ok but as mentioned before, I have to know where it is (trivial here but not always) | |
def appear_on_talk_show(): | |
print("Good evening") | |
carl_n_earl=JoinedTwins() | |
print("We have",People.get_population()," people ???") | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment