Search code examples
pythonsubclassclass-methodpython-3.9pathlib

Is this inconsequent python behaviour?


So, i was looking at the pathlib source code and i noticed some os lib functions were incorperated into the pathlib classes. I must admit that it took me a little while to understand how this works.

import os

class Foo:
    listdir = os.listdir
    join = os.path.join
    walk = os.walk

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

    def __fspath__(self):
        return self.name

curr = Foo('.')
pipo = Foo('pipo')

This class definition adds the listdir, join and walk functions of the os library as methods on the Foo class. The Foo class is recognized as a PathLike object because it also has the __fspath__ method.

When curr.join('bar') is called it will join the name of curr, '.', with 'bar'. The code below shows this exact behaviour, with results i would expect.

$ tree
.
├── pipo
│   └── clown
└── test.py

1 directory, 2 files
$

>>> curr.join("baz") 
./baz
>>>
>>> pipo.join("baz")
pipo/baz
>>>
>>> [x for x in curr.walk()]
[('.', ['pipo'], ['test.py']), ('./pipo', [], ['clown'])]
>>>
>>> [x for x in pipo.walk()]
[('pipo', [], ['clown'])]
>>>

Then i tried the listdir function ... and it failed. The listdir function shows different behaviour and i'm stumped as to why?

>>>
>>> os.listdir()
['pipo', 'test.py']
>>>
>>> curr.listdir()
['pipo', 'test.py']
>>>
>>> os.listdir("pipo")
['clown']
>>>
>>> pipo.listdir()
['pipo', 'test.py']
>>> 

The listdir method on pipo shows the same output as the listdir on curr?

It seems to me like join and walk accept the self reference but somehow listdir does not. Why would that be?


Solution

  • The function class implements the descriptor protocol, allowing os.path.join to act as an instance method. os.listdir is not an instance of function; it's an instance of builtin_function_or_method, which does not implement the descriptor protocol.

    curr.join(...) is equivalent to Foo.join.__get__(curr, Foo)(...). curr.listdir() is equivalent to Foo.listdir(): curr.listdir does not exist, and Foo.listdir just calls listdir, not the non-existent listdir.__get__ which would take care of passing curr as the implicit first argument.