1

Consider this code:

def gee(bool_, int32, int64, str_):

    class S:
        bool_ = bool_
        int32 = int32
        int64 = int64
        str_ = str_

    return S

gee(1, 2, 3, 4)

Running this gives an error:

Traceback (most recent call last):
  File "test_.py", line 36, in <module>
    gee(1, 2, 3, 4)
  File "test_.py", line 27, in gee
    class S:
  File "test_.py", line 28, in S
    bool_ = bool_
NameError: name 'bool_' is not defined

I have no idea which scope/closure rules apply here. nonlocal fixes the error but the result is not what I've expected:

def gee(bool_, int32, int64, str_):

    class S:
        nonlocal bool_, int32, int64, str_
        bool_ = None
        int32 = None
        int64 = None
        str_ = None
    print(bool_, int32, int64, str_ )

    return S

g = gee(1, 2, 3, 4)
g.bool_

Outputs:

None None None None
Traceback (most recent call last):
  File "test_.py", line 38, in <module>
    g.bool_
AttributeError: type object 'S' has no attribute 'bool_'

Beside rename what can I do to make assignments in 1st code snippet work? And why it behaves like that? Because there is name = ...? Why Python doesn't evaluate the name before assignment?

WloHu
  • 1,369
  • 17
  • 24

6 Answers6

2

Within a scope, if you assign to a name, it is local within that scope. A class body is a scope. Thus, _bool is local, and therefore attempting to assign _bool to it is an error because _bool hasn't been defined yet. Unlike some other languages, Python does not look in outer scopes when a name has not yet been defined in a scope.

nonlocal, as you note, doesn't solve the problem because it causes the outer scope's name to be assigned to.

The easy answer is to use different names.

Another possibility, if you'll be using these values only in methods of S, is to simply use a closure and access the values within the methods as though they were local values. Then you don't need them as attributes on the class at all.

kindall
  • 178,883
  • 35
  • 278
  • 309
  • Class body doesn't really create a new scope per say. This is why Python' has LEGB scope rules not LCEGB (or something). But as you said the point is that `bool_ =` makes Python decide that name is inside class' namespace and to that point it hasn't been defined. – WloHu Feb 20 '19 at 09:24
2

When the class is compiled similar rules to function are used to resolve names. Because the class body contains an assignment to the bool_ variable the interpreter regards it as a class variable, and therefore looks in the class namespace when asked to resolve its value on the right-hand side of the assignment.

One possibility would be to make them instance variables, assigned in the __init__ method, defaulting the arguments to the values provided in the call to gee:

def gee(bool_, int32, int64, str_):

    class S:
        def __init__(self, bool_=bool_, int32=int32, int64=int64, str_=str_):
            self.bool_ = bool_
            self.int32 = int32
            self.int64 = int64
            self.str_ = str_

    return S

my_class = gee(1, 2, 3, 4)
my_instance = my_class()

print(my_instance.bool_)

You should find this code prints 1. The reason this is possible is that the named parameters resolve the value expressions using standard (lexical) scoping rules, while the parameter names themselves are injected into the namespace at the start of each call.

You will find the code also works if you instead bind the values to class variables using assignments of the form

S.bool_ = bool_

because, by the time the __init__ method runs, the classname S has already been bound in the lexically enclosing namespace. Even though the values are bound to the class, they can be referenced relative to the instance - since they aren't present there the interpreter continues looking in the instance's class.

In either case, however, a call to the class is required to set the attribute values inside __init__.

holdenweb
  • 33,305
  • 7
  • 57
  • 77
  • 1
    Well explained. As you said, it seems that scope rules here are the same as in nested function with `bool_ = bool_`-like call. I thought it would work different in this case because classes are namespaces and class' body doesn't create new scope. Do you have documentation source to support your 1st statement or is it only in the cPython source code? – WloHu Feb 20 '19 at 09:07
  • 1
    I guess the logical candidate would be [this page](https://docs.python.org/3.7/tutorial/classes.html#python-scopes-and-namespaces) - the documentation on scopes and namespaces. – holdenweb Feb 20 '19 at 09:26
  • Yup, here is it: "*Class definitions place yet another namespace in the local scope.*". But to me from that statement only it would be hard to get real implications without the code. – WloHu Feb 20 '19 at 09:42
  • That section could probably use a review - we've had a couple of remarks about its nature at the Python webmaster address. – holdenweb Feb 20 '19 at 09:49
1

Let's answer your question with another question - how do you suppose Python knows which bool_ is which in the bool_ = bool_ line?

def gee(bool_, int32, int64, str_):

    class S:
        bool_ = bool_

Is the first bool_ a class attribute? Or is it the nonlocal bool_ from the function? Or is it *gasps* a global object? What about the assigned object, which scope is the = bool_ supposed to refer to?

That's why Python's mantra is "Explicit is better than Implicit". The scope should be well defined for Python to understand which you are referring to, otherwise the most immediate scope is assumed by the interpreter.

But you already know the solution to this one - simply renaming it to a different name will solve the problem, because it explicitly tells the Python Interpreter which scope the object is referring to.

Another way is you could move the class attribute definition outside of the class scope:

def gee(bool_, int32, int64, str_):

    class S:
        pass

    S.bool_ = bool_
    S.int32 = int32
    ...
    return S

This way you could clearly define which bool_ belongs to which scope.

r.ook
  • 13,466
  • 2
  • 22
  • 39
  • 1
    Well pointed that Python doesn't make guesses and my construct misses some explicit statements to tell interpreter exactly what I want. I accepted @holdenweb answer because it was posted before yours but your rhetorical questions are really valuable. – WloHu Feb 20 '19 at 09:11
  • Just a note, while @holdenweb provided a nice answer, if the definitions are wrapped inside the `def __init__()` the instance attributes will not be available until the class is instantiated. If you need to have the class attributes available *before* instantiation, you can try my method (that doesn't mean you need to accept my answer - you pick whichever suits your solution best). – r.ook Feb 20 '19 at 14:37
  • Actually there is class attributes case you mentioned in @holdenweb answer. It just takes less space than instance attribute example. – WloHu Feb 21 '19 at 09:35
  • You are right, I was too focused on the block of code. Either way if it solves your problem then it's all good, cheers! – r.ook Feb 21 '19 at 14:33
0

Variable naming and scope is your issue. If you slightly change your code to this it should work.

def gee(some_bool, some_int32, some_int64, some_str):

    class S:
        bool_ = some_bool
        int32 = some_int32
        int64 = some_int64
        str_ = some_str

    return S

print(gee(1, 2, 3, 4))
# 1, 2, 3, 4
0

You can use this code. Without changing the name of your variables. you can use properties and class constructor to solve this problem.

def gee(bool_, int32, int64, str_):

    class S:
        def __init__(self, bool_, int32, int64, str_):
            self.bool_ = bool_
            self.int32 = int32
            self.int64 = int64
            self.str_ = str_

        def get_bool_(self):
            return self.bool_

        def set_bool_(self, bool_):
            self.bool_ = bool_

        def get_int32(self):
            return self.int32

        def set_int32(self, int32):
            self.int32 = int32

        def get_int64(self):
            return self.int64

        def set_int64(self, int64):
            self.int64 = int64

        def get_str_(self):
            return self.str_

        def set_str_(self, str_):
            self.str_ = str_

    print(bool_, int32, int64, str_ )

    return S
g = gee(1, 2, 3, 4)
  • To be pythonic use @property decorator with getter and setter https://stackoverflow.com/questions/2627002/whats-the-pythonic-way-to-use-getters-and-setters – Mohammed Elmahgiubi Feb 19 '19 at 19:21
0

One possibility would be to make them instance variables, assigned in the init method, defaulting the arguments to the values provided in the call.