-1

I have a the following programme:

import QuantLib as ql

deposits = {ql.Period(1,ql.Weeks): 0.0023, 
            ql.Period(1,ql.Months): 0.0032,
            ql.Period(3,ql.Months): 0.0045,
            ql.Period(6,ql.Months): 0.0056}

for n, unit in [(1,ql.Weeks),(1,ql.Months),(3,ql.Months),(6,ql.Months)]:
    print deposits([n,unit])

What I expect this programme to do is: it loops through the dictionary keys, which comprises an embedded list of a 'number' (i.e. 1,1,3,6) and 'unit' (i.e. weeks and months), and extracts the correct value (or rate). Currently I get an error with the line print deposits([n,unit]).

Here is the error I get:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 699, in runfile
    execfile(filename, namespace)
  File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 74, in execfile
    exec(compile(scripttext, filename, 'exec'), glob, loc)
  File "TestFunction.py", line 16, in <module>
    print deposits([n,unit])   
TypeError: 'dict' object is not callable

The name of my file is TestFunction.py

I know a way round this issue, which is where I convert the dictionary into two lists as follows:

depoMaturities = [ql.Period(1,ql.Weeks), 
                  ql.Period(1,ql.Months),
                  ql.Period(3,ql.Months),
                  ql.Period(6,ql.Months)]

depoRates = [0.0023, 
             0.0032,
             0.0045,
             0.0056]

But then it does not look as tidy or as sophisticated. I'd be really grateful for your advice.

Audrius Kažukauskas
  • 13,173
  • 4
  • 53
  • 54
MacMavin
  • 3
  • 4
  • 3
    You should edit your question to include the exact and complete error that you get. As a first guess, the error is that you can't call a dict. If that is a typo in your question, then the next error is that lists are not hashable. If that weren't a problem, then finally, you would get a KeyError because the object you are using as the key to get a value for printing is not the same type (or value) of object that you used when you created the dictionary. – dsh Mar 07 '16 at 17:26
  • I've included the error I get. Given the dictionary 'deposits' that I have, can you advise me on how to extract the values? – MacMavin Mar 07 '16 at 18:39
  • Indeed: dicts are not callable. That is a typo: use `[]`, not `()`, to access elements of a dict. Next, as given in the answers, you need to create a `ql.Period` object from the quantity and units since your dict has `ql.Period` objects as keys. – dsh Mar 08 '16 at 22:10

3 Answers3

1

Update per comments: It looks like the Period class implemented __hash__ incorrectly, so it doesn't obey the hash invariant required by Python (specifically, objects that compare equal should hash to the same value). Per your comment, when you run:

p1 = ql.Period(1,ql.Weeks)
p2 = ql.Period(1,ql.Weeks)
if (p1 == p2): k = 5*2
else: k = 0

you get 10, so p1==p2 is True.

When you run:

if (hash(p1) == hash(p2)): b = 5*2
else: b = 0

you get 0, so hash(p1) == hash(p2) is False. This is a clear violation of the Python rules, which makes the type appear to be a legal key for a dict (or value in a set), but behave incorrectly. Basically, you can't use Periods as keys without having the QuantLib folks fix this, or doing terrible things to work around it (and really terrible things if Period is a C extension type, which seems likely since QuantLib is apparently a SWIG wrapper).

If the Period units behave properly, I'd recommend working with tuples of the paired counts and units most of the time, and only converting to Periods when you have need of a particular Period feature. So your dict would be:

deposits = {(1,ql.Weeks): 0.0023, 
            (1,ql.Months): 0.0032,
            (3,ql.Months): 0.0045,
            (6,ql.Months): 0.0056}

and your loop would be:

for n, unit in [(1,ql.Weeks),(1,ql.Months),(3,ql.Months),(6,ql.Months)]:
    print deposits[n, unit]

If that still fails, then even the basic unit types are broken, and you just can't use them at all.


If the keys are ql.Periods, you need to look up using ql.Periods (unless Period is tuple subclass). You also need to use brackets for dict lookup, not parentheses.

If ql.Period is a namedtuple or the like, you can just do tuple lookup (lists can't be dict keys, because they're mutable):

for n, unit in [(1,ql.Weeks),(1,ql.Months),(3,ql.Months),(6,ql.Months)]:
    print deposits[n, unit]

If ql.Period isn't a tuple subclass, you can do:

for n, unit in [(1,ql.Weeks),(1,ql.Months),(3,ql.Months),(6,ql.Months)]:
    print deposits[ql.Period(n, unit)]

or to make the periods in the loop,

import itertools

for period in itertools.starmap(ql.Period, [(1,ql.Weeks),(1,ql.Months),(3,ql.Months),(6,ql.Months)]):
    print deposits[period]

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Period is a class member function of quantlib. In my case, ql.Period takes in a number (to represent the time) and its unit (weeks, months, etc). So `ql.Period (1,ql.Weeks)` will yield "1W". From your response, I gather that you may not have come across quantlib. Sorry I failed to describe the quantlib portion clearer. I still get errors having implemented your suggestions – MacMavin Mar 07 '16 at 18:22
  • @MacMavin: Can you be more specific about the new errors? Your original code was wrong in a way that masks other issues (trying to call the `dict` as if it were a function with `()` instead of doing key lookup with `[]`). Since `ql.Period` isn't a `tuple` subclass, you probably want to use the second code block from my answer (where you do `print deposits[ql.Period(n, unit)]` on each loop); it should work by definition if the construction of `deposits` succeeded in the first place. – ShadowRanger Mar 07 '16 at 18:56
  • @MacMavin: Read your other comment. Is it possible `ql.Period` doesn't support equality or hashing, and just uses Python's default implementation based on identity? Or worse, implements equality and hashing, but doesn't obey the rules for hashing? Try doing the following in an interactive terminal on separate lines: `p1 = ql.Period(1,ql.Weeks)`, `p2 = ql.Period(1,ql.Weeks)`, `p1 == p2`, `hash(p1) == hash(p2)`. If the last two lines don't both return `True`, then you can't use `Period`s for lookups in a `dict` unless you store one canonical copy of each `Period` and reuse it. – ShadowRanger Mar 07 '16 at 19:04
  • Here are the errors (for your second suggestion): `Traceback (most recent call last): File "", line 1, in File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 699, in runfile execfile(filename, namespace) File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 74, in execfile exec(compile(scripttext, filename, 'exec'), glob, loc) File "TestFunction.py", line 29, in print deposits[ql.Period(n, unit)] KeyError: Period("1W") >>>` – MacMavin Mar 07 '16 at 19:09
  • Right. Like you said, `ql.Period` isn't a `tuple` subclass, so that won't work. The second suggestion is the one that would work (the third suggestion is identical to the second, just moving the work around, so if the second doesn't work, neither will the third). Can you try the minimal test I provided in my last comment? – ShadowRanger Mar 07 '16 at 19:10
  • @MacMavin: You can delete the comments with errors (or at least, the comments for the errors for suggestions #1 and #3, like I said, one definitely won't work, the other is functionally equivalent to #2). Please read [this comment](https://stackoverflow.com/questions/35849963/extract-value-from-a-dictionary-using-key-that-is-a-list#comment59369333_35850132) and tell me what results you get from testing equality and hash codes for two `Period`s initialized with the same values. – ShadowRanger Mar 07 '16 at 19:15
  • I was not sure about the interactive terminal as I am fairly new to python. instead I wrote the following to check as per your comments `p1 = ql.Period(1,ql.Weeks) p2 = ql.Period(1,ql.Weeks) if (p1 == p2): k = 5*2 else: k = 0` This returned 10, so p1==p2 is 'True' `if (hash(p1) == hash(p2)): b = 5*2 else: b = 0` This returned 0; so 'hash(p1)==hash(p2)' is false – MacMavin Mar 07 '16 at 19:17
  • @MacMavin: Okay, updated my answer. The problem is that `QuantLib` is directly violating [the Python hash invariant](https://docs.python.org/3/reference/datamodel.html?highlight=__hash__#object.__hash__), so it's unsuitable for use as a `dict` key or `set`/`frozenset` value. Sorry. – ShadowRanger Mar 07 '16 at 19:30
  • Thank you for your responses; I am grateful. – MacMavin Mar 07 '16 at 19:40
  • I confirm that `Period` exports `__eq__` through SWIG but not `__hash__`. Thanks for the heads-up; we'll fix this in next release. In the meantime, I second using the tuple `(n,unit)` as the key. – Luigi Ballabio Mar 07 '16 at 21:17
  • @LuigiBallabio: At the Python layer, Python handles defining `__eq__` without defining `__hash__` as "this type is not hashable". At the C level, [you need to explicitly set `nb_hash`](https://docs.python.org/2/c-api/typeobj.html#c.PyTypeObject.tp_hash) to [`PyObject_HashNotImplemented`](https://docs.python.org/2/c-api/object.html#c.PyObject_HashNotImplemented). Not sure how SWIG exposes this to be honest. – ShadowRanger Mar 08 '16 at 02:21
  • SWIG defines a Python class that stores a pointer to the underlying C++ object and whose methods call C++ functions acting on the pointer. Thus, classes exported through SWIG get the default `__hash__` function defined for custom classes. – Luigi Ballabio Mar 08 '16 at 09:04
0

deposits is a dictionary with keys and values. The reference of a dictionary is

 value = mydict[key]

Thus given n and unit you get that ql.Period(n, unit) returns a type of <class 'QuantLib.QuantLib.Period'>. The result of ql.period(1, ql.Weekly) for example would be 1W.

It would appear that if it is converted to a string, then it would be usable as a key.

deposits = {str(ql.Period(1,ql.Weeks)): 0.0023, 
            str(ql.Period(1,ql.Months)): 0.0032,
            str(ql.Period(3,ql.Months)): 0.0045,
            str(ql.Period(6,ql.Months)): 0.0056}

value = deposits[str(ql.Period(n, unit))]
print value
sabbahillel
  • 4,357
  • 1
  • 19
  • 36
  • Thank you for your response. I just ran your suggestion but get the error `print deposits[ql.Period(n, unit)]KeyError: Period("1W")` – MacMavin Mar 07 '16 at 17:39
  • @MacMavin do a print of n and unit and also print ql.Weeks to ensure that the two match. Also check that your code has the comma for the two arguments in period ql.Period(1,'W') – sabbahillel Mar 07 '16 at 17:56
  • I've done a print of the statement; here are the results: Command: `print n, unit, ql.Months` Results: `1 1 2; 1 2 2; 3 2 2; 6 2 2` I'd explain this result: within the quantlib library, the ql.Months has an index of 2, while ql.Weeks has an index of 1. The result produced is as expected. The first line represents 1 week, the second 1 month, the third 3 months and the fourth 6 months. I cannot anything wrong from the print statement. – MacMavin Mar 07 '16 at 18:09
  • @MacMavin I do not have the QuantLib module available on the computer that I can get do. I will try to install it at home and look at it tonight, but I do not know if I will be able to get to it. Try to explicitly look at `deposits[ql.Period[(1,1)]` and see if that works. if it does, then verify that you typed `deposits[n, unit]` correctly. – sabbahillel Mar 07 '16 at 19:35
  • I have tried your suggestion but I still get an error `Traceback (most recent call last): File "", line 1, in File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 699, in runfile execfile(filename, namespace) File "C:\Anaconda2\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 74, in execfile exec(compile(scripttext, filename, 'exec'), glob, loc) File "TestFunction.py", line 16, in deposits[ql.Period[(1,1)]] TypeError: 'type' object has no attribute '__getitem__' >>>` – MacMavin Mar 07 '16 at 19:56
  • @MacMavin That is odd what is `type(ql.Period(1, ql.Weeks))` That may show what it is and what you need to convert it to in order to allow it to be a key. – sabbahillel Mar 07 '16 at 20:03
  • It returns a type of ``and its result is `1W` – MacMavin Mar 07 '16 at 20:10
  • @MacMavin see if it can be converted to a string `str((ql.Period(1, ql.Weeks))` – sabbahillel Mar 07 '16 at 20:35
  • It works. Thank you for comments; I am really grateful. – MacMavin Mar 07 '16 at 22:44
0

In addition to the syntax problems others have identified, my guess is that your ql.Period object is not hashable; the keys for dictionaries need to be hashable objects. Here's a direct copy-and-past from this answer, which explains the situation nicely.

>>> a = {}
>>> b = ['some', 'list']
>>> hash(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list objects are unhashable
>>> a[b] = 'some'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list objects are unhashable

What happens when you try hash(ql.Period(1,ql.Weeks))? A similar TypeError? If you had control over QuantLib, you could possibly add a __hash__ method, so that they could be used in dictionaries. But I see that such a module exists on pypi, so I guess you're just using it rather than writing it.

You may still be able to monkey patch these objects to give them a __hash__ method:

# First define a function to add on as a method
def hash_method(period):
    hash_value = # some code that produces a unique hash, based on
                 # the data contained in the `period` object
    return hash_value

# Now, monkey patch the ql.Period object by giving it this method
ql.Period.__hash__ = hash_method
Community
  • 1
  • 1
Mike
  • 19,114
  • 12
  • 59
  • 91
  • It's definitely hashable (you'd get a `TypeError` if `Period` wasn't hashable, not a `KeyError` as the OP describes in the comments). Currently trying to suss out whether it's using the default Python equality and hashing checks (which are based on identity, not value), or whether they actually implemented equality and hashing, but did it wrong. – ShadowRanger Mar 07 '16 at 19:17
  • Oh, fair enough. :) I'll just leave this here in case others stumble across it due to the title. – Mike Mar 07 '16 at 19:24