如何将实例成员的默认参数值传递给方法?

我想使用实例属性的值将默认参数传递给实例方法:

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


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

在尝试这样做时,我得到以下错误消息:

NameError: name 'self' is not defined

我希望这个方法是这样的:

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

这里的问题是什么,为什么这不工作? 我怎么才能使这个工作?

39813 次浏览

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!

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 ==.

"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

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')

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

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'.