I have a class which should transform the image retrieved based on a path received(url or os image).
Not sure how is the right way to implement this to follow best principles, like SOLID, or should i even care about it?! in my case.
In particular, whenever i update the 'path' attribute the code should automatically update also the
'img' attribute based on the given path.
class TransfImage(path):
def __init__(self,path):
self._path = path
#self._img = img_from_url() ?! would be better to use here the method
#property
def path(self):
return self._path
#path.setter
def path(self,my_path):
if valid_os_file_image(my_path):
self._path = my_path
self.img = img_from_os_file(my_path) #not sure if is the proper way to set the image here
elif valid_url_image(my_path):
self._path = my_path
self.img = img_from_url(my_path) #not sure if is the proper way to set the image here
#property
def img(self):
return self._img
#img.setter
def img(self,my_img):
self._img = my_img
def img_from_os_file(self,my_os_file):
return cv.imread(my_os_file)
def img_from_url(self, url_path):
try:
req = urllib.request.urlopen(url_path)
except ValueError:
raise ValueError('not a valid url')
try:
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv.imdecode(arr, -1)
except ValueError as e:
print('could not read img from url', e)
else:
return img
def im_show(self):
cv.imshow(self.img)
t = TransfImage('lena.jpg')
t.im_show() #should show lena.jpg
t.path = 'cards.jpg'
t.im_show() #should show 'cards.jpg
There is no visible reason in your code to make img a property, Apart from this I see no problem (except for a few calls to methods missing a self prefix).
Related
I am writing some code to register two images. I am using inheritance where the base class is Image and the subclasses are ImageType1 and ImageType2.
class Image:
# Note: I don't have an __init__ fx bc there are no attributes to initialize.
# Is this appropriate?
def sweep_left(self, image)
# loop to find a specific pixel
return row, col # return row and col of pixel
def sweep_right(self, image)
# loop to find a specific pixel
return row, col # return row and col of pixel
def sweep(self, image):
left_row, left_col = self.sweep_left(image)
right_row, right_col = self.sweep_right(image)
return left_row, left_col, right_row, right_col
def register(self, img1, img2):
ref_left_row, ref_left_col, ref_right_row, ref_right_col = self.sweep(img1)
reg_left_row, reg_left_col, reg_right_row, reg_right_col = self.sweep(img2)
registered_img2 = #some calcs with variables above
return registered_img2
class ImageType1(Image):
def __init__(self, image):
self.image = image
def process_image_type_1(self):
# Do some image processing using skimage
processed_img = skimage_functions(image)
return processed_img
class ImageType2(Image):
def __init__(self, image):
self.image = image
def process_image_type_2(self):
# Do some image processing using skimage
processed_img = skimage_functions(image)
return processed_img
The following code runs successfully....
image1 = ImageType1(one_image)
image1_processed = image1.process_image_type_1()
image2 = ImageType2(another_image)
image2_processed = image2.process_image_type_2()
reg = Image()
register_images = reg.register(image1_processed, image2_processed)
However, I don't understand why this code returns an error. Initially I thought it wouldn't even work because there is no obj Image, but after running it seems as though it 'kinda' worked, just needs 1 more argument. Can someone explain the details here?
register_images = Image.register(image1_processed, image2_processed)
TypeError: Image.register() missing 1 required positional argument: 'img2'
How would you design the code to use method 2, e.g. Image.register(image1_processed, image2_processed)?
UPDATE: To hopefully clear some things up removed some generalities and added code snippets.
I'm going a little bug-eyed here trying to troubleshoot this. In the process, I've tried to create a self-contained function to reproduce the issue, but for some reason it works as expected in the micro-example, but not in my prod code.
I have a subclass of pathlib.Path:
class WalkPath(Path):
_flavour = type(Path())._flavour
def __init__(self, *args, origin: 'WalkPath'=None, dirs: []=None, files: []=None):
super().__init__()
if type(args[0]) is str:
self.origin = origin or self
else:
self.origin = origin or args[0].origin
self._dirs: [WalkPath] = list(map(WalkPath, dirs)) if dirs else None
self._files: [WalkPath] = list(map(WalkPath, files)) if files else None
self._lazy_attr = None
#staticmethod
def sync(wp: Union[str, Path, 'WalkPath']):
"""Syncronize lazy-loaded attributes"""
x = wp.lazy_attr
return wp
#property
def lazy_attr(self):
if self._lazy_attr:
return self._lazy_attr:
# long running op
self._lazy_attr = long_running_op(self)
return self._lazy_attr
class Find:
#staticmethod
def shallow(path: Union[str, Path, 'WalkPath'],
sort_key=lambda p: str(p).lower(),
hide_sys_files=True) -> Iterable['WalkPath']:
origin = WalkPath(path)
if origin.is_file():
return [origin]
for p in sorted(origin.iterdir(), key=sort_key):
if hide_sys_files and is_sys_file(p):
continue
yield WalkPath(p, origin=origin)
Using multiprocessing.Pool, I want to execute that long-running process in a pool.
That looks like this:
_paths = ['/path1', '/path2']
found = list(itertools.chain.from_iterable(Find.shallow(p) for p in _paths))
Find.shallow (see above) basically just does a Path.iterdir on origin and then maps the results to a WalkPath object, setting the origin to the path called. I know this works, because this outputs correctly:
for x in found:
print(x.origin, x.name)
Then we dispatch to a pool:
with mp.Pool() as pool:
done = [x for x in pool.map(WalkPath.sync, found) if x.origin]
But this fails, starting 'WalkPath' has no attribute 'origin'.
Here’s my attempt at reproducing it locally, but for some reason it works! I cannot spot the difference.
#!/usr/bin/env python
import multiprocessing as mp
import time
from itertools import tee, chain
r = None
class P:
def __init__(self, i, static=None):
# self.static = static if not static is None else i
self.static = static or i
# print(static, self.static)
self.i = i
self._a_thing = None
#property
def a_thing(self):
if self._a_thing:
print('Already have thing', self.i, 'static:', self.static)
return self._a_thing
time.sleep(0.05)
print('Did thing', self.i, 'static:', self.static)
self._a_thing = True
return self._a_thing
#staticmethod
def sync(x):
x.a_thing
x.another = 'done'
return x if x.a_thing else None
class Load:
#classmethod
def go(cls):
global r
if r:
return r
paths = [iter(P(i, static='0') for i in range(10)),
iter(P(i, static='0') for i in range(11, 20)),
iter(P(i, static='0') for i in range(21, 30))]
iternums, testnums = tee(chain.from_iterable(paths))
for t in testnums:
print('Want thing', t.i, 'to have static:', t.static)
with mp.Pool() as pool:
rex = [x for x in pool.map(P.sync, list(iternums)) if x.another]
r = rex
for done in rex:
print(done.i, done.static, done.a_thing, done.another)
Load.go()
The crux of the problem is that your Path objects cannot be shared between interpreter processes.
Instead, when using multiprocessing, Python serializes (pickles) all arguments and return values to/from subprocesses.
It seems that pathlib.Path defines custom pickling/unpickling logic that is incompatible with your origin attribute:
import pathlib
import pickle
class WalkPath(pathlib.Path):
_flavour = type(pathlib.Path())._flavour
def __init__(self, *args, origin: 'WalkPath'=None, dirs: []=None, files: []=None):
super().__init__()
if type(args[0]) is str:
self.origin = origin or self
else:
self.origin = origin or args[0].origin
self._dirs: [WalkPath] = list(map(WalkPath, dirs)) if dirs else None
self._files: [WalkPath] = list(map(WalkPath, files)) if files else None
self._lazy_attr = None
path = WalkPath('/tmp', origin='far away')
print(vars(path))
reloaded = pickle.loads(pickle.dumps(path))
print(vars(reloaded))
$ python3.9 test.py
{'origin': 'far away', '_dirs': None, '_files': None, '_lazy_attr': None}
{'origin': WalkPath('/tmp'), '_dirs': None, '_files': None, '_lazy_attr': None}
For fun, here's how I ended up solving this.
What happens here, is that Path implements the __reduce__ function, which is called before __getstate__ or __setstate__ would (which are higher level pickling functions).
Here's the __reduce__ function from PurePath, Path's base class:
def __reduce__(self):
# Using the parts tuple helps share interned path parts
# when pickling related paths.
return (self.__class__, tuple(self._parts))
Oh no! Well, we can see what happens - this is intentionally designed just to pass a tuple of its parts, dropping state altogether and forming a new version of itself.
I didn't want to mess with that, but I also wanted to make sure my state was preserved here. So I created a serializer that takes these properties as a tuple argument (since... __reduce__ for some ridiculous reason only takes a single tuple as an argument).
I also had to make sure that origin was now a Path object, not a WalkPath object, otherwise I would have ended up with an endless recursion. I added some type coercion and safety to the __init__:
if origin:
self.origin = Path(origin)
elif len(args) > 0:
try:
self.origin = Path(args[0].origin) or Path(args[0])
except:
self.origin = Path(self)
if not self.origin:
raise AttributeError(f"Could not infer 'origin' property when initializing 'WalkPath', for path '{args[0]}'")
Then I added these two methods to WalkPath:
# #overrides(__reduce__)
def __reduce__(self):
# From super()
# Using the parts tuple helps share internal path parts
# when pickling related paths.
# return (self.__class__, tuple(self._parts))
# This override passes its parts to a Path object (which
# natively pickles), then serializes and applies
# its remaining attributes.
args = {**{'_parts': self._parts}, **self.__dict__}
return (self.__class__._from_kwargs, tuple(args.items()))
#classmethod
def _from_kwargs(cls, *args):
kwargs = dict(args)
new = cls(super().__new__(cls,
*kwargs['_parts']),
origin=kwargs['origin'])
new.__dict__ = {**new.__dict__, **kwargs}
return new
I am creating a Blackjack GUI game with Tkinter and I'm running into an issue where the deal button clears the image of the old card from the screen when a new one is added. My educated guess is the card_image inside the deal() function is overwriting itself when I use the function again. If this is the case why is this and what's the best fix? Thanks.
import random
from tkinter import *
from PIL import Image, ImageTk
root =Tk()
root.title('21 Blackjack')
root.iconbitmap('images/21_cards.ico')
root.geometry('1280x750')
root.configure(bg='green')
cards = []
suits = ['hearts', 'clubs', 'diamonds', 'spades']
face_cards = ['ace', 'jack', 'queen', 'king']
extension = 'png'
for y in suits:
for x in range(2, 11):
name = 'images/{}-{}.{}'.format(str(x), y, extension)
cards.append(name)
for x in face_cards:
name = 'images/{}-{}.{}'.format(str(x), y, extension)
cards.append(name)
print(cards)
print(len(cards))
random.shuffle(cards)
print(cards[0])
hand = []
def deal():
global card_image, card_label, hand
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
hand += cards[:1]
cards.pop(0)
print(hand)
deal_button = Button(root, text="deal", command=deal).pack()
root.mainloop()
Instead of
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
Do:
card_label = Label(root, image=card_image, relief="raised").pack(side="left")
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label.image = card_image #Keeps reference to the image so it's not garbage collected
Do note when you want use the photo use the variable card_image
As people have pointed out I needed to add card_label.image = card_image to the function and remove card_image & card_label global to stop the image being removed. But for some reason Python didn't like having the image being packed before I did this.
The function now looks like this.
global hand
card_image = ImageTk.PhotoImage(Image.open(cards[0]).resize((180, 245), Image.ANTIALIAS))
card_label = Label(root, image=card_image, relief="raised")
card_label.image = card_image
card_label.pack(side="left")
hand += cards[:1]
cards.pop(0)
print(hand)
You should use an image pool. Lucky for you, I have one right here
import os
from glob import glob
from PIL import Image, ImageTk
from typing import List, Tuple, Union, Dict, Type
from dataclasses import dataclass
#dataclass
class Image_dc:
image :Image.Image
rotate :int = 0
photo :ImageTk.PhotoImage = None
def size(self) -> Tuple[int]:
if not self.photo is None:
return self.photo.width(), self.photo.height()
return self.image.width, self.image.height
class ImagePool(object):
##__> PRIVATE INTERFACE <__##
__PATHS = dict()
__IMAGES = dict()
#staticmethod
def __name(path) -> str:
return os.path.basename(os.path.splitext(path)[0])
#staticmethod
def __unique(path:str, prefix:str='') -> str:
name = f'{prefix}{ImagePool.__name(path)}'
if name in ImagePool.names():
sol = 'using a prefix' if not prefix else f'changing your prefix ({prefix})'
msg = ("WARNING:\n"
f"{name} was not loaded due to a same-name conflict.\n"
f"You may want to consider {sol}.\n\n")
print(msg)
return None
return name
#staticmethod
def __request(name:str) -> Image_dc:
if name in ImagePool.__PATHS:
path = ImagePool.paths(name)
if os.path.isfile(path):
if name not in ImagePool.__IMAGES:
ImagePool.__IMAGES[name] = Image_dc(Image.open(path))
return ImagePool.__IMAGES[name]
else:
raise ValueError(f'ImagePool::__request - Path Error:\n\tpath is not a valid file\n\t{path}')
raise NameError(f'ImagePool::__request - Name Error:\n\t"{name}" does not exist')
return None
#staticmethod
def __size(iw:int, ih:int, w:int=None, h:int=None, scale:float=1.0) -> Tuple[int]:
if not w is None and not h is None:
if iw>ih:
ih = ih*(w/iw)
r = h/ih if (ih/h) > 1 else 1
iw, ih = w*r, ih*r
else:
iw = iw*(h/ih)
r = w/iw if (iw/w) > 1 else 1
iw, ih = iw*r, h*r
return int(iw*scale), int(ih*scale)
##__> PUBLIC INTERFACE <__##
#staticmethod
def names(prefix:str='') -> List[str]:
names = [*ImagePool.__PATHS]
return names if not prefix else list(filter(lambda name, pre=prefix: name.startswith(pre), names))
#staticmethod
def paths(name:str=None) -> Union[Dict, str]:
if name is None:
return ImagePool.__PATHS
if name in ImagePool.__PATHS:
return ImagePool.__PATHS[name]
raise NameError(f'ImagePool::paths - Name Error:\n\tname "{name}" does not exist')
#staticmethod
def images(name:str=None, prefix:str='') -> Union[Dict, Image.Image]:
if name is None:
return {name:ImagePool.__request(name).image for name in self.names(prefix)}
return ImagePool.__request(name).image
#staticmethod
def photos(name:str=None, prefix:str='') -> Union[Dict, ImageTk.PhotoImage]:
if name is None:
return {name:ImagePool.__request(name).photo for name in self.names(prefix)}
return ImagePool.__request(name).photo
#staticmethod
def append_file(path:str, prefix:str='') -> Type:
if not os.path.isfile(path):
raise ValueError(f'ImagePool::append_file - Value Error:\n\tpath is not valid\n\t{path}')
name = ImagePool.__unique(path, prefix)
if name:
ImagePool.__PATHS[name] = path
return ImagePool
#staticmethod
def append_directory(directory:str, filters=['*.png', '*.jpg'], prefix:str='') -> Type:
if not os.path.isdir(directory):
raise ValueError(f'ImagePool::append_directory - Value Error:\n\tdirectory is not valid\n\t{directory}')
filters = filters if isinstance(filters, (List, Tuple)) else [filters]
for filter in filters:
for path in glob(f'{directory}/{filter}'):
ImagePool.append_file(path, prefix)
return ImagePool
#staticmethod
def photo(name:str, width:int=None, height:int=None, scale:float=1.00, rotate:int=None) -> ImageTk.PhotoImage:
image_t = ImagePool.__request(name)
size = ImagePool.__size(*image_t.size(), width, height, scale)
rotate = image_t.rotate if rotate is None else rotate
#only resize if the new size or rotation is different than the current photo size or rotation
#however, a small margin for error must be considered for the size
diff = tuple(map(lambda i, j: i-j, image_t.size(), size))
if (diff > (1, 1)) or (diff < (-1, -1)) or (image_t.rotate != rotate):
image_t.rotate = rotate
image_t.photo = ImageTk.PhotoImage(image_t.image.resize(size, Image.LANCZOS).rotate(rotate))
return image_t.photo
Using that file you can do a lot of things very easily. You can put all your card images in a folder and get them all with:
ImagePool.append_directory(path_to_folder)
You can then get any card and force it to fit in it's parent (no-matter-what) with:
somelabel['image'] = ImagePool.photo(image_name, allotted_width, allotted_height)
or just:
somelabel['image'] = ImagePool.photo(image_name)
you can get a list of every name in the pool with
deck = ImagePool.names()
or grab it while you also append a directory
deck = ImagePool.append_directory(path_to_folder).names()
That is very helpful to you, because you can simply shuffle and pop that list as the official "deck" in your game. Like this:
somecard['image'] = ImagePool.photo(deck.pop(0))
Assuming you will have more than just card graphics, but do not want to get all the images when creating a deck, there is a solution for that, as well.
first supply a prefix when appending the cards image directory, and then use that prefix when calling names(). As long as only card images were in the cards image directory, only card names will be returned.
deck = ImagePool.append_directory(path_to_folder, prefix='cards_').names('cards_')
notes:
allotted area is only allotted and in no way should be assumed to represent the final width and/or height of the actual image. The image will do whatever it has to to fit in the allotted space without losing it's original height and width ratio. If you don't supply allotted width and height the image will be it's full size.
All image and photo references are automatically maintained in ImagePool. There is no reason to store your own references
The entire class is static so references to all the images and photos can be accessed anywhere without an ImagePool instance. In other words, you never need to do this: images = ImagePool() and therefore never need to figure out how to get images into some class or other document.
No images actually load until you start requesting them, and then it happens automatically. This is a good thing. You have 52 cards, but if you only use ex: 6 in the first game ~ only 6 will fully load. Eventually all the cards will get played and be fully loaded, and you didn't have some huge burp in your game where you tried to fully create 52 cards all at once.
The ImagePool class has more features, but the ones explained here are all you should need for your game.
I'm trying to manage a directory tree which is created through a hierarchy of Python objects. I want to serialize the top-level object in JSON so I can share 2 things with users: the JSON file along with the directory. I'd like other users to be able to point to that directory, so the problem here is setting that root directory which might change on a different computer.
Here's an example of what I have right now:
import os.path as op
class Top():
def __init__(self, root_dir):
self._root_dir = root_dir
intop = InTop(self.base_dir)
self.intop = intop
#property
def root_dir(self):
return self._root_dir
#root_dir.setter
def root_dir(self, path):
self._root_dir = path
#property
def base_dir(self):
return op.join(self.root_dir, 'Top')
class InTop():
def __init__(self, root_dir):
self._intop_dir = op.join(root_dir, 'InTop')
#property
def intop_dir(self):
return self._intop_dir
#intop_dir.setter
def intop_dir(self, path):
self._intop_dir = path
I'm happy with how this works right now for updating the path in a Top object:
t = Top('~/projects/')
print(t.root_dir) # ~/projects/
print(t.base_dir) # ~/projects/Top
t.root_dir = '~/Downloads/'
print(t.root_dir) # ~/Downloads/
print(t.base_dir) # ~/Downloads/Top
But is there any way for that change to propagate to the InTop object?
t = Top('~/projects/')
print(t.root_dir) # ~/projects/
print(t.base_dir) # ~/projects/Top
print(t.intop.intop_dir) # ~/projects/Top/InTop
t.root_dir = '~/Downloads/'
print(t.root_dir) # ~/Downloads/
print(t.base_dir) # ~/Downloads/Top
print(t.intop.intop_dir) # ~/projects/Top/InTop <--- How to update this?
How do I get that last line to print "~/Downloads/Top/InTop" instead?
Perhaps there is a better way to manage relative file paths like this - if so please let me know.
Thanks in advance!
Figured it out..just needed to set it within the Top setter (also corrected my setters)
import os.path as op
class Top(object):
def __init__(self, root_dir):
self._root_dir = root_dir
intop_obj = InTop(self.top_dir)
self.intop = intop_obj
#property
def root_dir(self):
return self._root_dir
#root_dir.setter
def root_dir(self, path):
self._root_dir = path
self.intop.top_dir = self.top_dir
#property
def top_dir(self):
return op.join(self.root_dir, 'Top')
class InTop(object):
def __init__(self, top_dir):
self._top_dir = top_dir
#property
def top_dir(self):
return self._top_dir
#top_dir.setter
def top_dir(self, top_dir):
self._top_dir = top_dir
#property
def intop_dir(self):
return op.join(self.top_dir, 'InTop')
Gets me:
t = Top('~/projects/')
print(t.root_dir) # ~/projects/
print(t.top_dir) # ~/projects/Top
print(t.intop.intop_dir) # ~/projects/Top/InTop
t.root_dir = '~/Downloads/'
print(t.root_dir) # ~/Downloads/
print(t.top_dir) # ~/Downloads/Top
print(t.intop.intop_dir) # ~/Downloads/Top/InTop
Note: I see that I need to more clearly work out what it is that I want each property/descriptor/class/method to do before I ask how to do it! I don't think my question can be answered at this time. Thanks all for helping me out.
Thanks to icktoofay and BrenBarn, I'm starting to understand discriptors and properties, but now I have a slightly harder question to ask:
I see now how these work:
class Blub(object):
def __get__(self, instance, owner):
print('Blub gets ' + instance._blub)
return instance._blub
def __set__(self, instance, value):
print('Blub becomes ' + value)
instance._blub = value
class Quish(object):
blub = Blub()
def __init__(self, value):
self.blub = value
And how a = Quish('one') works (produces "Blub becomes one") but take a gander at this code:
import os
import glob
class Index(object):
def __init__(self, dir=os.getcwd()):
self.name = dir #index name is directory of indexes
# index is the list of indexes
self.index = glob.glob(os.path.join(self.name, 'BatchStarted*'))
# which is the pointer to the index (index[which] == BatchStarted_12312013_115959.txt)
self.which = 0
# self.file = self.File(self.index[self.which])
def get(self):
return self.index[self.which]
def next(self):
self.which += 1
if self.which < len(self.index):
return self.get()
else:
# loop back to the first
self.which = 0
return None
def back(self):
if self.which > 0:
self.which -= 1
return self.get()
class File(object):
def __init__(self, file):
# if the file exists, we'll use it.
if os.path.isfile(file):
self.name = file
# otherwise, our name is none and we return.
else:
self.name = None
return None
# 'file' attribute is the actual file object
self.file = open(self.name, 'r')
self.line = Lines(self.file)
class Lines(object):
# pass through the actual file object (not filename)
def __init__(self, file):
self.file = file
# line is the list if this file's lines
self.line = self.file.readlines()
self.which = 0
self.extension = Extension(self.line[self.which])
def __get__(self):
return self.line[self.which]
def __set__(self, value):
self.which = value
def next(self):
self.which += 1
return self.__get__()
def back(self):
self.which -= 1
return self.__get__()
class Extension(object):
def __init__(self, lineStr):
# check to make sure a string is passed
if lineStr:
self.lineStr = lineStr
self.line = self.lineStr.split('|')
self.pathStr = self.line[0]
self.path = self.pathStr.split('\\')
self.fileStr = self.path[-1]
self.file = self.fileStr.split('.')
else:
self.lineStr = None
def __get__(self):
self.line = self.lineStr.split('|')
self.pathStr = self.line[0]
self.path = self.pathStr.split('\\')
self.fileStr = self.path[-1]
self.file = self.fileStr.split('.')
return self.file[-1]
def __set__(self, ext):
self.file[-1] = ext
self.fileStr = '.'.join(self.file)
self.path[-1] = fileStr
self.pathStr = '\\'.join(self.path)
self.line[0] = self.pathStr
self.lineStr = '|'.join(self.line)
Firstly, there may be some typos in here because I've been working on it and leaving it half-arsed. That's not my point. My point is that in icktoofay's example, nothing gets passed to Blub(). Is there any way to do what I'm doing here, that is set some "self" attributes and after doing some processing, taking that and passing it to the next class? Would this be better suited for a property?
I would like to have it so that:
>>> i = Index() # i contains list of index files
>>> f = File(i.get()) # f is now one of those files
>>> f.line
'\\\\server\\share\\folder\\file0.txt|Name|Sean|Date|10-20-2000|Type|1'
>>> f.line.extension
'txt'
>>> f.line.extension = 'rtf'
>>> f.line
'\\\\server\\share\\folder\\file0.rtf|Name|Sean|Date|10-20-2000|Type|1'
You can do that, but the issue there is less about properties/descriptors and more about creating classes that give the behavior you want.
So, when you do f.line, that is some object. When you do f.line.extension, that is doing (f.line).extension --- that is, it first evalautes f.line and then gets the extension attribute of whatever f.line is.
The important thing here is that f.line cannot know whether you are later going to try to access its extension. So you can't have f.line do one thing for "plain" f.line and another thing for f.line.extension. The f.line part has to be the same in both, and the extension part can't change that.
The solution for what you seem to want to do is to make f.line return some kind of object that in some way looks or works like a string, but also allows setting attributes and updating itself accordingly. Exactly how you do this depends on how much you need f.lines to behave like a string and how much you need it to do other stuff. Basically you need f.line to be a "gatekeeper" object that handles some operations by acting like a string (e.g., you apparently want it to display as a string), and handles other objects in custom ways (e.g., you apparently want to be able to set an extension attribute on it and have that update its contents).
Here's a simplistic example:
class Line(object):
def __init__(self, txt):
self.base, self.extension = txt.split('.')
def __str__(self):
return self.base + "." + self.extension
Now you can do:
>>> line = Line('file.txt')
>>> print line
file.txt
>>> line.extension
'txt'
>>> line.extension = 'foo'
>>> print line
file.foo
However, notice that I did print line, not just line. By writing a __str__ method, I defined the behavior that happens when you do print line. But if you evaluate it "raw" without printing it, you'll see it's not really a string:
>>> line
<__main__.Line object at 0x000000000233D278>
You could override this behavior as well (by defining __repr__), but do you want to? That depends on how you want to use line. The point is that you need to decide what you want your line to do in what situations, and then craft a class that does that.