0

The following code is of course totally pointless; it's not supposed to do anything but illustrate what I'm confused about:

class func():
  def __call__(self, x):
   raise Exception("func.__call__ error")

def double(x):
  return 2*x

doubler = func()
doubler.__call__ = double

print doubler(2)

Can someone explain why this works? I would have expected that if I wanted to set doubler.__call__ to something it would be a function that takes two variables; I'd expect the code above to raise some sort of too-many-parameters error. What gets passed to what, when?

(And then: How could I set doubler.__call__ to a function that will actually have access to both "self" and "x"?)

(Context: An admittedly silly of-academic-interest example of why I might want to set an instance method this way: Each computable instance needs its own Approx method; creating a separate subclass for each instance seems "wrong"...)

Edit. Probably a better example, making it clear it has nothing to do with magic-method magic:

class func():
  def call(self, x):
    raise Exception("func.call error")

def double(x):
  return 2*x
doubler = func()
doubler.call = double

print doubler.call(2)

On third thought, probably the following is the right way to do it. (i) Seems cleaner somehow, using the Python object model instead of tinkering with it (ii) even 24 hours ago with my then much cruder understanding I would have expected it to work; somehow in this version it simply seems to make sense to me that the function passed to the constructor should take only one variable (iii) it seems to work regardless of whether I inherit from object, which I think means it would also work in 3.0.

class func3(object):
  def __init__(self, f):
    self.f = f
  def __call__(self, x):
    return self.f(x)

def double(x):
  return 2.0*x

f3=func3(double)
print f3(2)
  • 1
    (Sorry/thx: I didn't see the python-2.7 tag...) – David C. Ullrich Aug 30 '19 at 19:44
  • Unlike, say, JavaScript, where the way you call determines what the function gets, Python methods get `self` bound when they’re accessed via an instance. Things like `f = doubler.call; f()` will perform a correct call and raise the “func.call error” exception, for example. Looking for a duplicate. – Ry- Aug 30 '19 at 20:04
  • This question has some distractions, but the answer applies: https://stackoverflow.com/questions/44741054/python-class-methods-when-is-self-not-needed – Ry- Aug 30 '19 at 20:05
  • @Ry- "Some distractions" indeed! The guy could have trimmed a lot of irrelevant crap (I mean why would we care that the function we're assigning to the method lives in a dict?). Whatever - if you had a minute to answer the question contained in the "answer" I just posted I'd appreciate it. – David C. Ullrich Aug 30 '19 at 20:39
  • @Ry- "Things like f = doubler.call; f() will perform a correct call and raise the “func.call error” exception," I didn't get this at all. I tried it, and no exception, f(3) returned 6. Was there maybe a typo in what you wrote? If I say f-func.call then f(3) raises a "must be called with func instance as first parameter", while f(doubler, 3) gives the "func.call error". Which actually makes sense to me. – David C. Ullrich Aug 30 '19 at 21:00

2 Answers2

2

When you assign to doubler.__call__, you're binding an function to an instance attribute. This hides the class attribute of the same name that was created in the class statement.

Python's method binding only kicks in when you are looking up a class attribute via an instance. If the attribute's value is a descriptor (which functions are), then the descriptor's __get__ method gets called with appropriate parameters. For a function object, that binds the method to the instance (so self gets passed in automatically as the first argument).

Your first example wouldn't actually work in Python 3, only in Python 2. That's because in Python 2 you're creating an "old-style" class, which does all its method lookups on the instance. In new-style classes (which you can get in Python 2 by inheriting from object, or by default in Python 3), __special__ methods, when they're invoked by the interpreter (e.g. when you do doubler(2) to run doubler.__call__) are looked up only in the class, not in the instance's attributes. So your first example won't work with a new-style class, but the version that uses a normal method (call instead of __call__) would be fine.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Thanks. I think a lightbulb just went on. I've been reading a lot of sentences like " ...looking up a class attribute via an instance." and been totally confused, because it's seemed to me that I _was_ "looking up an attribute via an instance". OH! Been reading sloppily, ignoring a word - you said "....looking up a **class** attribute via an instance". Details.... – David C. Ullrich Aug 30 '19 at 21:25
  • I shoulda known that getting cute with `__call__` in the question was a mistake, giving something other than the most straightforward possible example... – David C. Ullrich Aug 30 '19 at 21:30
  • Yeah, you probably don't want to be using old-style classes in Python 2, even if they do make magic methods work like other methods. I'm glad my explanation of the significance of a method being a class attribute helped you understand what was going on! – Blckknght Aug 30 '19 at 23:13
  • If you have a minute please look at the third version I added, and tell me whether (i) you agree it's "cleaner", with attribute assignments inside `__init__` where they belong, (ii) yes, the fact that it works regardless of whether I inherit from `object` means it should still work in 3.0. – David C. Ullrich Aug 31 '19 at 15:44
0

This is something between an answer to the question and a continuation of the question. I was kindly referred to another thread where more or less the same question was answered. I didn't follow the answers in that thread very well, being ignorant of the things the people there are talking about, hence the Question: Is what I say below correct? (If yes then this is an answer to the question above; if no I'd appreciate someone explaining why not...)

(i) Since I assign a function to an instance of func instead of to the class, it is now an "instance method", as opposed to a "class method".

(ii) And that's why it's not passed the instance as the first parameter; that happens with class methods but not with instance methods...