Reputation: 3717
I am trying to understand Python classes before I delve into metaclasses. I have come upon some code I can't figure out. In this situation, classes are not using self but rather class namespaces (no option for using self, hence the question here). How can I define in one class some namespace variables, and then override a value they all depend on, in the child classes?
First Case
class B():
potato = "hey"
test = potato
#here would go a lot of more code that just depends on that potato value
class c(B):
B.potato = "not hey"
c_potato = c().test
print(c_potato)
It prints hey. That I understand because test is pointing to the string "hey", which is not mutable. Changing B.potato = "not hey"
only changes class namespace potato to a new string, but it doesn't change what test is pointing to. So I thought, hey what if I do it with a list, that's by reference right?
class B():
potato = ["hey"]
test = potato[0]
class c(B):
B.potato[0] = "not hey"
c_potato = c().test
print(c_potato)
In my mind, this should have worked. I have not changed what potato is pointing to, but rather the value. No? But I understand that it wont' actually work because test is pointing to potato[0] instead of just potato. So yea, I get why this also prints hey.
I realised then, if test needs to point to the result, which is not mutable, then what I"m trying to do with namespaces is impossible.
class B():
@staticmethod
def get_potato():
return "hey"
potato = get_potato.__func__
test = potato()
class c(B):
@staticmethod
def get_potato():
return "not hey"
B.potato = "6"
c_potato = c().test
print(c_potato)
I have changed here the entire value of B.potato but by now test already points to the result of the parent's potato(), so it doesn't matter and still prints out "hey".
So then I thought, could metaclasses fix this? Apparently yeah, it can.
class Meta(type):
def __new__(cls, name, bases, attrs):
x = super().__new__(cls, name, bases, attrs)
x.potato = "basic hey"
if 'meta_args' in attrs:
x.potato = attrs['meta_args'][0]
del attrs['meta_args'] # clean up
x.test = x.potato
return x
class A():
pass
class B(A, metaclass=Meta):
meta_args = ["super hey"]
pass
class C(B):
meta_args = ["not hey"]
b = B().test
c = C().test
print(b)
print(c)
And that correctly prints super hey fo b and not hey for c. Question is, could this be done without metaclasses? My brain hurts at this point.
Upvotes: 0
Views: 277
Reputation: 110561
As you have seem, you made a lot of experimentation with "surprising" results, with simple things, like assigning the first element of a list to a variable, to release you had wrong concepts about the issue.
Why do you think you have "nailed" it and got it right when you go past simple, and "find out" you can "change a value" with a metaclass?
No - just going on with your earlier trends you still have wrong assumptions.
The code in the metaclass __new__
method, just as the code in a class body is run once when each class is created. Variables are created, assigned, and not changed after that.
Your "metaclass experiment" cfeates a new value for both potato
and test
attributes by setting then on the new class at the time this new class (C) is created.
There is nothing "meta" in that that would be different from doing:
class B:
potato = 0
class C(B):
pass
C.potato = 1
print(C.potato) # Hey it works!
If you want "magic changing values", behaving like calculated values do on spreadsheet programs, Python has a mechanism to do that, and it is not related to metaclasses: these are the "descriptors". Of which the mostwell known are the created with the built-in property
.
So, descriptors do what you are trying with your exploring: they look like an ordinary attribute uon reading or writting, but can trigger and be calculated by any amount of code "behind the scenes". They are truly a mechanism that can be used to implement several aspects of "reactive programing".
But as they are meant to be used, not toyed with, the most popular form, property
is designed to work on class instances - not a silly using of classes as just namespaces.
So, I'd advise you to switch to that, and you will have a lot more of exploring to do:
class B:
def __init__(self):
self.potato = Hey
@property
def test(self):
return self.potato
c = B()
print(c.test)
c.potato = "not hey"
print(c.test)
Upvotes: 0
Reputation: 522499
You probably want a @property
:
class B:
potato = 'hey'
@property
def test(self):
return self.potato
class C(B):
potato = 'not hey'
What you did is assign "hey"
to test
in one way or another. Its value won't change unless you actually assign something else to test
. By making it a @property
, the function def test
is invoked every time you access .test
, so its value can be computed dynamically; in this case based on the value of potato
. The subclass declares its own potato
, which shadows the parent's potato
property.
Upvotes: 1