1

I am new to decorators and believe I understand their purpose/the general gist of them for the most part. Unfortunately, I am having some issues with this decorator and its usage in a class. I am trying to populate a dictionary _profile_dict = {} with a new key created by profile + str(<counter value>). I'd rather not instantiate the class and set it to a variable in my if __name__ == '__main' statement so I am trying to execute the class with the create_profiles function. Here is my code,

_profile_dict = {}


class Profile:
    def __init__(self):
        self.first_name = input('first name: ')
        self.last_name = input('last name: ')
        self.email = input('email: ')
        self.address1 = input('primary address: ')
        self.address2 = input('secondary address: ')
        self.city = input('city: ')
        self.country = input('country: ')
        self.province = input('province: ')
        self.zip = input('zip: ')
        self.phone = input('phone number:')
        self.cc_name = input('Name on credit card: ')
        self.cc_num = input('Credit card number: ')
        self.exp_month = input('card expiration month: ')
        self.exp_year = input('card expiration year: ')
        self.cvv = input('cvv: ')

        self.json_obj = {"utf8": "✓",
                          "_method": "",
                          "authenticity_token": "",
                          "previous_step": "",
                          "checkout[email]": self.email,
                          "checkout[buyer_accepts_marketing]": 1,
                          "checkout[shipping_address][first_name]": self.first_name,
                          "checkout[shipping_address][last_name]": self.last_name,
                          "checkout[shipping_address][address1]": self.address1,
                          "checkout[shipping_address][address2]": self.address2,
                          "checkout[shipping_address][city]": self.city,
                          "checkout[shipping_address][country]": self.country,
                          "checkout[shipping_address][province]": self.province,
                          "checkout[shipping_address][zip]": self.zip,
                          "checkout[shipping_address][phone]": self.phone,
                          "step": ""}

    def counter(self, func):
        def wrapper():
            wrapper.counter += 1
            return func(wrapper.counter)

        wrapper.counter = 0
        return wrapper

    @counter
    def _populate_profile_dict(self, count):
        profile = 'profile'
        _profile_dict[profile + str(count)] = self.json_obj
        print(_profile_dict)


def create_profiles():
    Profile()._populate_profile_dict()

if __name__ == "__main__":
    create_profiles()

At the moment, I am getting the following error,

Traceback (most recent call last):
  File "C:/Users/Carsten/Profile_Gen/src/config.py", line 6, in <module>
class Profile:
  File "C:/Users/Carsten/Profile_gen/src/config.py", line 49, in Profile
    @counter
TypeError: counter() missing 1 required positional argument: 'func'

Although from what I just read over here, Python decorators in classes , I'm assuming this is not possible. Is it possible to accomplish what I am trying to acheive with @classmethod at all?

Just by changing the last 3 functions/methods to this, still leads to no output:

@classmethod
def counter(cls, func):
    def wrapper():
        wrapper.counter += 1
        return func(wrapper.counter)

    wrapper.counter = 0
    return wrapper

def _populate_profile_dict(self, count):
    profile = 'profile'
    _profile_dict[profile + str(count)] = self.json_obj
    print(_profile_dict)

#def create_profiles():
#    Profile()._populate_profile_dict()


if __name__ == "__main__":
    Profile()._populate_profile_dict = 
    Profile.counter(Profile._populate_profile_dict)

Any help would be appreciated. Thank you for your time. I feel like decorators as well as a few other things that I have on my agenda will definitely help propel me into more of an intermediate stage of python.

UCProgrammer
  • 517
  • 7
  • 21
  • Can you give us a [mcve]? What you've shown here is a classmethod written outside of any class, a normal method written outside of any class, a commented-out function, and some code that appears to be trying to construct an object, call a classmethod on an attribute that doesn't exist yet, and then assign the result to that attribute, and then throw away the object you just worked so hard to modify, so… it's kind of hard to tell what you're trying to do or what's wrong with it. – abarnert Aug 23 '18 at 00:04
  • @abarnert oh crap. My apologies. The rest of my code didn't get pasted over – UCProgrammer Aug 23 '18 at 00:09
  • No problem; that’s what the [edit] button is for. :) – abarnert Aug 23 '18 at 00:13
  • @abarnert fixed it. My bad – UCProgrammer Aug 23 '18 at 00:14
  • Posting all of your code is still far from a [mcve]. Especially since, if anyone wanted to actually run your code to debug it, they'd have to sit through typing in more than a dozen `input` prompts before seeing what happens. – abarnert Aug 23 '18 at 00:59
  • the input doesn't matter. you could just hit enter for all of them – UCProgrammer Aug 23 '18 at 01:09

2 Answers2

2

Let me ignore your second attempt for now, because the first one was a lot closer to working:

def counter(self, func):
    def wrapper():
        wrapper.counter += 1
        return func(wrapper.counter)

    wrapper.counter = 0
    return wrapper

@counter
def _populate_profile_dict(self, count):
    # ...

The reason this doesn't work is that decorator syntax is trying to do this:

_populate_profile_dict = counter(_populate_profile_dict)

… but counter is defined to take two arguments, not one, self and a function. There's obviously no self here. In fact, even the class itself doesn't exist yet, much less any methods. You're calling it with just the function, so that's what it needs to take.

And if you just remove that self parameter, the decorator part will work just fine:

def counter(func):
    def wrapper():
        wrapper.counter += 1
        return func(wrapper.counter)

    wrapper.counter = 0
    return wrapper

Now, that counter is no longer useful as a method on instances of the class—but that's fine, since you never intend to use it that way.

Meanwhile, it's perfectly useful as a normal function while the class is still being defined. And that's exactly the way you want to use it.

It might be a bit confusing that counter ends up looking like a public method of Profile instances, even though it's not actually callable that way, which is pretty confusing. But you can solve that by either renaming it to _counter, or doing a del counter at the very end of the class definition.


Anyway, the decorator part works, but there's a second problem:

def wrapper():

This defines a function with no parameters. But you're trying to create something that can be called as a normal instance method, so it's going to be passed a self argument. You need to accept that self argument, and you also need to pass it on to the wrapped function. So:

def counter(func):
    def wrapper(self):
        wrapper.counter += 1
        return func(self, wrapper.counter)
    # etc.

And now, everything works as (I think) you intended.


While we're at it, this code is a bit weird:

def create_profiles():
    Profile()._populate_profile_dict()

This creates a Profile instance, calls a private method on it, and then discards the instance. I'm not sure why you want to do that. Sure, it adds a profile1 entry to the private global _profile_dict, but what does that get you?


Coming to your second attempt now:

@classmethod
def counter(cls, func):
    def wrapper():
        wrapper.counter += 1
        return func(wrapper.counter)

    wrapper.counter = 0
    return wrapper

Now you're successfully creating a public classmethod, which you try to call explicitly as a classmethod, which is perfect:

Profile()._populate_profile_dict = 
Profile.counter(Profile._populate_profile_dict)

But it has multiple other problems:

  • Trying to split an assignment onto two lines like this is a SyntaxError.
  • Profile.counter(Profile._populate_profile_dict) returns a new function, which you don't call, so it's just assigning the function itself as the value.
  • When you do call it, it's expecting to have two arguments, a self and a count, but you're only passing the count. And here, it's not clear where you want to get a self from. Maybe you wanted to call it on an instance of Profile, so you get a self and can pass it through?
  • You're assigning the value to an instance attribute of a Profile instance that you just created and are just going to throw away, which has no visible effect.
  • That instance attribute has the same name as the method you want to call--which is legal, and doesn't cause any particular problems here, but it's pretty confusing.

The closest reasonable thing I can see to what you're trying to do is to make counter return something callable as a method, as above:

@classmethod
def counter(cls, func):
    def wrapper(self):
        wrapper.counter += 1
        return func(self, wrapper.counter)

    wrapper.counter = 0
    return wrapper

… then monkeypatch Profile with the result, and then call the method on a Profile instance, like this:

if __name__ == "__main__":
    Profile._populate_profile_dict = Profile.counter(Profile._populate_profile_dict)
    Profile()._populate_profile_dict()

And now it works, but I'm not sure if it's what you wanted.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thanks for the reply. I just wanted to populate the _profile_dict with "user profile" entries. I plan on using them at a later stage in my program as part of form data for a request. I don't think I'll need an actual instance of a class. For now, I'm more interested in learning about decorators and exploring the different ways I can populate this dict. Also, I'm using pycharm and "def wrapper(self)" has self being taken as just a regular parameter. Not a spot for an instance. – UCProgrammer Aug 23 '18 at 00:52
  • Also, that syntax error you mentioned was just a snafu with stackoverflow. Their ctrl + k never seems to work for me so I always just hit space 4 times on all my lines. Must have goofed there – UCProgrammer Aug 23 '18 at 00:57
  • 1
    @UCProgrammer If you don't need an actual instance of the class, that almost always means you didn't need a class in the first place. Meanwhile, I don't know what "a spot" means on PyCharm or whatever, but I'm not surprised that PyCharm can't guess that the thing we're defining as `wrapper` will eventually be used as a method, because we're not actually defining it as a method, but putting it into place as one after it was defined. – abarnert Aug 23 '18 at 00:58
  • by "spot" I meant how self is typically the first parameter and signifies the reference to the instance whose method is called. Pycharm always puts 'self' in purple characters while other parameters are white and slightly larger – UCProgrammer Aug 23 '18 at 01:05
  • 2
    @UCProgrammer OK, I think the best thing to do is to just ignore that. AFAIK, PyCharm bases its highlighting (and other stuff) on some pretty clever and powerful static analysis, but there's only so much you can do statically with a language as dynamic as Python, so they have to use heuristics for some things, and defining a function intended to be used as a method even though it wasn't defined as one, that probably breaks their heuristic guesses. – abarnert Aug 23 '18 at 01:21
1

If you don't need to use a decorator then you could just measure the length of your dictionary:

def _populate_profile_dict(self):
        _profile_dict['profile' + str(len(_profile_dict))] = self.json_obj
        print(_profile_dict)
  • Thanks. I had thought of that but I really would like to learn and become more familiar with decorators. I feel like this is a prime time to use one but I will result to this is there is no other way – UCProgrammer Aug 23 '18 at 00:29