9

I am working with a Python class, and I don't have write access to its declaration. How can I attach a custom method (such as __str__) to the objects created from that class without modifying the class declaration?

EDIT: Thank you for all your answers. I tried them all but they haven't resolved my problem. Here is a minimal example that I hope will clarify the issue. I am using swig to wrap a C++ class, and the purpose is to override the __str__ function of an object returned by the swig module. I use cmake to build the example:

test.py

import example

ex = example.generate_example(2)

def prnt(self):
    return str(self.x)

#How can I replace the __str__ function of object ex with prnt?
print ex
print prnt(ex)

example.hpp

struct example
{
    int x;
};

example generate_example(int x);

example.cpp

#include "example.hpp"
#include <iostream>

example generate_example(int x)
{
    example ex;
    ex.x = x;
    return ex;
}

int main()
{
    example ex = generate_example(2);
    std::cout << ex.x << "\n";
    return 1;
}

example.i

%module example

%{
#include "example.hpp"
%}

%include "example.hpp"

CMakeLists.txt

cmake_minimum_required(VERSION 2.6)

find_package(SWIG REQUIRED)
include(${SWIG_USE_FILE})

find_package(PythonLibs)
include_directories(${PYTHON_INCLUDE_PATH})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})

set_source_files_properties(example.i PROPERTIES CPLUSPLUS ON)
swig_add_module(example python example.i example)
swig_link_libraries(example ${PYTHON_LIBRARIES})

if(APPLE)
    set(CMAKE_SHARED_MODULE_CREATE_CXX_FLAGS "${CMAKE_SHARED_MODULE_CREATE_CXX_FLAGS} -flat_namespace")
endif(APPLE)

To build and run test.py, copy all the files in a directory, and in that directory run

cmake .
make 
python test.py

This results in the following output:

<example.example; proxy of <Swig Object of type 'example *' at 0x10021cc40> >
2

As you can see the swig object has its own str function, and that is what I am trying to override.

D R
  • 20,826
  • 32
  • 104
  • 148

5 Answers5

23

If you create a wrapper class, this will work with any other class, either built-in or not. This is called "containment and delegation", and it is a common alternative to inheritance:

class SuperDuperWrapper(object):
    def __init__(self, origobj):
        self.myobj = origobj
    def __str__(self):
        return "SUPER DUPER " + str(self.myobj)
    def __getattr__(self,attr):
        return getattr(self.myobj, attr)

The __getattr__ method will delegate all undefined attribute requests on your SuperDuperWrapper object to the contained myobj object. In fact, given Python's dynamic typing, you could use this class to SuperDuper'ly wrap just about anything:

s = "hey ho!"
sds = SuperDuperWrapper(s)
print sds

i = 100
sdi = SuperDuperWrapper(i)
print sdi

Prints:

SUPER DUPER hey ho!
SUPER DUPER 100

In your case, you would take the returned object from the function you cannot modify, and wrap it in your own SuperDuperWrapper, but you could still otherwise access it just as if it were the base object.

print sds.split()
['hey', 'ho!']
PaulMcG
  • 59,676
  • 15
  • 85
  • 126
  • I used this technique very early in my Python career to write an interceptor for a proxy to a remote CORBA object - the client wanted one particular remote method to be slightly enhanced, but otherwise everything else was to behave the same. Having only recently switched over from C++, I was aghast at how little code it took to write, as opposed to what C++ would have made me do. – PaulMcG Sep 05 '09 at 16:03
  • Thanks Paul! That is exactly what I was looking for. – D R Sep 05 '09 at 16:15
  • The problem with this approach is that the resulting wrapped object are not pickable, which sometimes is a big issue – Boris Gorelik Sep 15 '11 at 07:22
  • +1. I was surprised this worked for a COM object. I expected `getattr` to fail. – Steven Rumbalski Mar 06 '12 at 19:17
  • Adding extra functionality this is commonly known as a Decorator. – jmoz Jul 19 '12 at 15:39
  • Access is not _always_ as if it were the base object. For instance `isinstance(s, basestring == True`, but `isinstance(sds, basestring) == False`. Similarly, the `id()` and `type()` functions return different results. – martineau Nov 29 '12 at 21:52
  • Is is too pythonic? Is there any other suggestion for a developer writes code for can-only-read-python teammates? Wish to have other resources for C++ on this term. – tomriddle_1234 Dec 05 '14 at 02:13
3
>>> class C(object):
...     pass
... 
>>> def spam(self):
...     return 'spam'
... 
>>> C.__str__ = spam
>>> print C()
spam

It won't work on classes which use __slots__.

Bastien Léonard
  • 58,016
  • 19
  • 77
  • 94
  • Unfortunately this is not working in my case. The class that I am trying to override was autogenerated using swig from C++ header files. – D R Sep 05 '09 at 09:52
3

Create a subclass. Example:

>>> import datetime
>>> t = datetime.datetime.now()
>>> datetime.datetime.__str__ = lambda self: 'spam'
...
TypeError: cant set attributes of built-in/extension type 'datetime.datetime'
>>> t.__str__ = lambda self: 'spam'
...
AttributeError: 'datetime.datetime' object attribute '__str__' is read-only
>>> class mydate(datetime.datetime):
    def __str__(self):
        return 'spam'

>>> myt = mydate.now()
>>> print t
2009-09-05 13:11:34.600000
>>> print myt
spam
Alex
  • 633
  • 1
  • 5
  • 10
  • Thanks for the suggestion Alex. It is true that I can easily create a subclass, but this will not help me much because the object that I am working with is returned by a function that I cannot modify. In the context of your example that object is of type datetime. I would like to override its `__str__` function, so in order for this to be useful I need a way to cast a datetime object to mydate. – D R Sep 05 '09 at 14:46
2

This is my answer from another question:

import types
class someclass(object):
    val = "Value"
    def some_method(self):
        print self.val

def some_method_upper(self):
    print self.val.upper()

obj = someclass()
obj.some_method()

obj.some_method = types.MethodType(some_method_upper, obj)
obj.some_method()
Community
  • 1
  • 1
gnud
  • 75,794
  • 5
  • 62
  • 77
1

Note that using Alex's subclass idea, you can help yourself a little bit more using "from ... import ... as":

from datetime import datetime as datetime_original

class datetime(datetime_original):
   def __str__(self):
      return 'spam'

so now the class has the standard name, but different behaviour.

>>> print datetime.now()
    'spam'

Of course, this can be dangerous...

Andrew Jaffe
  • 25,476
  • 4
  • 47
  • 58