How can I use an attribute of the instance as a default argument for a method?
Asked Answered
J

6

95

I want to pass a default argument to an instance method using the value of an attribute of the instance:

class C:
    def __init__(self, format):
        self.format = format

    def process(self, formatting=self.format):
        print(formatting)

When trying that, I get the following error message:

NameError: name 'self' is not defined

I want the method to behave like this:

C("abc").process()       # prints "abc"
C("abc").process("xyz")  # prints "xyz"

What is the problem here, why does this not work? And how could I make this work?

Jose answered 15/11, 2011 at 5:34 Comment(4)
do not use format as variable name, as it is built-in function in python.Ima
Editing the mistake of self in process methodJose
There is a draft PEP in the pipeline, which would make this possible using => rather than =. It is currently slated to appear in Python 3.12.Denaedenarius
[re-opening]: even though the some of the_ways_to_resolve the issue are the same as in the question marked as a duplicate, the problem to be resolved is different in several aspects from that.Protrusive
A
110

You can't really define this as the default value, since the default value is evaluated when the method is defined which is before any instances exist. The usual pattern is to do something like this instead:

class C:
    def __init__(self, format):
        self.format = format

    def process(self, formatting=None):
        if formatting is None:
            formatting = self.format
        print(formatting)

self.format will only be used if formatting is None.


To demonstrate the point of how default values work, see this example:

def mk_default():
    print("mk_default has been called!")

def myfun(foo=mk_default()):
    print("myfun has been called.")

print("about to test functions")
myfun("testing")
myfun("testing again")

And the output here:

mk_default has been called!
about to test functions
myfun has been called.
myfun has been called.

Notice how mk_default was called only once, and that happened before the function was ever called!

Anglo answered 15/11, 2011 at 5:37 Comment(3)
I believe mk_default was called before the functions were called since foo=mk_default() called it, due to the parenthesis. Shouldn't it be foo=mk_default? Then your example might change to myfun("testing") followed by myfun().Malayopolynesian
Note that formatting = formatting or self.formatwill set formatting to self.format if formatting is a falsey value, such as 0. This just bite me. A safer way is to type formatting = formatting if formatting is not None else self.format or equivalent.Saree
@Godsmith, good point! I've updated my answer to account for this.. thanks!Anglo
D
11

In Python, the name self is not special. It's just a convention for the parameter name, which is why there is a self parameter in __init__. (Actually, __init__ is not very special either, and in particular it does not actually create the object... that's a longer story)

C("abc").process() creates a C instance, looks up the process method in the C class, and calls that method with the C instance as the first parameter. So it will end up in the self parameter if you provided it.

Even if you had that parameter, though, you would not be allowed to write something like def process(self, formatting = self.formatting), because self is not in scope yet at the point where you set the default value. In Python, the default value for a parameter is calculated when the function is compiled, and "stuck" to the function. (This is the same reason why, if you use a default like [], that list will remember changes between calls to the function.)

How could I make this work?

The traditional way is to use None as a default, and check for that value and replace it inside the function. You may find it is a little safer to make a special value for the purpose (an object instance is all you need, as long as you hide it so that the calling code does not use the same instance) instead of None. Either way, you should check for this value with is, not ==.

Denaedenarius answered 15/11, 2011 at 5:45 Comment(2)
Your workaround doesn't meets the desired output on using None.Jose
If None is a valid value for formatting, then you will have to pick something else, like I explained.Denaedenarius
P
6

Since you want to use self.format as a default argument this implies that the method needs to be instance specific (i.e. there is no way to define this at class level). Instead you can define the specific method during the class' __init__ for example. This is where you have access to instance specific attributes.

One approach is to use functools.partial in order to obtain an updated (specific) version of the method:

from functools import partial


class C:
    def __init__(self, format):
        self.format = format
        self.process = partial(self.process, formatting=self.format)

    def process(self, formatting):
        print(formatting)


c = C('default')
c.process()
# c.process('custom')  # Doesn't work!
c.process(formatting='custom')

Note that with this approach you can only pass the corresponding argument by keyword, since if you provided it by position, this would create a conflict in partial.

Another approach is to define and set the method in __init__:

from types import MethodType


class C:
    def __init__(self, format):
        self.format = format

        def process(self, formatting=self.format):
            print(formatting)

        self.process = MethodType(process, self)


c = C('test')
c.process()
c.process('custom')
c.process(formatting='custom')

This allows also passing the argument by position, however the method resolution order becomes less apparent (which can affect the IDE inspection for example, but I suppose there are IDE specific workarounds for that).

Another approach would be to create a custom type for these kind of "instance attribute defaults" together with a special decorator that performs the corresponding getattr argument filling:

import inspect


class Attribute:
    def __init__(self, name):
        self.name = name


def decorator(method):
    signature = inspect.signature(method)

    def wrapper(self, *args, **kwargs):
        bound = signature.bind(*((self,) + args), **kwargs)
        bound.apply_defaults()
        bound.arguments.update({k: getattr(self, v.name) for k, v in bound.arguments.items()
                                if isinstance(v, Attribute)})
        return method(*bound.args, **bound.kwargs)

    return wrapper


class C:
    def __init__(self, format):
        self.format = format

    @decorator
    def process(self, formatting=Attribute('format')):
        print(formatting)


c = C('test')
c.process()
c.process('custom')
c.process(formatting='custom')
Polis answered 17/10, 2018 at 13:12 Comment(0)
L
2

You can't access self in the method definition. My workaround is this -

class Test:
  def __init__(self):
    self.default_v = 20

  def test(self, v=None):
    v = v or self.default_v
    print(v)

Test().test()
> 20

Test().test(10)
> 10
Leopard answered 21/2, 2019 at 20:41 Comment(0)
I
1

"self" need to be pass as the first argument to any class functions if you want them to behave as non-static methods.

it refers to the object itself. You could not pass "self" as default argument as it's position is fix as first argument.

In your case instead of "formatting=self.format" use "formatting=None" and then assign value from code as below:

[EDIT]

class c:
        def __init__(self, cformat):
            self.cformat = cformat

        def process(self, formatting=None):
            print "Formating---",formatting
            if formatting == None:
                formatting = self.cformat
                print formatting
                return formatting
            else:
                print formatting
                return formatting

c("abc").process()          # prints "abc"
c("abc").process("xyz")     # prints "xyz"

Note : do not use "format" as variable name, 'cause it is built-in function in python

Ima answered 15/11, 2011 at 5:52 Comment(1)
Well, I corrected the self problem. But your answer doesn't meets the desired output.Jose
P
0

Instead of creating a list of if-thens that span your default arguements, one can make use of a 'defaults' dictionary and create new instances of a class by using eval():

class foo():
    def __init__(self,arg):
        self.arg = arg

class bar():
    def __init__(self,*args,**kwargs):
        #default values are given in a dictionary
        defaults = {'foo1':'foo()','foo2':'foo()'}
        for key in defaults.keys():

            #if key is passed through kwargs, use that value of that key
            if key in kwargs: setattr(self,key,kwargs[key]) 

            #if no key is not passed through kwargs
            #create a new instance of the default value
            else: setattr(self,key, eval(defaults[key]))

I throw this at the beginning of every class that instantiates another class as a default argument. It avoids python evaluating the default at compile... I would love a cleaner pythonic approach, but lo'.

Precocity answered 1/7, 2019 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.